You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Prelude to #210: retire redundant payload origin-typing (Kotlin, Python) and close Java's KNOWN_GAPS entry as moot #270
Payload-origin handling diverges across the five ports today, and the divergence is
load-bearing for nothing. Retiring it is output-identical and de-risks #210.
Port
Today
Kotlin
KotlinPayloadGenerator resolves origin.* for payload field typing
Python
payload_vo_generator resolves origin.* for payload field typing
TypeScript
origin-blind
C#
origin-blind
Java
standing KNOWN_GAPS deferral (codegen-spring/.../KNOWN_GAPS.md), also listed as an open question in CLAUDE.md
The divergence is benign only because #185 made origin.passthrough type-preserving
(ERR_PASSTHROUGH_TYPE_MISMATCH, with @convert: true as the opt-out). A passthrough field's
declared type must already match its @from source, so deriving the payload field's type through
the origin is redundant with reading the declared type — the two can no longer disagree.
Why now
This is the first of two non-breaking preludes to #210 (assembly origins retire from object.value). Doing it first means #210's payload-tier change lands against five ports that
agree, instead of three different behaviours. Payload emission is this repo's recent cross-port
bug hotspot — ADR-0044 (#219/#220) took three stages to land, and #228 found the extract tier
resolving payload refs package-blind — so reducing the surface before the breaking change is
worth more than the diff suggests.
Scope
Kotlin — delete the origin-resolution path in KotlinPayloadGenerator; type payload fields
from the declared field type.
Python — same in payload_vo_generator.
Java — close the KNOWN_GAPS payload-origin.* entry as moot (not "deferred"), and
drop the matching open question from CLAUDE.md.
TypeScript / C# — no change; verify they stay origin-blind.
Gate
Output must be byte-identical in every port — this is a dead-code removal, not a behaviour
change. Pin it: run the render / payload conformance corpora before and after and diff the
emitted payload records. Any byte that moves means the redundancy claim above is wrong for some
shape, and that shape belongs in a fixture before this lands.
Non-goals
No metamodel vocabulary change. origin.* stays licensed on field.base; nothing in expected-registry.json moves.
Premise falsified — needs recon before any deletion. This issue was scoped as retiring "redundant" payload origin-typing in Kotlin/Python, on the grounds that #185 made origin.passthrough type-preserving so deriving the payload field's type through the origin is redundant with reading the declared type.
That holds for passthrough. It does not hold for the rest of what those generators do:
There the origin genuinely determines the type and the declared field type cannot express it. The corpus exercises both: fixtures/conformance/origin-collection-simple and fixtures/conformance/flattened-kitchen-sink each carry origins on an object.value.
So deleting the handling would not be byte-identical (this issue's own stated gate). It would either break real type derivation, or make Kotlin/Python match TypeScript's origin-blindness — and TS being origin-blind may be the actual defect rather than the baseline worth converging on. That is the open question this issue should answer first.
Same failure mode as #271, which was also filed on a premise that recon overturned. Recommend: recon what each port derives from payload origins today and which behavior is correct, then decide what (if anything) retires.
Why declared-type-authoritative is right. The loader already guarantees declared ≡ derived everywhere it is checkable: passthrough (#185ERR_PASSTHROUGH_TYPE_MISMATCH), computed (ERR_COMPUTED_TYPE_MISMATCH), first, and aggregate shape (#195). In the two places it does not pin, the origin-derivers are on the wrong side:
origin.collection discards the declared @objectRef and substitutes the @via relationship's entity. On fixtures/conformance/origin-collection-simple — declared field.object isArray @objectRef PostBrief, @via Author.posts → entity Post — Kotlin/Python emit List<PostPayload> shaped from the full entity while TS/C# emit the declared PostBrief. That silently defeats the payload-bloat contract ("the declared field set IS the exposure"), which is the point of the prompt pillar.
Numeric width — a declared field.int with @agg count loads legally; Kotlin hardwires LONG, contradicting the declaration.
Python's own test encodes the pathology: a declared field.string asserted as list[PostPayload] (test_payload_vo_generator.py:304). Nothing gates payload typing cross-port — render-conformance has zero origin fixtures — which is how a 3-vs-2 divergence survived.
Re-scoped work: delete the Kotlin/Python payload origin dispatch, including the origin.collection walk in each port's ADR-0044 name-map closure and the extract tier (which shares the name map, #228) — they must move in lockstep per port. Nullability falls back to declared @required. Add a declared-VO-vs-@via-entity disagreement fixture per port asserting the declared shape wins. Also closes the repo-root CLAUDE.md open question ("Payload origin.* resolution in codegen-spring") as moot — the KNOWN_GAPS.md entry it cites no longer exists.
Scope: Maven + PyPI product code; rides the next coordinated cut. Nothing from this shipped in 0.20.15.
Stop signal for whoever executes: if any Kotlin/Python payload snapshot shows declared and derived disagreeing beyond the collection/width cases above, stop before deleting — a shipped consumer could be relying on origin-derived typing.
Shipped in 0.20.16 / 7.20.16 — a coordinated PATCH across all four registries (npm 0.20.16 · PyPI 0.20.16 · NuGet 0.20.16 · Maven 7.20.16).
Merged as #274 (18792e52); commits c5ad0802 → 2d67696d → 34705ce9 → 608f4217 → 9af7558b → ebd71ebf. local-ci green 9/9 on the merge SHA.
Scope correction: this issue's premise was wrong about Java
The issue (and the ruling in spec/roadmap.md) recorded TS / C# / Java as origin-blind reference emitters, with only Kotlin and Python affected. That is false for Java.SpringPayloadGenerator carried the identical origin dispatch — imports at L12–16, the contract documented in its Javadoc at L63–75, dispatch at L453–461, @agg count→Long at L531–532, the @via walk at L552, plus a CollectionOrigin edge in the #228-shared ADR-0044 name-map closure at L255–272 that resolved its via-target with no subtype filter at all. SpringPayloadGeneratorTest pinned the pathology behaviorally. TypeScript and C# were independently re-verified as genuinely origin-blind.
Root cause of the miss, worth recording: the file contained two raw NUL bytes in string literals (composite map-key delimiters written as literal 0x00 rather than the \0 escape). That made it test as binary, so binary-skipping search tools returned nothing for Origin — read as "zero matches" when real grep -c reports 22. Both NUL bytes are now proper escapes (runtime-identical), so the file is greppable again.
Java was fixed here rather than the ruling being narrowed, because #210 makes prompt payloads object.projections and projections legitimately carry assembly origins — an origin-dispatching payload emitter would fire on the shape about to become the norm.
What shipped
All three affected ports (Kotlin, Python, Java) now type a payload field only from its declared field.<subType> + @isArray + @objectRef, never derive nullability from origin semantics, and walk the nested-payload closure only over declared field.object @objectRef edges. A field carrying any origin.* child types exactly as if the child were absent; a non-object field carrying origin.collection contributes no nested class. The origin.collection edge is deleted in lockstep from the ADR-0044 name-map closure each port shares with its extract tier (#228).
Gating. A new disagreement test per port: a field declared field.object @objectRef: <CuratedVO> @isArray that also carries an origin.collection @via at a different, fuller entity now asserts the declared curated VO wins and that the closure emits the curated VO's class, not the entity's. Each was observed RED against the old behavior first (pre-fix Python emitted posts: list[PostPayload] | None = None). A separate two-package collision-naming gate per port pins the closure edit itself, which was initially ungated because both emitters fall back to a bare <Short>Payload on a name-map miss — so the disagreement tests alone would have passed even if the closure returned nothing.
TypeScript and C# — the reference emitters — were previously ungated entirely, which is how this divergence survived long enough to be written into a ruling as settled fact. Each now carries a regression pin. Test-only: neither port has a changed product file, so npm and NuGet are version-parity bumps.
Byte-identical proof:server/java/codegen-kotlin/src/test/resources/snapshots/payload-with-origins/** is unchanged — its fixture is constructed so declared == derived on every field, making it the declared==derived gate. All other Kotlin snapshot output unchanged.
Adopter-visible
Java: a payload component declaring a plain scalar array changes from T to java.util.List<T> — array-ness was being dropped (Kotlin and Python already wrapped it; Java honored @isArray for enums and object refs but not plain scalars). Generated api-docs correspondingly document such a component as optional rather than required.
Kotlin:origin.first / origin.computed were the only producers of a nullable T? payload property and both now emit T. Kotlin's payload emitter has never read @required, so there is currently no declared route to a nullable payload property in that port — a generated @Serializabletemplate.output class will now throw on a JSON null where it previously yielded null.
Python: nested payload class docstrings now read "object field target" rather than the now-false "collection target".
Also closes the long-standing codegen-spring "payload origin.* resolution" open question as moot — origin-blindness is the intended contract, not a gap.
Activity
docs(roadmap): record the #210 ruling and its two blocking preludes
dmealing commentedon Aug 6, 2026
Premise falsified — needs recon before any deletion. This issue was scoped as retiring "redundant" payload origin-typing in Kotlin/Python, on the grounds that #185 made
origin.passthroughtype-preserving so deriving the payload field's type through the origin is redundant with reading the declared type.That holds for
passthrough. It does not hold for the rest of what those generators do:origin.aggregate—@agg count→Long,@agg avg→Doubleorigin.collection—@via→List<TargetPayload>There the origin genuinely determines the type and the declared field type cannot express it. The corpus exercises both:
fixtures/conformance/origin-collection-simpleandfixtures/conformance/flattened-kitchen-sinkeach carry origins on anobject.value.So deleting the handling would not be byte-identical (this issue's own stated gate). It would either break real type derivation, or make Kotlin/Python match TypeScript's origin-blindness — and TS being origin-blind may be the actual defect rather than the baseline worth converging on. That is the open question this issue should answer first.
Same failure mode as #271, which was also filed on a premise that recon overturned. Recommend: recon what each port derives from payload origins today and which behavior is correct, then decide what (if anything) retires.
dmealing commentedon Aug 6, 2026
Recon complete — re-scoped. This is a bug fix, not a redundancy retirement, and the direction is the opposite of what this issue assumed.
TypeScript's origin-blindness is correct; Kotlin's and Python's origin-derived payload typing is the defect.
What each port does today
@objectRefnestingcodegen-ts/src/payload-codegen.tsMetaObjects.Codegen/PayloadCodegen.cscodegen-spring/.../SpringPayloadGenerator.javacodegen-kotlin/.../KotlinPayloadGenerator.kt(resolveCollectionType)payload_vo_generator.py(_resolve_collection_type)Why declared-type-authoritative is right. The loader already guarantees declared ≡ derived everywhere it is checkable: passthrough (#185
ERR_PASSTHROUGH_TYPE_MISMATCH), computed (ERR_COMPUTED_TYPE_MISMATCH), first, and aggregate shape (#195). In the two places it does not pin, the origin-derivers are on the wrong side:origin.collectiondiscards the declared@objectRefand substitutes the@viarelationship's entity. Onfixtures/conformance/origin-collection-simple— declaredfield.object isArray @objectRef PostBrief,@via Author.posts→ entityPost— Kotlin/Python emitList<PostPayload>shaped from the full entity while TS/C# emit the declaredPostBrief. That silently defeats the payload-bloat contract ("the declared field set IS the exposure"), which is the point of the prompt pillar.field.intwith@agg countloads legally; Kotlin hardwiresLONG, contradicting the declaration.Python's own test encodes the pathology: a declared
field.stringasserted aslist[PostPayload](test_payload_vo_generator.py:304). Nothing gates payload typing cross-port — render-conformance has zero origin fixtures — which is how a 3-vs-2 divergence survived.Re-scoped work: delete the Kotlin/Python payload origin dispatch, including the
origin.collectionwalk in each port's ADR-0044 name-map closure and the extract tier (which shares the name map, #228) — they must move in lockstep per port. Nullability falls back to declared@required. Add a declared-VO-vs-@via-entity disagreement fixture per port asserting the declared shape wins. Also closes the repo-rootCLAUDE.mdopen question ("Payloadorigin.*resolution in codegen-spring") as moot — theKNOWN_GAPS.mdentry it cites no longer exists.Scope: Maven + PyPI product code; rides the next coordinated cut. Nothing from this shipped in
0.20.15.Stop signal for whoever executes: if any Kotlin/Python payload snapshot shows declared and derived disagreeing beyond the collection/width cases above, stop before deleting — a shipped consumer could be relying on origin-derived typing.
docs(plan): resumable execution plan for the projection/payload vocab…
Merge pull request #274 from metaobjectsdev/fix/270-declared-type-aut…
chore(release): 0.20.16 coordinated (npm/PyPI/NuGet 0.20.16 · Maven 7…
docs: propagate 0.20.16 / 7.20.16
dmealing commentedon Aug 7, 2026
Shipped in
0.20.16/7.20.16— a coordinated PATCH across all four registries (npm0.20.16· PyPI0.20.16· NuGet0.20.16· Maven7.20.16).Merged as #274 (
18792e52); commitsc5ad0802→2d67696d→34705ce9→608f4217→9af7558b→ebd71ebf.local-cigreen 9/9 on the merge SHA.Scope correction: this issue's premise was wrong about Java
The issue (and the ruling in
spec/roadmap.md) recorded TS / C# / Java as origin-blind reference emitters, with only Kotlin and Python affected. That is false for Java.SpringPayloadGeneratorcarried the identical origin dispatch — imports at L12–16, the contract documented in its Javadoc at L63–75, dispatch at L453–461,@agg count→Longat L531–532, the@viawalk at L552, plus aCollectionOriginedge in the #228-shared ADR-0044 name-map closure at L255–272 that resolved its via-target with no subtype filter at all.SpringPayloadGeneratorTestpinned the pathology behaviorally. TypeScript and C# were independently re-verified as genuinely origin-blind.Root cause of the miss, worth recording: the file contained two raw
NULbytes in string literals (composite map-key delimiters written as literal0x00rather than the\0escape). That made it test as binary, so binary-skipping search tools returned nothing forOrigin— read as "zero matches" when realgrep -creports22. Both NUL bytes are now proper escapes (runtime-identical), so the file is greppable again.Java was fixed here rather than the ruling being narrowed, because #210 makes prompt payloads
object.projections and projections legitimately carry assembly origins — an origin-dispatching payload emitter would fire on the shape about to become the norm.What shipped
All three affected ports (Kotlin, Python, Java) now type a payload field only from its declared
field.<subType>+@isArray+@objectRef, never derive nullability from origin semantics, and walk the nested-payload closure only over declaredfield.object @objectRefedges. A field carrying anyorigin.*child types exactly as if the child were absent; a non-object field carryingorigin.collectioncontributes no nested class. Theorigin.collectionedge is deleted in lockstep from the ADR-0044 name-map closure each port shares with its extract tier (#228).Gating. A new disagreement test per port: a field declared
field.object @objectRef: <CuratedVO> @isArraythat also carries anorigin.collection @viaat a different, fuller entity now asserts the declared curated VO wins and that the closure emits the curated VO's class, not the entity's. Each was observed RED against the old behavior first (pre-fix Python emittedposts: list[PostPayload] | None = None). A separate two-package collision-naming gate per port pins the closure edit itself, which was initially ungated because both emitters fall back to a bare<Short>Payloadon a name-map miss — so the disagreement tests alone would have passed even if the closure returned nothing.TypeScript and C# — the reference emitters — were previously ungated entirely, which is how this divergence survived long enough to be written into a ruling as settled fact. Each now carries a regression pin. Test-only: neither port has a changed product file, so npm and NuGet are version-parity bumps.
Byte-identical proof:
server/java/codegen-kotlin/src/test/resources/snapshots/payload-with-origins/**is unchanged — its fixture is constructed so declared == derived on every field, making it the declared==derived gate. All other Kotlin snapshot output unchanged.Adopter-visible
Ttojava.util.List<T>— array-ness was being dropped (Kotlin and Python already wrapped it; Java honored@isArrayfor enums and object refs but not plain scalars). Generated api-docs correspondingly document such a component as optional rather than required.origin.first/origin.computedwere the only producers of a nullableT?payload property and both now emitT. Kotlin's payload emitter has never read@required, so there is currently no declared route to a nullable payload property in that port — a generated@Serializabletemplate.outputclass will now throw on a JSONnullwhere it previously yieldednull.Also closes the long-standing
codegen-spring"payloadorigin.*resolution" open question as moot — origin-blindness is the intended contract, not a gap.See
CHANGELOG.md[0.20.16].docs(FR-037): decide the open items; reject the SQL escape for a sema…
fix(codegen): payload optionality comes from @required, in every port (…
feat(metamodel)!: origin.collection retires to reserved-not-registered (