Skip to content

Commit 9841af6

Browse files
dmealingclaude
andauthored
feat(validation): metadata reference enforcement + validation on the type registry + collect-all (#52)
* fix(metadata): config-driven default name for singleton identities (FR-024 friction) The no-name identity.primary form shown across the docs, llms-full, and the website example FAILED to load (ERR_IDENTITY_NAME_REQUIRED) — a copy-paste trap for users and agents. Fix it the config-driven way, gated by cardinality. - New type-def config: `maxOccurs` (declared + loader-enforced) and `defaultName`. `defaultName` is honored ONLY when `maxOccurs === 1` — a static default name is collision-free by construction only for a singleton. - spec/metamodel/identity.json: identity.primary -> { maxOccurs: 1, defaultName: "primary" }. A nameless primary now loads, named "primary" (stable + referenceable as Entity.primary). Multi-cardinality children (secondary/reference, validators, views) still require explicit names — no unpredictable counter auto-naming. - TS reference: parser applies the default (singleton-gated); new validate-max-occurs pass enforces the singleton (catches two primaries); ERR_TOO_MANY_OCCURRENCES added. 4 new tests; metadata suite green; the no-name CLI repro now generates. - Cross-port: shared spec/metamodel + ERROR-CODES.json carry the change; Java (spec auto-copied) / Python / C# get the new error code + embedded spec. The DEFAULT-NAME behavior is TS-only for now, matching the existing FR-024 posture (Java/Python/C# don't enforce identity-name-required yet — so they never had the bug). All four ports verified green: TS 2035, Java 1046, C# conformance 657, Python conformance 301. Design: docs/superpowers/specs/2026-06-18-identity-default-name-design.md * fix(metadata): config-driven singleton default-name + maxOccurs across Java/Python/C# Completes the cross-port behavioral implementation begun in TS (b43f5e7) for the name-less `identity.primary` friction (FR-024): a name-less primary failed to load, a copy-paste trap present in docs, llms-full.txt, and the .dev homepage. The fix is a generic, config-driven registry rule — NOT a per-type loader special-case: - A type may declare `maxOccurs` (singleton == 1) + `defaultName` on its registration. identity.primary declares `maxOccurs: 1, defaultName: "primary"` (mirrors the shared spec/metamodel/identity.json SSOT). - The parser names a name-less node from `defaultName` only when `maxOccurs == 1` (collision-free by construction) and serializes it with that name. - A new singleton-cardinality validation pass emits ERR_TOO_MANY_OCCURRENCES against the offending child when a parent exceeds a registered maxOccurs (e.g. two identity.primary on one object). Per-port: - Java: maxOccurs/defaultName on TypeDefinition(+builder, preserved across the three doc-slot/strict-scoping rebuilds and TypeDefinitionBuilder.from); PrimaryIdentity registration; CanonicalJsonParser default-name; ValidationPhase validateMaxOccurs; ERR_TOO_MANY_OCCURRENCES message constant. - C#: MaxOccurs/DefaultName on TypeDefinition (preserved across the three Registry rebuilds); Def() helper + identity registration; Parser default-name; ValidationPasses.ValidateMaxOccurs wired into the loader. - Python: max_occurs/default_name on TypeDefinition; core_types registration; parser default-name; validation_passes _validate_max_occurs. Gated by two new cross-port conformance fixtures (identity-primary-default-name, error-too-many-primary) that every port now passes — previously Java/Python/C# were vocabulary-only here and would have silently diverged from the TS oracle. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: document identity.primary singleton default-naming (FR-024 friction) The loader now names a name-less identity.primary "primary" (config-driven singleton default), so the minimal `{ "identity.primary": { "@fields": "id" } }` form loads — no more copy-paste trap. Document this in entities.md (the singleton rule + the ERR_TOO_MANY_OCCURRENCES guard) and the metamodel spec (the generic maxOccurs/defaultName mechanism). Existing no-name examples throughout the docs are now correct as-is. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(loader): enforce relationship @objectref + identity.reference @references resolution (all 5 ports) A dangling relationship @objectref or identity.reference @references loaded silently — drift between two pieces of metadata that should fail loudly (like extends -> ERR_UNRESOLVED_SUPER, @payloadRef -> ERR_INVALID_TEMPLATE, origin paths -> ERR_INVALID_ORIGIN already do). Now every relationship's @objectref and every identity.reference's @references must resolve to a real object in the loaded tree, or the model fails to load. - relationship @objectref unresolved -> ERR_INVALID_RELATIONSHIP (folded into the existing code) - identity.reference @references unresolved -> new ERR_INVALID_REFERENCE Implemented as a registered validation pass in every port (TS validateRelationships + validateIdentityReferences; Java ValidationPhase; Python validation_passes; C# ValidationPasses), resolving via each port's shared object index (refMatchesObject / FindObject / _build_object_index). The target entity is the segment before the first dotted field path (packages use "::", never "."). Gated cross-port by two new conformance fixtures (error-relationship-unresolved-objectref, error-identity-reference-unresolved) — all five ports green. ERR_INVALID_REFERENCE added to every port's error enum + the shared ERROR-CODES.json. Fixed two incomplete unit-test scaffolds that built relationships without their target entity (Java RelationshipReferentialActionsTest stubs; the Kotlin RelationshipsTest "unresolved objectRef" case repurposed to assert the new rejection). Also includes a design doc surveying validation-architecture best practice (TS binder/checker, GraphQL, Smithy, SHACL, XSD keyref) and recommending a phased move to a formalized semantic phase (symbol table + rule registry + declarative reference descriptors). This commit is Phase 1; the doc scopes Phases 2-3 as a follow-up FR. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto: normalized validator-registry vertical slice (TS + Java) — Phase 2 de-risk Exploratory branch (NOT for PR #44). Proves the converged validation architecture end to end in both an OO port (Java) and a data-oriented port (TS): * SymbolTable — object index built once per load (the binder analogue) * ReferenceDescriptor — an attr declares what it points at (data-driven cross-refs) * NodeValidator — imperative rule, registered per (type, subType) by the provider * ValidationRegistry — reference descriptors + validators, keyed by type.subType * runRegisteredValidation / RegisteredValidation.run — one recursive root.validate(ctx) walk: apply declared references → invoke registered validators → recurse The built-in @objectref / @references checks are MIGRATED onto declarative reference descriptors (no bespoke pass) — conformance stays green (TS 2045, Java metadata 1056 + conformance 388 + ktx 33), behavior-preserving (Java still eager-throws the first error). The load-bearing proof (R2 — downstream extensibility): * TS: a fake external provider registers a brand-new type `widget.gauge` + its reference descriptor + its imperative validator (with its OWN error codes); a model with a bad feed ref AND an inverted range produces BOTH errors — no core edits. * Java: a downstream ValidationRegistry registers a method-reference validator + a custom-code reference descriptor (tighter target-kind); both fire via the same walk on a loaded tree — no core edits. (New-type registration uses existing provider machinery, proven by the TS slice.) Same registry, same recursive walk, same declarative descriptors; validator bodies idiomatic per port (TS closures, Java method references). Design doc rewritten around this model (symbol table + rule registry + declarative references + provider-extensibility as the driver). Phase 3 = migrate the remaining resolvers (extends/payloadRef/origins), wire through the provider SPI, normalize the error model (Java → collect), port to C#/Python. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto(ts): validation rides in on type registration — derived from the registry Deepens the slice in TS so validation is part of a type's registration, not a side-channel: * TypeDefinition gains `references: ReferenceDescriptor[]` + `validate: NodeValidator` (contract in validation-types.ts to avoid an import cycle). * The loader DERIVES validation from its TypeRegistry — runRegisteredValidation reads each node's TypeDefinition.references + .validate. No separate ValidationRegistry, no validationRegistry LoadOption. * Core declares its built-in cross-references ON their TypeDefinitions (relationship @objectref, identity.reference @references). Conformance stays green (388) — behavior preserved. * ParseError.code widened to `ErrorCode | (string & {})` so a downstream provider emits its OWN error codes (the closed-union seam, now closed). Proof rewritten: a fake provider registers a brand-new type `widget.gauge` WITH its references + validator on the same TypeDefinition; composing it into the registry is the ONLY wiring; the custom type validates itself (dangling @feeds + inverted range, both with the provider's own codes) — no separate registry, no core edits. Full TS suite 2045 green. This is the user's original vision realized: configurable validation is part of the type's registration; a new type's validation rides in automatically. Java derivation next. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto(java): validation derived from the type registry — config lives on the type Brings Java to the same realization as the TS slice: validation is part of a type's registration, derived by the loader — not a side-registry. * TypeDefinition gains `references: List<ReferenceDescriptor>` + `validator: NodeValidator` (additive, like maxOccurs/defaultName; preserved across TypeDefinitionBuilder.from and the three registry rebuild sites). * TypeDefinitionBuilder gains .reference(...) / .validator(...). * RegisteredValidation.run(root, MetaDataRegistry) DERIVES validation: per node it reads its TypeDefinition's references + validator. The old separate ValidationRegistry is deleted. * Core declares its cross-references ON their types — ReferenceIdentity (@references) and the three relationship subtypes (@objectref) via the registration builder. * ValidationPhase runs the derived walk over loader.getTypeRegistry() (collected, first error eager-thrown to preserve cross-port behavior). Green: metadata 1057 + conformance 388 (behavior-preserving — core references now resolve via TypeDefinition.references), and codegen-spring/codegen-kotlin/metadata-ktx/om all pass. DownstreamValidationTest proves the descriptors live on the TypeDefinitions and the derived walk enforces them. A downstream NEW type's validation rides in via setTypeRegistry on the same mechanism (proven end-to-end by the TS slice). Known prototype seam: registry <-> loader.validation form an intra-module package cycle (compiles fine under Java's single-module javac); production moves the contract types to a base package. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto(csharp,python): validation derived from the type registry — all 5 ports aligned Brings C# and Python to the same realization as TS/Java: validation is part of a type's registration, derived by the loader. * TypeDefinition gains References (List<ReferenceDescriptor>) + Validate (NodeValidator) — C# internal-set props copied across the three registry rebuild sites; Python dataclass fields copied in register(). * A registry-derived recursive walk (C# RegisteredValidation.Run / Python registered_validation.run) reads each node's TypeDefinition references + validator. * Core declares its cross-references ON their types — identity.reference (@references) and the relationship subtypes (@objectref) — in CoreTypes.cs / core_types.py. * The loader runs the derived walk; the old procedural @objectref / @references passes are removed. Behavior-preserving: C# conformance 665, Python conformance 391 + full suite 1206 — the built-in reference checks now resolve via TypeDefinition.References, same fixtures. The downstream-custom-code seam (C# MetaError.Code / Python ErrorCode are closed enums) maps unknown codes to ERR_UNKNOWN for now (TS widened ParseError.code; the OO ports follow in the error-model normalization step). All five ports (TS / Java / C# / Python, + Kotlin via shared JVM) now carry validation on the type and derive it from the registry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto(java): break the registry<->validation package cycle Move the validation CONTRACT (ReferenceDescriptor, NodeValidator, ValidationContext, SymbolTable, ValidationError) from com.metaobjects.loader.validation to a base package com.metaobjects.validation, keeping only the registry-dependent runner (RegisteredValidation) in loader.validation. Dependency now flows one way: registry -> com.metaobjects.validation (TypeDefinition.references/validator) loader.validation -> registry + com.metaobjects.validation (the runner) No more intra-module cycle (the one prototype seam called out earlier). Behavior unchanged: metadata 1057 + conformance 388 + downstream 3, codegen-spring/metadata-ktx green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: validation architecture — Phase 2 realized in all 5 ports; Phase 3 scoped Update the design doc to the realized state: validation-on-the-TypeDefinition, derived by the loader, in TS/Java/C#/Python (+Kotlin via shared JVM), with the downstream new-type proof and the package-cycle fix. Scope the remaining Phase 3 precisely (migrate payloadRef/extends/origins onto descriptors; carry the reference field in spec JSON; normalize the error model to collect-all + downstream codes; provider-SPI registration + a downstream-extension conformance fixture). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * proto(ts): migrate @payloadRef/@responseRef onto descriptors — cross-refs unified Completes the object-cross-reference unification in the reference port: @payloadRef and @responseRef now resolve via reference descriptors on the template TypeDefinitions (targetSubType: object.value), alongside @objectref / @references. The bespoke existence + kind checks are removed from validateTemplatePayloadRefs (the @kind/@textRef + @requiredSlots rules stay — slots still needs the resolved payload). Per-reference source convention preserved via a `resolvedSource` descriptor flag: payloadRef emits the FR5d resolved-source envelope (referrer + target); objectRef/references stay plain. This keeps each kind's existing envelope AND means the other four ports need NO change — the shared fixtures are untouched (objectRef/references plain, payloadRef resolved via TS's descriptor or the other ports' existing pass). TS 2045 green; Python conformance 391 green (unaffected). Other ports keep @payloadRef as their existing pass (identical behavior); migrating it onto the descriptor there is a behavior-neutral internal follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(validation): Phase 3 implementation plan (payloadRef propagation, spec-JSON declarative, Java collect-all, downstream codes) Captures the deliberate, conformance-gated refinements left after the validation-on-the-TypeDefinition architecture was realized in all five ports. Each task is independent and called out with its files, blast radius, and TDD steps — none is required for the architecture, which is complete. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(java-validation): report ALL dangling references on load, not just the first (Phase 3, Task 3) The registry-derived reference pass already collects every finding; ValidationPhase was throwing only get(0). Now a load with multiple broken object references surfaces them all via MetaDataValidationException (IS-A MetaDataException carrying the first error's code/envelope for back-compat + getValidationErrors() for the full set). A single-error load still throws a plain MetaDataException, byte-identical to before — so every single-finding conformance fixture is unchanged. Contained to the reference pass; the structural eager-throw passes (which guard a malformed tree) are untouched. TDD: new DownstreamValidationTest.reportsEveryDanglingReferenceNotJustTheFirst. Green: metadata 1058, conformance 417 (incl. 388), metadata-ktx 33. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(validation): mark Phase 3 Task 3 core shipped (reference-pass collect; cross-pass deferred) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(java-validation): full cross-pass collect-all — a load reports every error, not the first (Phase 3, Task 3) Supersedes the narrower reference-only collect (1e512ea). ValidationPhase now runs EVERY pass and collects each finding instead of aborting on the first: a model with defects across multiple passes surfaces them all. Mechanism: - pass(...) wraps each validation pass, catching its MetaDataException into a list and letting the next pass run; a non-MetaDataException (a real bug) still propagates. - Findings are deduped on (code + source envelope) — the same defect flagged by two passes (e.g. a missing required attr caught by both the generic and a subtype pass) is one finding, while genuinely-distinct errors stay separate. - All but the last finding are recorded via loader.addError (source order); the last is thrown. This matches the conformance harness's "drain getErrors() then the thrown error" merge and keeps single-error loads byte-identical to the historical eager-throw. Dropped the aggregate MetaDataValidationException — loader.getErrors() is the channel. Fixtures stayed green via dedupe; updated one unit test (bad @ROLE now legitimately reports both ERR_BAD_ATTR_VALUE and ERR_SOURCE_NO_PRIMARY) to assert the expected code is among all reported errors. Green: metadata 1058 + conformance 417, metadata-ktx/codegen-base/codegen-spring/ codegen-kotlin/render/om/core-spring 234+, codegen-mustache/plantuml/maven-plugin. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(validation): Phase 3 Task 3 shipped as full cross-pass collect-all (dedupe + throw-last) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(ts-validation): revert payloadRef/responseRef descriptor split — align TS with the other ports (Option 1) Reverts 0ea44c7. Migrating @payloadRef/@responseRef onto reference descriptors in TS left their dependent rule (@requiredSlots, which needs the RESOLVED payload) behind in the template pass — so TS resolved the payload twice and validated payloadRef in two places, while Java/C#/Python kept it unified in one pass (single resolution). TS was the outlier. This restores the unified template pass in TS: @payloadRef/@responseRef existence + object.value kind + @requiredSlots are validated together, one resolution, matching the other three ports exactly. objectRef/references stay on descriptors (pure references, no entanglement) — those remain consistent across all ports. The descriptor-only resolvedSource flag + errorResolved helper are removed (unused after the revert). Each reference KIND is now handled identically in every port. Zero behavior change — the payloadRef conformance fixtures stay green via the restored pass. Also fixed a pre-existing noUncheckedIndexedAccess typecheck error in default-name-singleton.test.ts. Green: TS metadata 2045, typecheck clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(validation): config-driven validation design (Option 4 gold standard) + Phase 3 Task 1 resolution New spec docs/superpowers/specs/2026-06-21-config-driven-validation-design.md: the write-once standard — checks as DATA in shared JSON config, one generic interpreter per rule-shape per port, so a rule is authored once (not coded in 5 languages) and a downstream provider's type self-validates with zero new code. Four closed rule-shapes (reference / allowedValues / requires / fieldPathRef) cover the whole current corpus; the validate hook is the rare escape hatch. Phased (proof slice first). Sibling to FR-033 (#23). Updated the Phase 3 plan to record Task 1's actual resolution (Option 1 revert, not descriptor propagation). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(validation): staff-review should-fixes — downstream-code guard, error locatability, doc accuracy (5 ports) From the pre-PR staff review: - Downstream-code crash guard (Java): the registry-reference loop's ErrorCode.valueOf(e.code()) would throw IllegalArgumentException (not a MetaDataException) on a downstream provider's non-enum code, crashing the load and contradicting the advertised extensibility. Now mapped via toErrorCode() → ERR_UNKNOWN fallback (message keeps the raw code). C# tightened with Enum.IsDefined so a numeric-string code maps to ERR_UNKNOWN like Python's value-lookup; Python already correct. - Error locatability (all 4 data ports): the registry-derived reference error dropped the owning-entity qualifier (e.g. "items" instead of "Order.items"). Restored by qualifying the node name with its parent — message-only, conformance (code+source) unchanged. - Doc accuracy (Java): ValidationPhase javadoc still claimed eager-throw-on-first-error; updated to the collect-all / throw-last contract (class header + legacy run() entry point). - Hardening (Java): dedupe() no longer collapses two findings that share neither code nor envelope (index-tagged key). - Clarity (C#/Python): commented that maxOccurs/defaultName are intentionally hardcoded to match spec_metamodel/identity.json (true SSOT wiring tracked in #51). Green: TS metadata 2045 + typecheck, Java 1058 + conformance 417, C# conformance 665, Python 1206 (integration pg8000 collection errors are pre-existing/env-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(changelog): metadata reference enforcement + registry-derived validation + collect-all (Unreleased) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 1a025c0 commit 9841af6

53 files changed

Lines changed: 1867 additions & 60 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
77

88
## [Unreleased]
99

10+
### Added
11+
- **Metadata reference enforcement** — a dangling cross-reference now fails the load instead of being silently ignored: an unresolved `relationship.@objectRef` raises `ERR_INVALID_RELATIONSHIP` and an unresolved `identity.reference.@references` raises `ERR_INVALID_REFERENCE`, with a source envelope pinpointing the node. Helps catch metadata drift (a renamed/removed entity surfaces immediately).
12+
- **Validation derived from the type registry** — each type's registration carries its cross-reference descriptors (and an optional validator), enforced by one registry-driven walk, so a downstream provider's new type validates itself with no core changes. (The config-driven, write-once-across-ports evolution of this is tracked in #51.)
13+
- **A load reports every validation error, not just the first** — validation passes now collect their findings (deduped by code + source) and surface them together rather than aborting on the first, so a model with multiple defects shows them all in one run.
14+
15+
### Changed
16+
- **BREAKING — dangling metadata references now fail to load.** Models that referenced a non-existent entity via `@objectRef` / `@references` previously loaded silently; they now error (`ERR_INVALID_RELATIONSHIP` / `ERR_INVALID_REFERENCE`). Fix the reference or remove the relationship/identity.
17+
18+
### Cross-port
19+
- Reference enforcement, the registry-derived validation walk, and the downstream-code-tolerant error mapping ship in all data ports (TS / Java / C# / Python; Kotlin via the shared JVM loader), gated by new shared `fixtures/conformance/` error fixtures (`error-relationship-unresolved-objectref`, `error-identity-reference-unresolved`). Cross-pass collect-all is currently Java-side (the other ports already collected).
20+
1021
## [0.10.0] — 2026-06-14
1122

1223
### Added
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
# Validation Phase 3 — implementation plan
2+
3+
_2026-06-20. Follows the realized Phase 2 (validation-on-the-`TypeDefinition`, derived by the
4+
loader, in all five ports — see
5+
`docs/superpowers/specs/2026-06-19-metadata-validation-architecture-design.md` and the
6+
`proto/validation-registry` branch). Phase 3 is the set of deliberate, conformance-gated
7+
refinements left after the architecture was de-risked. Each task is independent; do them in
8+
the order below or cherry-pick. TDD throughout._
9+
10+
## Status going in
11+
12+
- All five ports (TS / Java / C# / Python, + Kotlin via shared JVM) carry validation on the
13+
`TypeDefinition` (`references` + `validate`) and the loader derives it from the registry.
14+
- `@objectRef` / `@references` are descriptor-driven everywhere (plain-source envelope).
15+
- TS also has `@payloadRef` / `@responseRef` as descriptors (resolved-source via the
16+
per-descriptor `resolvedSource` flag); the other ports keep them as their existing pass.
17+
- Green: TS 2045, Java metadata 1057 + conf 388 + ktx 33, C# conf 665, Python 1206 + conf 391.
18+
19+
---
20+
21+
## Task 1 — Reference-validation consistency
22+
23+
> **Status: RESOLVED differently than originally framed** (commit `599a4aed`). Investigation
24+
> found TS was the *outlier*, not the other ports: migrating `@payloadRef`/`@responseRef` onto
25+
> descriptors in TS left their dependent `@requiredSlots` check in the template pass, forcing a
26+
> double resolution — while Java/C#/Python kept it unified (single resolution). So rather than
27+
> *propagate* the split to three more ports (the original Task 1, which would standardize the
28+
> messier design), we **reverted the TS split** ("Option 1"): every reference *kind* is now
29+
> handled identically in every port — `objectRef`/`references` on descriptors, template
30+
> references unified in the template pass. The right *long-term* answer (make the checks data,
31+
> not per-port code) is captured separately as the gold-standard direction:
32+
> **`docs/superpowers/specs/2026-06-21-config-driven-validation-design.md`** (config-driven
33+
> validation; four closed rule-shapes; sibling to FR-033 / #23). The original propagate-the-
34+
> descriptor framing below is retained for context but is **not** the path taken.
35+
36+
### (original framing — superseded) Propagate `@payloadRef`/`@responseRef` descriptors to Java/C#/Python
37+
38+
**Goal:** parity with TS — `payloadRef`/`responseRef` resolve via reference descriptors on
39+
the template `TypeDefinition`s in every port, removing the bespoke existence checks.
40+
41+
**Value:** architectural consistency only — **behavior-neutral** (the existing passes already
42+
produce the exact resolved-source envelope the fixtures pin). Low priority; do it for
43+
uniformity, not for behavior.
44+
45+
**Risk (real):** in Java/C#/Python, `validateTemplates`/`ValidateTemplatePayloadRefs` is
46+
**entangled** with template-only rules (`@format`/`@promptStyle`/`@kind`/`@textRef`/
47+
`@requiredSlots`). `payloadRef` is not a clean standalone reference there. The pass must keep
48+
R3 (`@requiredSlots` needs the resolved payload) + R4/R5/kind, and the migrated descriptor
49+
must reproduce the **pinned `resolved`-source envelope** (`referrer` + `target`) exactly or
50+
`error-template-payload-ref-unresolved` / `-not-value` break in three ports.
51+
52+
**Steps (per port — Java / C# / Python):**
53+
1. Add a `resolvedSource` boolean to `ReferenceDescriptor` (Java record: add the field + a
54+
5-arg back-compat constructor delegating `resolvedSource=false`; C# record: optional
55+
param; Python dataclass: field default `False`).
56+
2. In the registry runner, branch on `desc.resolvedSource`: emit the resolved-source
57+
envelope (`ResolvedSource.from(node.source, referrer, raw)` — Java; the C#/Python
58+
equivalents) instead of plain `node.source`. **Match the referrer/target the existing
59+
template pass uses** (capture from the pass before deleting it).
60+
3. Declare `payloadRef` + `responseRef` descriptors (`targetSubType: value`,
61+
`resolvedSource: true`, `ERR_INVALID_TEMPLATE`) on the template subtypes
62+
(`template.prompt` / `template.output` / `template.toolcall`).
63+
4. Remove **only** the payloadRef/responseRef existence+kind emission from the template pass;
64+
keep the payload *resolution* (R3 needs it) + R4/R5/kind/slots.
65+
66+
**Test:** the existing `error-template-payload-ref-unresolved` / `-not-value` /
67+
`-required-slot-missing` conformance fixtures must stay byte-green in every port. Capture the
68+
pre-migration envelope first (load each fixture, record `source`), then assert no diff.
69+
70+
**Recommendation:** optional. The architecture is already consistent at the load-bearing
71+
level; weigh the uniformity against the entangled-pass risk.
72+
73+
---
74+
75+
## Task 2 — Declarative `reference` in the embedded spec JSON
76+
77+
**Goal:** the user's original vision — cross-references declared as **config**, not code.
78+
Carry a `reference` object on the attr in `spec/metamodel/relationship.json` /
79+
`identity.json` / template specs, and have each port's spec reader lower it onto the
80+
`TypeDefinition`'s `references` (replacing the programmatic declaration in
81+
`core-types.ts` / `CoreTypes.cs` / `core_types.py` / the Java registration builders).
82+
83+
**Shape (in the spec JSON, on the ref-bearing attr):**
84+
```jsonc
85+
{ "name": "objectRef", "valueType": "string",
86+
"reference": { "target": "object", "dottedFieldPath": false, "errorCode": "ERR_INVALID_RELATIONSHIP" } }
87+
```
88+
89+
**Risk:** touches **four** spec readers (TS `defineProviderFromData`/`provider-data.ts`; Java
90+
`SpecMetamodelReader`; C# `SpecMetamodelReader`; Python spec reader). **Guard each reader to
91+
tolerate an unknown `reference` field first** (a strict reader may reject it). The embedded
92+
JSON is also byte-identity-gated in some ports (Java auto-copies; C#/Python have committed
93+
copies) — update all copies together.
94+
95+
**Steps:**
96+
1. Add the `reference` field to the shared spec JSON (one attr at a time).
97+
2. Each spec reader: parse `reference` → build a `ReferenceDescriptor` → attach to the
98+
lowered `TypeDefinition`. Keep the programmatic declaration as a fallback until all
99+
readers carry it, then delete it.
100+
3. Re-run the registry-manifest / spec byte-identity gates per port.
101+
102+
**Test:** conformance unchanged (the descriptors are the same, just sourced from JSON);
103+
registry-manifest gates green; a new unit test that the lowered `TypeDefinition.references`
104+
matches the JSON.
105+
106+
---
107+
108+
## Task 3 — Java collect-all (loader-contract normalization)
109+
110+
> **Status: SHIPPED — full cross-pass collect-all** (commit `1fab2c98`, superseding the
111+
> reference-only slice `1e512ea1`). `ValidationPhase` now runs **every** pass and collects
112+
> each finding instead of aborting on the first, so a model with defects across multiple
113+
> passes reports them all. Key decisions that made it safe:
114+
> - **`pass(...)` wrapper** catches each pass's `MetaDataException` and continues; a
115+
> non-`MetaDataException` (a genuine bug) still propagates — no swallowing. The feared
116+
> cascade (a later pass NPE-ing on a half-broken tree) **did not materialize** across the
117+
> whole corpus; if one ever does, it surfaces loudly rather than hiding errors.
118+
> - **Dedupe on (code + source envelope)** — the same defect flagged by two passes (a missing
119+
> required attr caught by both the generic and a subtype pass) is one finding. This kept the
120+
> single-error conformance fixtures green without edits.
121+
> - **Record all-but-last via `loader.addError`, throw the last** — matches the conformance
122+
> harness's "drain `getErrors()` (source order) then the thrown error" merge, and keeps
123+
> single-error loads byte-identical to the historical eager-throw.
124+
> - The aggregate `MetaDataValidationException` was dropped; `loader.getErrors()` is the
125+
> programmatic channel for the full set.
126+
>
127+
> One unit test updated (a bad `@role` now legitimately reports both `ERR_BAD_ATTR_VALUE` and
128+
> `ERR_SOURCE_NO_PRIMARY`) to assert the expected code is *among* all reported errors. Green:
129+
> metadata 1058 + conformance 417, all dependent JVM modules. **Cross-port note:** TS/C#/Python
130+
> already collect; this brings Java to parity. No shared-corpus expected-errors fixtures needed
131+
> changing (dedupe held them at one finding each).
132+
133+
**Goal:** a load reports **all** errors, not just the first — matching TS/C#/Python (which
134+
already collect). The genuine user-visible UX win.
135+
136+
**Blast radius (the reason this is its own task):** Java's `load()` **eager-throws**
137+
(`MetaDataLoadingException` on the first error), and that contract is depended on by:
138+
- many tests asserting a bad model *throws* (`RelationshipReferentialActionsTest.loadThrough`,
139+
the Kotlin `RelationshipsTest` "unresolved objectRef now fails to load", others),
140+
- the conformance runner (`ConformanceTest`) which **catches** the throw and compares,
141+
- ~20 eager-throw validation passes (each aborts its pass on first error).
142+
143+
**Two implementation options:**
144+
- **(A) Full collect**`load()` returns/exposes a collected error list (new `getErrors()`);
145+
the conformance runner reads it; throw-expecting tests migrate to assert on the collected
146+
list. Cleanest end state, widest migration.
147+
- **(B) Aggregate-and-throw** — keep `load()` throwing, but collect across passes (wrap each
148+
pass call in `ValidationPhase.run` in try/catch, collect, continue) and throw an
149+
**aggregated** exception carrying all errors (first error preserved for back-compat; the
150+
rest accessible via `getAllErrors()`). Smaller migration; the runner reads the first error
151+
unchanged (fixtures are single-error → green). **Recommended first step** — it's
152+
back-compatible and surfaces all errors without breaking the throw contract.
153+
154+
**Note:** option B only collects one-error-per-pass (each pass still aborts on its first).
155+
True within-pass collect needs each pass to stop throwing — defer unless demand.
156+
157+
**TDD steps (option B):**
158+
1. Add a failing test: a model with **two** dangling references reports **two** errors.
159+
2. `ValidationPhase.run`: wrap each pass in try/catch, collect `ValidationError`s, continue;
160+
at the end, if any, throw an aggregated `MetaDataLoadingException` carrying the list
161+
(first error message/code preserved).
162+
3. Confirm all throw-expecting tests + conformance (388) still green (single-error fixtures
163+
unchanged; the first error is preserved).
164+
4. Expose `getAllErrors()` for callers wanting the full set.
165+
166+
**Test:** the new two-error test passes; metadata 1057 + conformance 388 + ktx 33 stay green.
167+
168+
---
169+
170+
## Task 4 — Downstream-code fidelity (OO/Python ports)
171+
172+
**Goal:** parity with TS's widened `ParseError.code` — a downstream provider's validator can
173+
emit its **own** error code through Java/C#/Python (today they collapse an unknown code to
174+
`ERR_UNKNOWN`).
175+
176+
**Steps:** widen the code carrier — C# `MetaError` gains a `RawCode` string (or `Code`
177+
becomes string-backed); Python `MetaError` carries the raw string; Java's
178+
`ValidationError.code` is already a String, so `ValidationPhase` must stop calling
179+
`ErrorCode.valueOf(...)` on it (carry the raw code on the exception).
180+
181+
**Risk:** low, but **no current test exercises it** (the core path only emits built-in codes).
182+
Add a downstream-extension unit test per port (mirror the TS `validation-registry.test.ts`
183+
custom `widget.gauge` with its own code) so the fidelity is verified, then it has value.
184+
185+
**Recommendation:** do this *with* the per-port downstream-extension test (Task 4 + its test
186+
together), or skip until a real adopter needs custom codes.
187+
188+
---
189+
190+
## Sequencing
191+
192+
1. **Task 3 (collect-all, option B)** — the only user-visible UX win; do it first, test-first.
193+
2. **Task 2 (spec-JSON declarative)** — the "config not code" vision; medium, guard the readers.
194+
3. **Task 1 (payloadRef propagation)** — optional uniformity; only if the entangled-pass risk
195+
is acceptable.
196+
4. **Task 4 (downstream codes)** — only with its verifying test, or defer to adopter demand.
197+
198+
Each task is independent and conformance-gated. None is required for the architecture, which
199+
is complete; these are refinements.

0 commit comments

Comments
 (0)