Skip to content

feat(metamodel): origin.passthrough is type-preserving unless @convert#186

Merged
dmealing merged 3 commits into
mainfrom
passthrough-type-preservation
Jul 9, 2026
Merged

feat(metamodel): origin.passthrough is type-preserving unless @convert#186
dmealing merged 3 commits into
mainfrom
passthrough-type-preservation

Conversation

@dmealing

@dmealing dmealing commented Jul 9, 2026

Copy link
Copy Markdown
Member

Intent

Implement GitHub issue #185: make origin.passthrough type-preserving across all five language ports. A projection/view field forwarding another field's value via origin.passthrough @from must declare the same field. and array-ness as its resolved source field; a divergence fails load with a new ERR_PASSTHROUGH_TYPE_MISMATCH. Catches the mismodeling where a field.uuid source is surfaced as field.string on a view (leaving it String-typed end-to-end, forcing hand-written String<->UUID coercion) — verify --db can't see it (the column really is uuid); only the metadata knows both ends.

KEY DELIBERATE DECISIONS (not mistakes — treat as intended):

  1. Compare subType + array-ness ONLY, NOT nullability. An outer-join view legitimately widens NOT NULL -> nullable, so a nullability check would false-positive (repo has a >15% FP kill bar). The check builds a 'field.[]' label and compares labels.
  2. GENERALIZE + RETIRE the pre-existing narrow ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH (FR-015 stored-proc @parameterRef only) into ONE host-agnostic ERR_PASSTHROUGH_TYPE_MISMATCH in the origin-paths pass (runs over every object incl. parameter-ref values). Repointed fixture + removed narrow-check blocks + retirement comments are intentional.
  3. Escape hatch is a NEW boolean attr @convert on origin.passthrough — ACKNOWLEDGEMENT ONLY, suppresses the error, does NOT generate a cast (consumer owns coercion). Deliberately NOT tied to origin.expression: computed projection columns (post-1.0, additive) #159/origin.expression (unbuilt/post-1.0). Registered as a first-class provider attr in all 5 ports (ADR-0023) and byte-matches the shared registry-conformance manifest.
  4. Reads @convert via own-accessor (origin.* never inherits, ADR-0029), compares subType/isArray via RESOLVING/effective accessors (ADR-0039). Deliberate + commented.
  5. Also fixed a pre-existing generator gap: 'index' concept missing from generate-embedded-metamodel.ts CONCEPT_DIRS; adding it normalized the index embedded file header comment (body byte-identical).

Fixtures-first TDD: 3 new conformance fixtures as the cross-port oracle. Ported to Python/C#/Java (Kotlin shares Java's loader) — all green (TS 459, Python 340, Java 438, C# 718). 4-angle code-simplifier pass applied and re-verified. BREAKING metamodel change: adopter metadata with a divergent passthrough now fails load — intended. CHANGELOG breaking note + agent-context authoring skill + regenerated goldens/registry/metamodel-docs all included.

