Skip to content

Retire assembly origins on object.value (one spelling of derived read shape) — RULED: batch into the pre-1.0 consolidation #210

Closed

Description

@dmealing

Summary

There are currently two spellings of "derived read shape": object.projection (+ origin.*, the SQL/read-model path) and object.value (+ origin.*, used for FR-004 prompt/template payloads). FR-024 §1 states outright that these arose "by accident of which feature shipped them," migrated one (entity+extends+view), and spared the other. The value spelling is a grandfathered compatibility spelling, not a principled category — and it leaves a permanent "why are there two?" seam every adopter re-asks.

The principled boundary (direction of data flow)

  • Projection = system-PRODUCED output. Derived from the entity graph, read-only at the subtype level, optionally addressable (borrowed identity ⇒ get-by-id) and/or materialized, referenced as an operation's outputRef.
  • Value = caller-CONSTRUCTED input / embedded shape. Command inputs, proc args (@parameterRef), embedded VOs whose storage belongs to the owning entity's field. Never populated, never sourced, never keyed; content equality.

A payload the framework assembles from Author.name + count(posts) + a nested collection is system-produced derived output — a projection by every test above. Calling its assembly "construction" stretches the population doctrine past usefulness (templates-and-payloads.md notes the payload codegen path is already shared with the projection path).

Proposal (pre-1.0, within ADR-0035's window)

  • Retire origin.* on object.value.
  • Let template.* @payloadRef accept sourceless projections.
  • Nested payload shapes become sourceless projections reached by origin.collection.

Then value purity becomes total — no identity, no source, no origins — so the value rule regime is trivially checkable (child-licensing, not conditional rules), and the ecosystem has exactly one spelling of "derived read shape."

Cost / benefit

Payload-VO codegen paths + conformance fixtures churn across 5 ports — but mostly node-type relabeling, since the payload path already shares the projection codegen path. Benefit: the projection/value boundary stops being a recurring question, and "value" becomes a clean, total-purity category. If the churn is declined, the fallback rule of thumb ("origins on a value are legal only inside template-payload assembly; any top-level, independently addressable derived read shape is a projection") leaves the seam permanently.

Activity

  1. dmealing commented on Aug 5, 2026

    @dmealing
    MemberAuthor

    Ruling (2026-08-05): DO IT — but not yet, and not as written

    Trigger: the close of the Phase-1 feedback window, landed as part of the single pre-1.0
    breaking-consolidation batch. Never as a standalone release. If nothing else breaking has
    surfaced within ~90 days of the launch, it ships alone as that batch.

    Scope — narrower than the original proposal

    The proposal above retires all origin.* on object.value. That is broader than the doctrine
    the loader already encodes, and broader than the seam actually justifies.

    • Retire the ASSEMBLY origins on values — origin.aggregate, origin.computed,
      origin.collection, origin.first become illegal on a value-hosted field (a new
      ERR_SUBTYPE_RULE_VIOLATION case).

    • Keep origin.passthrough on values. FR-015 parameter VOs (source.rdb @parameterRef)
      stay exactly as they are. The loader already draws this line —
      validation-passes.ts validateOriginPaths:

      FR-024 B5: object.value hosts are EXEMPT from @via inference and cardinality checks —
      a value's origin.passthrough is FR-015 parameter lineage (values are constructed,
      never assembled; spec §7), not an assembly path.

      A proc arg is caller-constructed input; routing it through object.projection would violate
      the direction-of-flow principle this issue itself invokes. Retiring its lineage would also
      silently drop the _checkPassthroughType / ERR_PASSTHROUGH_TYPE_MISMATCH type-preservation
      check on proc args — a real regression, not a cleanup.

    • Widen @payloadRef / @responseRef to accept an object.projection carrying zero
      source.* children. Today this is a hard check on OBJECT_SUBTYPE_VALUE
      (validation-passes.ts, gated by fixtures/conformance/error-template-payload-ref-not-value,
      whose meaning flips).

    • No new vocabulary required. origin.collection is already registered (required @via),
      and a sourceless projection loads legally today — validateProjectionLicensing has no
      source-required rule and validateDerivedFieldProvidability exempts projections. This sits in
      the ADR-0046 / Non-persisted shapes (messages/DTOs) can't declare field-level references to entities #238 cost class: a child-licensing + validation-rule change.

    • But expected-registry.json DOES change. object.value's rules string reads
      "constructed — by a caller, by assembly, or by embedding". That string is byte-checked, so
      all five ports' embedded definitions sync in lockstep.

    Drop the "total value purity" framing. It was never achievable: ADR-0046's @enforce: false
    navigation-only reference already made value rules conditional, and parameter lineage keeps them
    so. The post-ruling rule is better because it is subtype-scoped rather than context-scoped:

    Passthrough on a value is lineage. Assembly origins live on projections.

    Why do it at all

    1. Near-zero usage, and that only worsens. A scan of every conformance fixture, the
      examples/ tree and the public reference app found a handful of fixture dirs and one teaching
      example (examples/advanced-modeling ProgramDescriptionPayload). The reference app — the
      heaviest prompt-pillar consumer, 18 files of payload VOs — uses zero. Migration cost is at
      its historical minimum.
    2. This is a live behavioral divergence, not a prose seam. Payload-origin handling already
      differs across ports: Kotlin (KotlinPayloadGenerator) and Python (payload_vo_generator)
      resolve origins for payload field typing; TS and C# are origin-blind; Java carries a standing
      KNOWN_GAPS deferral. It is only benign because verify: origin.passthrough field type must match its source unless an explicit transform is declared #185's ERR_PASSTHROUGH_TYPE_MISMATCH makes
      origin-derived typing redundant with declared typing. That debt gets paid either way — finished
      in five ports, or retired in five. Retiring is strictly cheaper and deletes the open question.
    3. Declining does not avoid the cost, it defers it into a post-1.0 world where it becomes a
      Metamodel 2.0 event.

    Why not now

    "Mostly node-type relabeling" is half true. Fixture churn is trivially small. The code churn
    sits in the payload tier — this repo's recent cross-port bug hotspot (ADR-0044 #219/#220 took
    three stages; #228 found the extract tier resolving payload refs package-blind, both latent behind
    green suites). The riskiest piece: every port's projection fan-out must cleanly no-op on a
    sourceless projection, and #248 ("persistability derives from a declared source.*") shipped
    npm-only, so four ports have never seen one.

    Honest estimate: comparable to #228 plus a breaking loader rule — a 1–2 week focused effort with
    the no-mistakes gate, with the real risk concentrated in the four non-TS ports' dispatch rather
    than in diff size. That is not work to discover during a launch that showcases the prompt pillar.

    Sequencing

    1. Phase-1 launch ships first, untouched, on 0.20.x.
    2. During the feedback window, land two free, non-breaking preludes that de-risk the real
      work (filed separately, both blocking this issue):
    3. Then this rides the one pre-1.0 consolidation batch, before any quiet-period declaration.
      ADR-0035 §3: a breaking move during the run-up restarts the quiet-period clock, so it must be
      batched, and it must land while 1.0 remains unscheduled — which it explicitly is.

    What would reverse this ruling

    • Phase-1 feedback showing real adopters authoring assembly origins on payload VOs. Each one
      raises migration cost and argues the pattern is loved rather than vestigial; more than a couple
      → decline and document the rule-of-thumb instead.
    • The Persistability is decided by a hardcoded subType === "value" compare — any custom object subtype silently becomes a table #248 prelude revealing that any port's projection fan-out cannot cleanly no-op on a
      sourceless projection without new vocabulary (e.g. a marker distinguishing a wire-assembled
      projection from a DB one). New vocabulary is a different cost class under ADR-0023 and reopens
      the call.
    • 1.0 being scheduled imminently off strong Phase-1 signal. Then the seam is not worth restarting
      the clock: ship 1.0, document the rule-of-thumb, and let this be a 2.0 candidate.
  2. changed the title [-]Total value purity: retire origin.* on object.value (one spelling of derived read shape)[/-] [+]Retire assembly origins on object.value (one spelling of derived read shape) — RULED: batch into the pre-1.0 consolidation[/+] on Aug 5, 2026
  3. dmealing commented on Aug 5, 2026

    @dmealing
    MemberAuthor

    Preludes filed and blocking this issue: #270 (retire redundant payload origin-typing in Kotlin/Python, close Java's KNOWN_GAPS entry as moot) and #271 (port #248's persistability-from-source contract cross-port; prove each fan-out no-ops on a sourceless projection). #271 also carries the falsification signal — if any port needs new vocabulary to handle a sourceless projection, it reopens this ruling.

  4. added a commit that references this issue on Aug 5, 2026
  5. dmealing commented on Aug 5, 2026

    @dmealing
    MemberAuthor

    Batch scope note: the pre-1.0 consolidation batch this issue rides now also carries the @role registry change from #212 — removing publish from source.rdb @role's allowedValues (live in expected-registry.json, spec/metamodel/db.json, and five ports' embedded definitions), and possibly the wider shrink to primary | replica pending the sub-decision recorded there. Same class as this issue's object.value rules-string change: lockstep across five ports, technically breaking, zero fixtures affected. Batching them means one window, one review.

  6. added a commit that references this issue on Aug 5, 2026
  7. dmealing commented on Aug 5, 2026

    @dmealing
    MemberAuthor

    Batch scope, updated: the @role change riding this batch is now the full shrink to primary | replica (ruled on #212), not just the publish removal. Cross-port verification closed the gate — every read of @role in all five ports is an equality test against primary; Java OMDB has zero role usage, and Kotlin/Python's write-through read paths are explicitly documented "role-agnostic" (they find the replica by @kind). index/cache/publish/mirror become reserved-not-registered per the ADR-0040 precedent. One pre-landing check inherited from that ruling: these are shipped registered members, so re-run ADR-0036's adopter scan plus the public reference app before removing them — zero uses were verified in this repo only.

  8. added 3 commits that reference this issue on Aug 5, 2026
  9. dmealing commented on Aug 6, 2026

    @dmealing
    MemberAuthor

    Correction to this ruling's prelude framing. It describes #270 as "output-identical post-#185." That is false, and the correction makes the prelude more necessary rather than less.

    #270 is a bug fix: Kotlin's and Python's payload origin.collection handling discards the field's declared @objectRef and substitutes the @via relationship's entity shape, so a declared curated VO silently becomes the full entity — defeating the payload-bloat contract this pillar exists to enforce. TypeScript's origin-blindness is the correct baseline, not the gap. Full recon on #270.

    Bearing on this issue: #210 does not subsume #270, and the dependency direction is unchanged. Post-#210, @payloadRef widens to sourceless projections whose fields are also explicitly declared, so declared-driven payload emission is the right contract through and after the transition — no port ever needs payload-tier origin typing.

    One item to fold into this batch's scope: validateOriginPaths has no origin.collection branch at all — collection is the one origin subtype whose declared-vs-derived agreement the loader never checks, which is exactly why the Kotlin/Python divergence went unnoticed. Since this batch retires assembly origins (including collection) from object.value, the validation gap dies with it on values; if collection remains reachable on projections, the agreement check belongs here rather than as a separate issue.

  10. dmealing commented on Aug 8, 2026

    @dmealing
    MemberAuthor

    Shipped in 0.21.0 / 7.21.0 — a coordinated MINOR across all four registries (npm 0.21.0 · PyPI 0.21.0 · NuGet 0.21.0 · Maven 7.21.0), the pre-1.0 breaking slot.

    Merged as #276; commits 6c73e77a → e0e561ab → 9c4fbadd → 286bba78 → 03a7eed7. local-ci green 9/9 on the merge SHA.

    Migration guide: docs/features/migrations/value-assembly-origins-and-source-role-shrink.md, also reachable from the 0.x-to-1.0 hub §7.

    What shipped

    origin.aggregate, origin.computed, origin.collection and origin.first on an object.value-hosted field now fail load with ERR_SUBTYPE_RULE_VIOLATION in all four loaders. origin.passthrough stays legal on a value, exactly as the ruling required — there it is FR-015 parameter lineage, not assembly, and retiring it would have silently dropped the ERR_PASSTHROUGH_TYPE_MISMATCH check on stored-proc arguments. The durable rule is now encoded in ADR-0028: "passthrough on a value is lineage; assembly origins live on projections."

    The migration path is additive: @payloadRef/@responseRef accept a sourceless object.projection, so a payload that was assembling values re-hosts as a projection and keeps its origins.

    Implementation note worth recording. Rather than patching four subtype branches per port, the ban is one named ASSEMBLY_ORIGIN_SUBTYPES set hoisted above the origin dispatch in every loader — so cross-port coverage is a property of the constant rather than of locating four sites, and a future fifth assembly origin is caught automatically. Both TS and Python tests assert set-equality between their fixture tables and that constant, closing a children: [undefined] vacuity hole. Now-unreachable value-host arms were removed with comments naming why, not left behind.

    A third breaking change surfaced in review

    The ruling anticipated two. Final review found a third: a payload's nested field.object @objectRef must now target an object.value. TypeScript, C# and Python did not filter nested targets before — that shape loaded and emitted a nested interface from the entity. It is now a fail-closed loader rule (one rule, four loaders) rather than five per-port codegen filters, reusing ERR_SUBTYPE_RULE_VIOLATION. It carries its own section and rewrite rule in the guide.

    An independent adjudication earlier in this batch is why it landed in the loader: filtering one port's payload closure without its extract closure would have emitted mappers constructing classes that no longer exist — strictly worse than the bloat it removed.

    Also fixed, found by the release gate

    runner.ts's dbImport/dialect guard keyed on subType !== OBJECT_SUBTYPE_VALUE, so a sourceless projection — the shape this release's own migration guide tells adopters to create — would have demanded database config and thrown (Only a value-object-only model may omit them.) for an object that provably emits zero DB code. It now keys on hasAnyRdbSource, which is #248's persistability-from-source doctrine and was the last subtype-based persistability check in the codebase. Regression test covers the exact migration-guide shape.

    Preludes

    Both closed. #270 shipped in 0.20.16 (and turned out to affect Java too, which its own text had recorded as a correct reference port). #271 was discharged — its premise was falsified by recon; every port already accepted a sourceless projection with no new vocabulary, retiring this ruling's ADR-0023 falsifier.

    Gates

    No new vocabulary, attributes or error codes (ADR-0023). Registry manifest diff confined to three object.value/object.projection doctrinal strings. All codegen golden output and every Kotlin snapshot byte-identical — including the re-hosted payload-with-origins snapshot, which regenerated byte-for-byte because #270 had already made those emitters declared-type-authoritative. Conformance corpus 262 → 270.

    Seven new fixtures for this issue: error-value-origin-{aggregate,collection,computed,first}, template-payload-ref-sourceless-projection, error-template-payload-ref-sourced-projection, error-payload-nested-object-ref-entity.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:metamodelMetamodel vocabulary / loaderenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions