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.

  11. added a commit that references this issue on Nov 14, 2026
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