What Changed

  • origin.passthrough is now type-preserving across all five ports (TS/C#/Java/Python; Kotlin shares Java's loader): a projection/view field forwarding another field's value via @from must declare the same field.<subType> and array-ness as its resolved source. A divergence (e.g. a field.uuid source surfaced as field.string on a view) now fails load with the new generalized ERR_PASSTHROUGH_TYPE_MISMATCH, raised in the origin-paths validation pass that runs over every object. SubType + array-ness are compared only — nullability is deliberately excluded so a legitimate NOT NULL → nullable outer-join widening does not false-positive.
  • Generalizes and retires the narrow FR-015 ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH (stored-proc @parameterRef only) into the single host-agnostic ERR_PASSTHROUGH_TYPE_MISMATCH; the old narrow-check blocks are removed and the parameter-ref fixture is repointed to the generalized code. Adds a new boolean @convert attr on origin.passthrough as an acknowledgement-only escape hatch that suppresses the error (consumer owns coercion; no cast is generated), registered as a first-class provider attr in all five ports and byte-matched to the shared registry-conformance manifest.
  • Ships 3 new cross-port conformance fixtures as the oracle, regenerated goldens/metamodel-docs/ERROR-CODES, a CHANGELOG breaking note, and synced authoring-skill + migration/porting docs. Also fixes a pre-existing generator gap (index concept missing from generate-embedded-metamodel.ts CONCEPT_DIRS). This is a breaking metamodel change: adopter metadata with a divergent passthrough now fails to load.

Risk Assessment

✅ Low: A well-bounded, cross-port-conformance-gated loader change with consistent TS/C#/Java/Python implementations, correct ADR-0039 own-vs-resolving accessor usage, full coverage preservation of the retired narrow check, and a documented @convert escape hatch; the breaking metamodel behavior is intentional and well-justified.

Testing

Installed TS deps (bun install), Python dev extras (uv sync), and built the C#/Java ports; ran the cross-port conformance oracles which are the test of record for the #185 fixtures, plus the touched TS unit tests (fr015, origin-completeness, registry). All green across TS, C#, Java (Kotlin shares its loader), and Python. To produce reviewer-visible end-to-end evidence, I drove the real TS loader directly against the three #185 conformance fixture inputs (same provider composition the conformance runner uses) and captured the actual user-facing behavior: a field.uuid source surfaced as field.string on a projection view fires ERR_PASSTHROUGH_TYPE_MISMATCH with the message telling the author to either declare field.uuid or set @convert:true; a string[]→string array-ness divergence fires the same code; and @convert:true loads clean with zero errors. The transient evidence script was removed from the working tree after capturing the transcript; the tree is clean and evidence lives only in the dedicated evidence dir. No setup issues, flakiness, or failures encountered.

Evidence: TS loader transcript — #185 type-preservation end-to-end (ERR_PASSTHROUGH_TYPE_MISMATCH on subType + array-ness divergence; @convert loads clean; full human-facing error message)

=== error-origin-passthrough-type-mismatch === errorCodes: ["ERR_PASSTHROUGH_TYPE_MISMATCH"] - code=ERR_PASSTHROUGH_TYPE_MISMATCH referrer=CharacterView::currentLocationId target=demo::GameCharacter.currentLocationId === error-origin-passthrough-array-mismatch === errorCodes: ["ERR_PASSTHROUGH_TYPE_MISMATCH"] - code=ERR_PASSTHROUGH_TYPE_MISMATCH referrer=ProductView::tags target=demo::Product.tags === origin-passthrough-convert-optout === errorCodes: [] (loaded clean) === Human-facing loader message (subType mismatch) === origin.passthrough on CharacterView.currentLocationId: field is field.string but its @from source "demo::GameCharacter.currentLocationId" is field.uuid — a passthrough forwards the value unchanged, so the types must match. Declare field.uuid, or set @convert: true to acknowledge a deliberate type change.


=== error-origin-passthrough-type-mismatch ===
providers: ["metaobjects-core-types","metaobjects-db"]
errorCodes: ["ERR_PASSTHROUGH_TYPE_MISMATCH"]
errors:
  - code=ERR_PASSTHROUGH_TYPE_MISMATCH referrer=CharacterView::currentLocationId target=demo::GameCharacter.currentLocationId
warnings: 0

=== error-origin-passthrough-array-mismatch ===
providers: ["metaobjects-core-types","metaobjects-db"]
errorCodes: ["ERR_PASSTHROUGH_TYPE_MISMATCH"]
errors:
  - code=ERR_PASSTHROUGH_TYPE_MISMATCH referrer=ProductView::tags target=demo::Product.tags
warnings: 0

=== origin-passthrough-convert-optout ===
providers: ["metaobjects-core-types","metaobjects-db"]
errorCodes: []
errors:
warnings: 0

=== SUMMARY (#185 type-preservation) ===
subType mismatch  -> ERR_PASSTHROUGH_TYPE_MISMATCH  [expect ERR_PASSTHROUGH_TYPE_MISMATCH]
array-ness mismatch-> ERR_PASSTHROUGH_TYPE_MISMATCH  [expect ERR_PASSTHROUGH_TYPE_MISMATCH]
@convert opt-out   -> (loaded clean)  [expect loaded clean]

All #185 assertions passed.

=== Human-facing loader messages (subType mismatch) ===
  origin.passthrough on CharacterView.currentLocationId: field is field.string but its @from source "demo::GameCharacter.currentLocationId" is field.uuid — a passthrough forwards the value unchanged, so the types must match. Declare field.uuid, or set @convert: true to acknowledge a deliberate type change.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

✅ **Test** - passed

✅ No issues found.

  • bun test packages/metadata (TS metadata package — 2119 pass, includes origin/validation-passes)
  • bun test packages/metadata/test/conformance.test.ts -t passthrough (16 passthrough conformance scenarios — the 3 new #185 fixtures + repointed parameter-ref fixture + existing passthrough fixtures)
  • bun test packages/metadata/test/fr015-source-parameter-ref.test.ts packages/metadata/test/origin-definition-completeness.test.ts packages/metadata/test/registry-conformance.test.ts (31 pass — touched by the diff)
  • dotnet test MetaObjects.Conformance.Tests (C# — 718 pass, includes new passthrough fixtures)
  • mvn -o test -Dtest=ConformanceTest (Java metadata — 428 pass) + -Dtest=RegistryManifestConformanceTest (3 pass — @convert registration)
  • uv run pytest tests/conformance/test_conformance.py tests/conformance/test_registry_conformance.py (Python — 221 pass)
  • bun run transient evidence script driving the real TS loader against the 3 #185 fixtures — captured ERR_PASSTHROUGH_TYPE_MISMATCH firing on subType + array-ness mismatch and clean load with @convert, plus the full human-facing ParseError message
⚠️ **Document** - 1 info
  • ℹ️ docs/superpowers/specs/2026-05-28-fr-015-source-parameter-ref-design.md:106 - The historical FR-015 design spec (and two sibling plan/spec docs) still name the retired ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH error code and describe the pre-generalization parameter-ref-only type-match check. The living reference docs (ERROR-CODES.json, metamodel-docs, migration guide, cross-language porting guide) now all reflect the generalized ERR_PASSTHROUGH_TYPE_MISMATCH, but these design journals were left as-is.
⚠️ **Lint** - 1 info
  • ℹ️ server/python/src/metaobjects/loader/validation_passes.py:1522 - New _check_passthrough_type calls resolved_source(origin_source, ...) where origin_source: object but resolved_source expects ErrorSource — mypy strict reports Argument 1 ... incompatible type &#39;object&#39;; expected &#39;ErrorSource&#39;. This is one new mypy error vs. the 53-error baseline; it copies the identical pre-existing pattern already present 5× in this file (e.g. line 1475 in _check_extends_origin_agreement), originating from MetaOrigin.source being typed object. mypy is configured (strict) but NOT a CI gate (Python CI runs pytest only), so the codebase tolerates these. Fixing properly requires a cross-cutting MetaOrigin.source typing change, out of scope for this change.
✅ **Push** - passed

✅ No issues found.

dmealing and others added 3 commits July 8, 2026 20:37
#185)

A field carrying `origin.passthrough @from: "Entity.field"` forwards the source
value unchanged, so its declared `field.<subType>` and array-ness must be
identical to the resolved source field. A divergence now fails load with
`ERR_PASSTHROUGH_TYPE_MISMATCH` — catching the mismodeling where a `field.uuid`
source is surfaced by a projection/view as `field.string` (which silently leaves
the field String-typed end-to-end and forces hand-written String<->UUID
bridging, defeating a UUID migration). `verify --db` can't see this — the column
IS uuid; only the metadata model knows both ends of the passthrough.

The check compares subType + array-ness only — nullability is deliberately NOT
judged (a view over an outer join legitimately widens NOT NULL -> nullable, so a
nullability check would false-positive on valid projections).

Escape hatch: a new optional boolean attr `@convert: true` on `origin.passthrough`
acknowledges a deliberate type change and suppresses the error. It is an
acknowledgement ONLY — it does not generate a cast; the consumer owns any
coercion. Real type-converting projections remain origin.expression's job (#159),
so @convert is NOT blocked on that unbuilt feature.

This GENERALIZES and RETIRES the narrow, stored-proc-parameter-ref-only
`ERR_PARAMETER_REF_PASSTHROUGH_TYPE_MISMATCH` (FR-015) into one host-agnostic
invariant covering projections, entities, values, and parameter refs alike,
enforced once in the origin-paths validation pass.

Cross-port: implemented in all five ports (TS reference + Python / C# / Java;
Kotlin inherits Java's loader), each registering @convert on origin.passthrough
and byte-matching the shared registry-conformance manifest. Gated by three new
conformance fixtures (type-mismatch, array-mismatch, @convert opt-out) plus the
repointed parameter-ref fixture. BREAKING: adopter metadata with a divergent
passthrough now fails load (the point — it names both types + the fix).

Also fixes a pre-existing generator gap: the `index` metamodel concept was
missing from generate-embedded-metamodel.ts's CONCEPT_DIRS.

Co-Authored-By: Claude <noreply@anthropic.com>
@dmealing
dmealing merged commit 644d27f into main Jul 9, 2026
1 check passed
@dmealing
dmealing deleted the passthrough-type-preservation branch July 9, 2026 04:20
dmealing added a commit that referenced this pull request Jul 9, 2026
….8 (Maven)

Coordinated release across all four registries. Three merged efforts:
- #185/#186 — BREAKING: origin.passthrough is type-preserving (+@convert)
- #187 — typed value-object jsonb columns all ports (Kotlin→Jackson; C#/Java/Python array-of-VO)
- #188/#189 — load-order-independent super-resolution (Node-vs-Bun build portability)

npm: 14-package lockstep 0.15.17. PyPI metaobjects 0.15.10. NuGet MetaObjects* 0.15.8.
Maven com.metaobjects:* 7.7.8. See CHANGELOG.md [0.15.17].

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant