feat(metamodel)!: ADR-0052 — a template subtype's axis is DIRECTION (all 5 ports) - #318
Conversation
…te.prompt Closes the open question ADR-0052 deferred. The deferral assumed the question was separable; it is not. Every inbound emitter in all five ports dispatches on one bit (is the reply JSON or XML) and reads it from `template.output @format`. Move the inbound tier and that bit has no source. `template.prompt @format` cannot supply it — it is already the *prompt body's* syntax, and the two genuinely differ: the docs-site fixture renders a `text` prompt whose reply is XML (`NpcResponse.reason` carries `@xmlText: true`). `trace-helper-file.ts:116-143` already reads one `@format` twice, under a comment naming the collision: "Same @Format attr, two intentionally different shapes." And because `@format` defaults to `text`, gating the inbound tier on it would emit no parser at all, silently — the mirror of the absurd artifact ADR-0052 removes. Decision: `@responseFormat` on `template.prompt` — optional, closed enum `json | xml`, default `json`. The enum is two members because two is what every shipping consumer dispatches on; the other five `@format` members are reserved-not-registered under ADR-0007 Amd 2's re-entry bar. The default reproduces the trace helper's existing fallback exactly, so the change is behaviour-preserving for every JSON responseRef carrier. Also records the implementation plan, and flags that the ADR index stopped being maintained after ADR-0030 — 0031-0051 are on disk but unlisted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…eFormat ADR-0052 makes a template subtype's axis DIRECTION. This is the vocabulary half, landed in all five ports at once because expected-registry.json is byte-matched by every one of them. template.prompt gains @PromptStyle (moved) + @responseFormat (new, ADR-0053) template.output keeps @kind + the email part-refs; OUTBOUND ONLY @PromptStyle governs a fragment that instructs an LLM how to format its reply, so its old home was the subtype whose own registered description reads "every rendered artifact other than an LLM prompt" — a contradiction visible inside a single attribute. @responseFormat is the syntax of the REPLY, distinct from @Format, which is the syntax of the rendered prompt BODY; ADR-0053 records why one attribute cannot serve both directions. Closed enum json|xml, default json. Two members because two is what every shipping consumer dispatches on; the other five @Format members are reserved-not-registered under ADR-0007 Amd 2's re-entry bar. The default reproduces the trace helper's existing fallback exactly, so every JSON @responseRef carrier is unaffected. BREAKING: a @PromptStyle left on a template.output now fails the load. That is deliberate — Python's wrong-subtype pass and every port's strict-mode ERR_UNKNOWN_ATTR both catch it, so the migration surfaces instead of being silently ignored. Under a non-strict load (the downstream open-attr policy) it is dropped, which is the documented behaviour for any unregistered attr. The two inbound conformance fixtures are renamed to stop describing themselves as outputs, and now declare @Format: text with @responseFormat: json/xml — the shape the old single-@Format design could not express, so the corpus itself discriminates the case. flattened-kitchen-sink gains a response VO for the same reason. Downstream tiers (parser/extractor/fragment codegen, api-docs, verify) still key on template.output and are re-pointed in the commits that follow. Verified: TS metadata 2404 pass · C# 903+354+291+46 pass · Java metadata 1404 pass, BUILD SUCCESS · Python loader+wrong-subtype 23 pass · 10/10 gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
ADR-0052 in the reference port. The parser-on-receipt, the tolerant extract and
the FR-010 response-format fragment now derive from a `template.prompt` that
declares `@responseRef`; `template.output` renders outbound and emits none of
them.
The direction rule lives in ONE new module, templates/find-inbound.ts. Three
call sites each deciding for themselves is how the old tier drifted: the parser
had no `@kind` filter at all, so an email template generated a parser for text
the system had just rendered, while the extractor and the fragment emitter each
applied a different json/xml gate on `@format`.
Three behaviour changes fall out:
- The shape parsed INTO is @responseRef, not @payloadRef. That is the
distinction trace-helper-file.ts has always drawn ("@responseRef types the
result; @payloadRef types the request") and this tier ignored, so a model
using one template for both directions could not tell them apart.
- The reply syntax is @responseFormat (ADR-0053), not @Format. The fragment
emitter was typing "produce your answer like this" off the format of the
QUESTION.
- The tolerant extract is unconditional. The old json/xml gate on @Format is
what made a text-bodied prompt with a JSON reply emit a strict parser and no
extract at all.
Emitted names follow the direction axis: <Prompt>.response.ts (was
<Output>.output.ts) and <Prompt>.responseFormat.ts (was <Output>.prompt.ts).
Leaving a parser named .output.ts generated from a prompt would reproduce the
confusion being removed, and ClassifyPrompt.prompt.ts is worse still.
The generator test file is rewritten as the gate: confirmed RED (6 fail) before
the change and green after, including a prompt with @Format: text and
@responseFormat: xml — the shape a single @Format could not express.
Records one KNOWN GAP found while doing this and deliberately not changed: the
STRICT parser body is Schema.parse(JSON.parse(text)) regardless of syntax, so
parse<Name> cannot work for an XML reply — only the tolerant extract handles
XML. It pre-dates ADR-0052 and changing it would withdraw a public generated
function from existing XML consumers, so it is pinned with a comment naming it
rather than silently blessed.
25 codegen-ts tests still assert the old polarity via template.output fixtures;
they are re-pointed next, together with the goldens.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
… strict The strict Zod tier is JSON-only by construction: its body is `Schema.parse(JSON.parse(text))`, and there is no XML equivalent to generate. The TS runtime ships no XML parser — which is precisely why this reached for JSON.parse in the first place, and it did so for XML templates too, so `parse<Name>` was a generated function that could never work. Supplying a real one would mean taking an XML-parser dependency AND assuming a model emits exactly well-formed XML — the assumption FR-010's tolerant extract exists because you cannot make. So an XML reply now emits the tolerant extract and nothing else; its typed shape is the nullable `<Name>Extracted` mirror, which is the honest type for a best-effort parse of model output. A JSON reply keeps the strict tier, because JSON.parse is a real exact parser and `Schema.parse(JSON.parse(text))` is a correct thing to generate. Nothing is withdrawn from a working consumer: every strict-parse assertion in the suite is on a json template, no test or fixture exercises a strict XML parse, and the extractor tier imports only `extractLenient<Name>WithLoader` plus the mirror types — never `parse`/`safeParse`. The `zod` import is now conditional so an XML file carries no unused import. Also fixes an import path this branch's own rename broke: the emitted extractor still imported from `./<Name>.output.js` after the parser file became `<Name>.response.ts`, so every generated extractor would have failed to resolve. Found by grepping the emitted filename rather than the source one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
The strict Zod tier emitted every field as mandatory. It never read @required and never emitted .optional(), so `parse<Name>` threw on a reply that correctly omitted a declared-OPTIONAL field — it enforced a contract the metadata does not declare, and disagreed with the tolerant tier sitting in the same generated file, which reads the real attr. Proven before fixing: a value-object with `answer` (@required) and `note` (unmarked) emitted `answer: z.string(), note: z.string(),` — both mandatory — while the tolerant mirror correctly typed `note: string | null`. Optionality now comes from the SAME `isRequired` predicate the tolerant tier uses, so the two tiers in one file cannot disagree about the contract. The `.optional()` wraps the ARRAY rather than its element: an absent list and a list of absent things are different claims. This is the #309 defect family, one tier over. Every other port reuses the payload VO that #309 made @required-correct; only TypeScript re-derives its schema inline here, which is how it drifted — the same "payload tier disagrees with the rest of the toolchain" shape ADR-0052 came out of. Found by a /challenge whose two arms split: one proposed deleting the strict tier outright, the other showed why that cannot happen (Java has no extractor generator, so strict is its ONLY enforcement path). Checking both positions turned up the real state — the strict tier is OVER-strict while the tolerant tier is UNDER-strict (it fires only on lost-required, MALFORMED never throws, and response VOs in the corpus mostly declare no @required at all). This commit fixes the over-strict half; the under-strict half is deliberate and documented at render/src/extract/types.ts:377-383, so it is left alone. One test assertion was written as a lazy spanning regex that matched ACROSS the required field into the optional one's closer, so its negative form could not fail. Replaced with exact block matches. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…g prompt
Mechanical conversion of the test fixtures that declared a `template.output`
purely to drive the inbound tier: they become a `template.prompt` whose
`@responseRef` names the shape they used to name via `@payloadRef`.
`@payloadRef` stays (required on a prompt, and now types the REQUEST) pointing
at the same object — these fixtures never exercise the request side, so reusing
the shape keeps the diff to the direction change alone. A parser-selecting
`@format: json` is dropped; `@format: xml` becomes `@responseFormat: "xml"`,
since on a prompt `@format` is the BODY's syntax.
Two guards inverted rather than moved, and are rewritten as such:
- "throws when template name is not a template.output" used to reject a
template.prompt. A prompt is now the only thing that can carry a response,
so it asserts the opposite, and gains a sibling for a prompt that declares
no @responseRef at all.
- The nested-indentation golden now shows `.optional()` on both levels, since
neither field is @required. The indentation contract it exists to pin is
unchanged.
codegen-ts: 1225 → 1243 passing, 25 → 11 failing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Six commits in: phases A and B green in all five ports, TS codegen re-pointed, codegen-ts at 1243 pass / 11 fail. Records what is done, what remains in dependency order, and two findings deliberately not acted on (the tolerant tier's under-strictness, and whether toolcall is a parsing concern at all). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Surfaced by a peer session reading the site rather than the claim. The Python trace helper's comment asserts it uses "the SAME rule the output-parser / extractor generators use" — and that parity claim is factual: verified that output_parser_generator.py:136, extractor_generator.py:194, output_prompt_generator.py:76 and output_format_spec_emitter.py:19 all read TEMPLATE_ATTR_FORMAT with TEMPLATE_FORMAT_DEFAULT, the identical pattern. So the whole cluster moves to @responseFormat together, and the comment must be UPDATED rather than deleted — deleting it would hide that five sites share one rule, which is how the original @Format misreading spread. Records the rejected branch too: "reading @Format is correct for template.output, since the output body IS the response" is exactly the conflation ADR-0052 dissolves — once template.output is outbound-only its @Format is the syntax of a document rendered OUT, never a response. The trace helper was only implicit in the plan; it is not one of the three generators Phase D/F enumerates, so it was the item most likely to be missed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…ponding prompt codegen-ts 1244 → 1249 passing, 10 → 5 failing. The shared five-port `xpkg-collision-json` fixture gains a responding `template.prompt` rather than having its `template.output` converted. That corpus is the OUTBOUND render oracle and its collision sub-case is also the payload tier's optionality oracle, so converting the node would have weakened two guarantees to fix a third. Adding `DigestPrompt` beside `DigestDoc` leaves every outbound assertion intact and gives the inbound tier a target — the collision test's compile-and-run proof passes against it, extracting each cross-package `Note` into its own shape. The "text-format output gets NO extract block" test is INVERTED rather than deleted. Its premise was the defect: @Format is the syntax of the rendered prompt BODY, so gating the tolerant path on it meant a plain-text prompt whose reply is JSON got a strict parser and no extract at all — the common case, silently unserved. It now pins the opposite, and is a regression test for the bug ADR-0053 fixed. Remaining 5, all heavier: api-docs ACCURACY x3 (api-model.ts:820,914 still keys inbound facts on template.output), the `template-output-simple` conformance golden, and one extractor payload-import case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
A single-reviewer pass (the challenge's second arm died on a budget limit, so
this is one opinion, not a challenge result) found four real problems in changes
I made earlier on this branch. All verified before fixing.
1. A SILENTLY VACUOUS TEST, one edit away from firing. The parser-golden suite
discovers fixtures by `f.endsWith(".output.ts")` and installed an
`expect(true).toBe(true)` placeholder when zero matched. Converting the last
fixture and renaming its golden to `.response.ts` — the obvious next step —
would have left the suite reporting green while asserting nothing. It now
accepts both suffixes AND fails loudly on zero discoveries: a discovery-driven
suite that passes on an empty set is worse than no suite.
2. A DANGLING REF PLANTED IN A FIVE-PORT ORACLE. The `DigestPrompt` node I added
to `xpkg-collision-json` carried `@textRef: "xpkg/digest-prompt"`, and
`templates/xpkg/` contains only `digest.mustache`. Nothing resolves it today,
which is precisely why it would have rotted quietly. Re-pointed at the
existing `xpkg/digest`.
3. THAT CORPUS'S README STILL DOCUMENTED THE OLD GATE — "the extract tier gates
on @Format ∈ {json,xml}" — which is the rule ADR-0052 removed. Four ports
implement against that README, so a stale spec there propagates the defect
into every port that reads it. Rewritten to state the @responseRef gate, that
template.output emits no inbound artifact at all, and why the prompt was ADDED
beside the output rather than replacing it.
4. A COMMENT THAT WOULD HAVE BEEN QUOTED BACK. The XML rationale said "the TS
runtime ships no XML parser." It ships `xml-forgiving-reader.ts`. The honest
reason is that strict all-or-nothing semantics over a FORGIVING reader is
incoherent — it would throw or accept based on how much repair happened. The
false version would have been cited the next time someone proposed strict XML.
Also narrows an over-claim rather than leaving it: `isRequired` reads the
`@required` ATTR only, while the registry documents that attr as "equivalent to
attaching a validator.required child". The fix still did what it claimed — the
strict and tolerant tiers now agree, verified byte-identical predicates — but
"the schema's contract is the metadata's contract" is broader than shipped.
Recorded with the other known findings.
codegen-ts 1249 → 1250 pass, 5 fail (unchanged set). 10/10 gates.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…g prompt api-model.ts built the extractor symbols inside buildTemplateUnit — gated on @Format in {json,xml} and typed on @payloadRef. After ADR-0052 that documented functions template.output no longer emits, named the REQUEST shape as the parse result, and said nothing about the prompts that actually emit them. The block moves to buildPromptUnit, keyed on @responseRef via responseShape(). buildPromptUnit also gains the worked example buildTemplateUnit already had. Without it the page documented extract<Name>(root, ...) while the setup preamble never introduced `root` — the preamble derives its handles from the rendered EXAMPLE text, so moving the symbols without their example silently dropped it. The accuracy gate could not check `kind: prompt` at all — it fell through to "unhandled kind", and its importPath dispatch returned [] for that kind. Both were invisible until this fixture grew its first responding prompt. Now wired to promptRender()'s aggregated prompts.ts, so the gate covers the kind it was already meant to. Its FIELD SHAPE case also moves: the documented extractor shape is the @responseRef VO, asserted on the prompt unit rather than on an outbound document. codegen-ts 1243 -> 1249 pass, 12 -> 8 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
… api-docs move
Two defects, one mine and one it exposed.
MINE: moving the extractor symbols to the prompt unit silently degraded the
outbound render example to `render<Name>({}, provider)`. templateExample built
the render call's object literal from `extract?.fields` — the render symbol had
never carried its own shape, borrowing the extractor's. The moment the extractor
left the unit, the literal had nothing to build from. The render symbol now
carries `payloadFieldShapes` directly, which is where it always belonged: that
is the payload the render handle takes.
EXPOSED, pre-existing, matched rather than hidden: the agent form labels a
field-shape annotation with the symbol's `returns`, so a prompt's render handle
reads `// string: { headline: string }` — the shape describes the PAYLOAD, not
the string it returns. Correct for an extractor (returns IS the shape), wrong for
any render. Unrelated to ADR-0052; the golden now states the real output so the
suite is honest about it.
Three api-docs fixtures gain a responding prompt, because `root` only appears in
the setup preamble when a documented example calls extract<Name>(root, …), and
those examples moved.
codegen-ts 1249 → 1255 pass, 8 → 2 fail. C# still fully green (903+354+291+46),
typecheck clean, 10/10 gates.
The last 2 failures are NOT fixed, deliberately: both need
`fixtures/conformance/template-output-simple/` converted to a responding prompt,
and C# drives its own OutputParserGenerator from that same directory
(OutputParserGeneratorTests.cs:142) while still keying on template.output.
Converting it now would trade 2 TypeScript failures for a red C# port. Those two
reds are a true signal that Phase F is incomplete — the honest place to fix them
is with the port conversion, not before it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
The last TS consumer still reading @Format for the REPLY. It read that one attribute twice — once as the reply's syntax, once as the prompt body's — under a comment calling them "two intentionally different shapes". They are two different FACTS, not two shapes of one, and conflating them mis-parsed every prompt whose body format differed from its reply's. The shipped docs-site fixture is exactly that shape: a text-bodied prompt whose reply is XML. All four TS inbound consumers now go through responseFormatOf(): output-parser, extractor, response-format fragment, and this. `@format` keeps its real job here — the raw body format string render() takes. Pinned by a new test whose discriminating case is @Format: text + @responseFormat: xml, plus its mirror (@Format: xml + @responseFormat: json) so it cannot pass by reading either attribute alone, plus the absent-default and a case asserting the body still renders in its own format. GATE PROVEN TO FAIL: reverting the fix to the old @Format read turns 3 of the 4 red. A gate believed without being seen fail is how the original misreading survived — it shipped under a comment that described the collision accurately and drew the wrong conclusion from it. codegen-ts 1255 → 1259 pass, 2 fail (unchanged: both need the shared template-output-simple fixture, which is coupled to the C#/Java/Python port conversion). Typecheck clean, 10/10 gates. Python and Java trace helpers still read @Format; they port with Phase F. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
`template-output-simple` was the shared inbound oracle: a `template.output` whose `expected/` held the generated PARSER. Under ADR-0052 an output emits no parser, so that golden had no owner — it is the last thing pinning the tier to the wrong subtype. Phase B already added `template-prompt-response-json` (a `@format: text` prompt with a `@responseFormat: json` reply, which is the shape the old design could not express), so the golden moves THERE and `template-output-simple` keeps only its loader expectation — a pure outbound fixture now, which is what its name has always claimed. Two gates were repaired in passing, both the same shape as the defect: - `output-parser-conformance` discovered fixtures by EITHER golden suffix but the inner loop still filtered `.output.ts` alone, so a renamed golden would admit the fixture and then assert over zero files — reporting green while checking nothing, one line below the comment warning about exactly that. It now filters by the same suffix set and fails on an empty match. - `extractor-render-payload-imports` had its template converted to a prompt but still asked `renderRenderHelper()` for it. The render helper is the OUTBOUND tier and is deliberately untouched by this ADR — it is the control proving the inbound move did not drag the outbound tier along — so it throws on a prompt. The model gains its own `template.output` and the render half binds to that. codegen-ts: 1261 pass / 0 fail (was 1259 / 2). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…onseRef ADR-0052 in the C# port. `template.output` is OUTBOUND ONLY — it renders a document or an email and generates nothing that reads a model's reply. The parser-on-receipt, the FR-010 response-format fragment and the tolerant extract now key on a `template.prompt` carrying `@responseRef`, and the reply's syntax comes from `@responseFormat` (ADR-0053) rather than from `@format`, which is the syntax of the prompt BODY being sent. The direction rule lives in ONE place (`Generators/FindInbound.cs`, mirroring `find-inbound.ts`). Three generators previously each decided for themselves and had drifted: OutputParserGenerator applied NO format filter at all, so an email template got a `JsonSerializer.Deserialize` for text the system had just rendered, while OutputPromptGenerator and ExtractorGenerator each applied their own json/xml gate against the wrong attribute. THE PAYLOAD TIER GAINS THE RESPONSE SHAPE. This is the part the plan did not anticipate. C# named its strict record after the resolved VALUE-OBJECT but emitted one only for a `template.output`'s `@payloadRef` — nothing in MetaObjects.Codegen mentioned `template.prompt` at all. A parser bound to `@responseRef` therefore referenced a type nobody declared, and the generated code would not compile. PayloadGenerator now emits the `@responseRef` record too, deduped by resolved VO FQN because CodegenRunner THROWS on a duplicate path (it has no byte-identical collapse) and an output's payload may legitimately be the same declared shape as a prompt's response. TypeScript never hit this: its payload types come from entityFile(), which emits per `object.value` regardless of any template. The strict tier is now JSON-ONLY. `JsonSerializer.Deserialize` has no XML equivalent worth generating — not because no XML reader exists (Render ships a forgiving one) but because strict all-or-nothing semantics layered over a REPAIRING parser is incoherent. An XML reply gets the tolerant extract alone. Before this an XML template got `Deserialize` anyway: a method that could never work. Artifact names follow the direction axis (D4): `<Prompt>.response.cs` and `<Prompt>.responseFormat.cs`. `CSharpNaming.PromptClassName` becomes `ResponseFormatClassName` — `<Name>Prompt` generated FROM a prompt reads as "ClassifyPromptPrompt", which is the confusion this ADR removes. The parser class keeps `<Name>Parser`, matching TypeScript keeping `parse<Template>`. Two obsolete-premise tests were rewritten rather than deleted, because they now prove the opposite and that inversion IS the feature: a text-BODIED prompt with a JSON reply — the common case — used to get a strict parser and no tolerant extract at all. Both halves of the split are pinned in api-docs: a responding prompt documents PAYLOAD/PROMPT/OUTPUT_PARSER, an output documents RENDER alone. Gate proven by breaking it: removing the `@responseRef` predicate turns the "prompt that declares no response" test red. The subtype half of the same filter was found NOT to discriminate on its own — `@responseRef` is prompt-only vocabulary the loader already enforces — so the ref gate is what carries the weight, and that is the one now proven. C#: 903 + 357 + 291 + 46 pass, 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Two corrections a fresh session needs before touching Java. C# is green but its green is weak evidence: most of its 25 failures were fixed by REWRITING the tests, which is the move that launders a bad change into a passing suite. One gate-can-it-fail probe was run and immediately caught a pin that asserted nothing. Three concrete holes are now the first item in the plan — the response-record emission has no gate at all (deleting it leaves C# green), the compile gate bypasses the seam by calling PayloadCodegen directly, and no C# test exercises an xml reply. Java/Kotlin/Python will copy whatever precedent C# sets, so the precedent gets verified first. And Phase F is not three generator re-points per port. The payload tier must emit the @responseRef shape in every non-TS port, because none of them did — TypeScript never hit it since entityFile() emits per object.value regardless of any template. The ports need DIFFERENT answers: C# names its record after the value-object so extending the walk sufficed, while Java and Python name the primary after the TEMPLATE and so need a second record and a name for it. Also records the vacuous-pin mechanism (the subtype half of the direction filter does not discriminate — @responseRef is prompt-only vocabulary the loader already enforces, so pin the ref, not the subtype), that Phase B had already created the replacement fixture the old Task E1 was going to write, and two pre-existing weaknesses found in passing: @required never reads a validator.required child, and the strict Zod schema types field.enum as z.unknown(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…by breaking it C# was green after the ADR-0052 port, and green was weaker evidence than usual: most of its 25 failures were fixed by rewriting the tests, which is the move that launders a bad change into a passing suite. Three holes, all closed here, all verified by deleting the product behaviour and watching the new test go red. 1. The headline change had NO gate. Every C# fixture set @responseRef equal to @payloadRef, so the OUTBOUND walk emitted the response record by coincidence and nothing could tell the two walks apart — deleting the @responseRef walk out of PayloadGenerator left the suite green. The two new PayloadGenerator tests use DIFFERENT value-objects for the two refs, which is the only shape that discriminates, plus the dedupe case where an output's payload and a prompt's response are legitimately the same shape (CodegenRunner throws on a duplicate path — no byte-identical collapse like TS #266). 2. The compile gate bypassed the seam. It built its payload by calling PayloadCodegen.GeneratePayloadRecords directly with a hand-written ref, which proves only that a record CAN be produced for a name the test already knew — it cannot see the generator failing to emit that record at all, which is exactly the ADR-0052 failure mode. Now routed through PayloadGenerator.Generate, with the two refs distinct; removing the inbound walk turns it red on a missing type. 3. Zero C# tests used @responseFormat: "xml", so the strict-tier-is-JSON-only behaviour was entirely unverified. Added the XML case (tolerant extract, no strict Parse/TryParse, no System.Text.Json) and its JSON control — without the control, "no strict tier" could pass because the generator emits nothing useful for either format. Writing 3 first caught a false assertion of my own: a bare `TryParse(` matches the tolerant tier's own int.TryParse coercion mappers, so the pin is on the public signatures. Product code is untouched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…onseRef The Java half of ADR-0052. Phase A/B moved @PromptStyle onto template.prompt and added @responseFormat in all five ports, but only the TypeScript and C# codegen followed — so this branch left the Java reactor NOT COMPILING (OutputFormatSpecEmitter called ot.getPromptStyle() on an OutputTemplate that no longer has it). The plan recorded "Java BUILD SUCCESS" as the baseline; it was not one, and nobody had run it. FindInbound.java is the rule, in one place, mirroring the TS and C# files of the same name: a responding prompt is a template.prompt whose @responseRef resolves, and the gate is @responseRef PRESENCE, never a format value. Three call sites each deciding for themselves is how the old tier drifted — the parser applied NO format filter (so an email template got a Jackson readValue for text the system had just rendered), while the fragment emitter applied its own @Format ∈ {json,xml} gate against the OUTBOUND body's syntax, which is not the syntax of the reply. A text-bodied prompt asking for a JSON answer — the common case — got a strict parser and no fragment at all. Naming. Java's records are TEMPLATE-named, so a responding prompt needs a second record and a name for it: <Short>Response, beside the existing <Short>Payload. The plan proposed routing the response VO through ADR-0044's nested-closure name map instead, but that needs a conditional (the map deliberately excludes a template's primary VO, so a prompt whose @responseRef equals its @payloadRef would have no entry) and it mixes a value-object-derived name into a generator whose convention is template-derived. C# diverges deliberately and correctly: its records are VO-named, so there the response record simply IS the VO's record. Per D4 the fragment class is <Short>ResponseFormat — generated from a prompt now, the old "Prompt" suffix produced ClassifyPromptPrompt. The response VO's nested closure still enters the shared ADR-0044 name map, or a response-side nested VO could collide with a request-side one and clobber its file — the #219 defect one tier down. api-docs moves with codegen (JavaApiModelBuilder shares the same appliesTo predicates, so docs cannot claim a symbol codegen suppressed) and now documents the response record the parser actually returns. Tests: every model asserting an inbound concern was a template.output, so each was converted, and the two tests pinning the OLD rule were replaced rather than adjusted — "@Format=text emits nothing" and "template.prompt is ignored" are the exact statements ADR-0052 reverses. The replacements pin both negative arms separately, because they now fail for different reasons: direction (an output, at @Format json — the value that used to make it apply) versus no declared response. The shared fixtures renamed in Phase B (template-prompt-response-{json,xml}) carry @Format: text with a differing @responseFormat, so a generator still reading @Format emits Format.JSON for both rows and the xml row fails. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…ts no parser Found by challenging the branch's own implementation, then reproduced. The C# inbound tier resolved @responseRef with the ANY-OBJECT NamingRefs.ResolveObjectRef while PayloadGenerator — which emits the record that parser binds — resolves value-only through ResolvePayloadVo. So a @responseRef naming an object.entity resolved in FindInbound, OutputParserGenerator emitted `public static Answer Parse(string text)`, and PayloadGenerator emitted no record at all. CS0246: generated C# that cannot compile. Nothing upstream catches it. TypeScript's loader validates the @responseRef target (validation-passes.ts:336-345); the C# loader validates @payloadRef and never @responseRef, so the model loads with ZERO errors. Measured, not inferred: loaderErrors=0, parsers=1, payloads=0, and the emitted parser binds `Answer`. Two changes, because the resolver alone was not enough: FindInbound.ResponseShape now resolves through ResolvePayloadVo, the same target rule @payloadRef obeys. Java has always done this (SpringPayloadGenerator.resolveValueObject), which is why Java was never exposed. OutputParserGenerator.Generate now skips on RESOLUTION, not presence. InboundTemplates filters on @responseRef presence — it has no root to resolve against — so this loop was the only place the target could be checked, and it called EmitParser unconditionally. Every sibling inbound generator already skipped on ResponseShape; this one alone did not. AppliesTo gains the root parameter it needed to answer the same question, so the api-docs builder can no longer claim a parser symbol codegen suppressed. The regression test asserts the loader accepts the model before asserting codegen emits nothing — otherwise a future C# loader rule would make it pass vacuously. Also fixes Java javadoc left asserting the rule ADR-0052 reverses, in the two classes the previous commit rewrote: "one <TemplateShortName>Parser per template.output", "template.prompt is ignored — only outputs need parsing", "only outputs need prompt-fragment codegen". The code was right and its own documentation contradicted it. Known and NOT fixed here: the C# loader still does not validate @responseRef's target, so the same metadata that fails TypeScript's load still passes C#'s. That is a cross-port loader parity gap covering four ports plus a conformance error fixture — it wants its own change, and codegen now fails closed regardless. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…cs documents it again Two C# gaps, both surfaced by challenging the branch's own implementation. 1. PayloadGenerator filtered its outbound walk to template.output, so a template.prompt's REQUEST shape got no record in this port while Java, Kotlin and Python all emitted one. It reads as dead output from inside codegen — the render HELPER is outbound-only in every port, so nothing generated binds it — which is exactly the reasoning the previous commit's test wrote down. That reasoning was wrong: the binding is HAND-WRITTEN by the adopter, and the shipped docs show the call for a prompt (docs/features/templates-and-payloads.md:224 — `new WelcomePromptPayload(...)` passed to Renderer.render). The walk now covers every template subtype, matching the other ports; the existing dedupe by resolved VO FQN already handles a request and a response naming one shape. Emits_the_response_record_for_a_responding_prompt asserted the request record was ABSENT. That assertion is inverted here, not deleted — it encoded a premise about the consumer surface that turned out to be false, and the test is stronger now: it pins BOTH records, as separate files, from a prompt whose two refs are different value-objects. 2. The api-docs builder stopped emitting a PAYLOAD symbol for a template.output when the inbound tier landed, so a record PayloadGenerator still emits was documented nowhere — contradicting PayloadGenerator's own comment naming AppliesTo "the SINGLE SOURCE OF TRUTH the api-docs builder shares for the PAYLOAD symbol". Restored, gated by that same predicate, with the response symbol deduped by NAME against it: C# names records for the value-object, so when @responseRef and @payloadRef are one shape both walks land on one file and documenting it twice would claim two records exist. Both symbols now take their name from PayloadCodegen.ResolveEmittedName rather than the authored ref. The ref is what the author typed: an FQN one printed `record acme::ai::SupportAnswer`, and under an ADR-0044 collision the emitted record is package-qualified while the ref is not — so the docs named a type that was never emitted. Each behaviour verified by reverting it and watching the corresponding test go red. Emits_no_files_when_no_template_output_nodes became Emits_no_files_when_there_are_no_templates: @payloadRef is required AND loader-validated, so a declared template resolving to nothing is unreachable in a valid load — the case it claimed to cover could not occur, and the reachable version of it lives in A_responseRef_that_is_not_a_value_object_emits_no_parser, which exists only because @responseRef has no equivalent loader rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…onseRef The Kotlin half of ADR-0052, and the last JVM port. Like Java, Kotlin did not compile on this branch — KotlinOutputFormatSpecEmitter read `promptStyle` off an OutputTemplate that no longer has it — so the plan's recorded "Kotlin 315" baseline was never observed. The full server/java reactor now builds and tests green end to end for the first time on this branch. FindInbound.kt is the rule in one place, mirroring the Java, C# and TypeScript files of the same name. Kotlin needs its own copy rather than reusing codegen-spring's because codegen-kotlin does not depend on that module. It resolves @responseRef through KotlinGenUtil.resolveValueObjectRef — the same value-object resolver the payload tier uses — so the parser can never bind a class the payload tier refused to emit. That is the defect the C# port shipped, and writing this port after finding it is why Kotlin never had it. Four generators re-point (parser, fragment, extractor, payload) plus the api-docs builder, which had walked OutputTemplate only and so produced no unit at all for a prompt. Names follow Java: <Short>Response for the response record, <Short>ResponseFormat for the fragment (D4 — generated from a prompt now, the old suffix produced ClassifyPromptPrompt). The response VO's nested closure enters the shared ADR-0044 name maps, both of them, since computePayloadNameMap and computeExtractedNameMap share one walk. The extract tier's own @Format gate is gone too: it sat over the parser's delegating extract and gated on the OUTBOUND body's syntax, so a text-bodied prompt expecting JSON got no extractor. Tests: models asserting an inbound concern were converted, and the four pinning the OLD rule were replaced rather than adjusted — "@Format: text emits nothing" and "template.prompt is ignored" are the statements ADR-0052 reverses. The cross-package collision test needed BOTH nodes, because it asserts the render helper (outbound, template.output-only) and the response-format fragment (inbound, prompt-only) in one pass; a prompt alone left the render helper with nothing to emit. Two snapshots were regenerated from ACTUAL and two written for the new response classes — the snapshot harness has no update flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…in every port Only TypeScript validated it. C#, Java and Python (and Kotlin, which inherits the JVM loader) checked @payloadRef's target and never @responseRef's, so the SAME metadata failed one port's load and passed four — a four-way divergence in the loader, which is the layer whose whole job is that this cannot happen. It was latent while @responseRef only fed the trace helper. ADR-0052 makes the inbound codegen tier key on it, which turned a silent divergence into generated code that does not compile: a @responseRef naming an object.entity resolved in the C# generator, emitted `public static Answer Parse(string text)`, and the payload tier — which resolves value-only — emitted no such record. Measured before the fix: loaderErrors=0, parsers=1, payloads=0. The codegen tiers now fail closed too (094f580 for C#; Java and Kotlin resolve through their payload resolver by construction), so this is belt and braces on purpose: a generator skipping silently is the right behaviour for a generator, and a wrong ref is still an authoring error the author should be told about once, at load, in whatever port they happen to run. The rule is @payloadRef's verbatim — object.value or sourceless object.projection (#210), ERR_INVALID_TEMPLATE, resolved-source envelope — and the response closure's nested targets get the same value-only walk, so the two refs cannot drift on what a legal payload target is. ADR-0039: read resolving, since @responseRef may arrive through extends. Gated by fixtures/conformance/error-template-response-ref-not-value, and each port's rule verified by disabling it and watching that fixture go red: C# 905→1 failure, Java 568→1, Python 404→1. TypeScript already passed it unchanged, which is what makes it the reference. Also drops @PromptStyle from the api-docs-cross-port fixture's template.output — illegal since the attribute moved, and it had been failing Python's load for the whole branch (that fixture is loaded by the api-docs conformance test, not the metamodel corpus, which is why the metamodel gate never saw it). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
… carry it ADR-0052 said it "rides the coordinated pre-1.0 breaking slot alongside FR-037/FR-038"; spec/roadmap.md targeted both of those at 1.1. Challenged, and the roadmap is wrong — but not because 1.1 is the wrong SCHEDULE. It is unreachable. ADR-0035 §1 (+ Amendment 1, and Consequences) : after 1.0, a breaking change to the metamodel vocabulary requires a 2.0. FR-037 R1/R2 retire @readonly and FR-038 retires @Verifiedby — both retire registered vocabulary. A 1.1 MINOR cannot carry either. So the two cells did not express a plan I disagree with; they expressed one the project's own compatibility rule forbids. That is a drafting defect, and it is fixed here rather than adjudicated. The distinction that makes the cells wrong is milestone vs mechanism. docs/RELEASING.md:167 is a row in a VERSION-BUMP table whose axis is pre-1.0 / post-1.0, not a calendar: while the project is 0.x, a MINOR is HOW a break ships, because ^0.20.x does not resolve 0.21.0 and the break is therefore adopted deliberately. The roadmap's Release column is a milestone bucket — FR-031/032/035/036 all read "1.0" and all shipped in 0.x. Both rows now carry the compound form the table already uses for FR-024 ("1.0 · 1.1"), splitting each FR's breaking sliver from its additive body. ADR-0052 keeps its claim but loses the definite article: 0.21.0 was "the pre-1.0 breaking slot" (CHANGELOG, AGENTS.md) and it shipped. ADR-0035 §3 charters a WINDOW — "the next one or two releases" — and ADR-0051:60 already says "a scarce pre-1.0 breaking slot". So this is the next MINOR in that window, not a slot someone else is holding. Adds the section neither document had: what the next breaking MINOR actually carries. Both challenge arms independently reported that no populated successor slot is recorded anywhere — three design docs claim one and nothing enumerates it, which is precisely what a peer session hit when it went to cut the next release and found itself blocked with no way to tell what it was blocked ON. It also records the cost, which had been stated nowhere: this MINOR resets the §G3 quiet-period clock, so it pushes the 1.0 renumbering out by at least one coordinated release. The analogous trade was already adjudicated ship-it for #210. Two corrections folded in: @responseFormat is ADR-0053, not ADR-0052, so anyone counting "the ADR-0052 batch" under-counts the breaking changes by one; and ADR-0035 §C3 still names 0.15.1 as the last breaking move, which 0.21.0 superseded. Challenge record: ~/.claude/challenge-log/adr-0052-release-slot/ (both arms agreed on the verdict from different evidence — one from the CHANGELOG's 0.21.0 precedent, one from the roadmap condemning its own cells at :154). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…ot explicitly Three corrections from the challenge's differential, which caught things both arms missed. The Release column is a MILESTONE bucket, not a version: FR-031/032/035/036 all read "1.0" and all shipped in 0.x. "pre-1.0 MINOR" was a mechanism token in a milestone column, so the cells now read "1.0 · 1.1" — the same compound form FR-024 already uses, and accurate, since the 1.0 line IS where a pre-1.0 breaking MINOR lands. Rules out the option both challenge arms treated as non-existent — and it is the one a reader will reach for first, because it looks free. 1.0.0 is a MAJOR under semver, so a break landing IN the renumbering is neither a pre-1.0 MINOR nor a 2.0 event. It is still wrong: §G3 wants "at least one coordinated release … with no metamodel-breaking changes, to prove the rate has actually dropped". A 1.0 that carries a break has had no quiet period, so it freezes the vocabulary on a stability claim nothing tested. The gate is about evidence, not arithmetic — worth writing down, because the arithmetic reading is the tempting one. Names the tension instead of implying it is closed. CLAUDE.md and the "Before 1.0" section say GA is next and nothing blocks it; this section says a breaking MINOR comes first. Both challenge arms leaned on roadmap:139 to conclude a further MINOR is available and neither engaged the contrary evidence — so the section now states the choice and points at #210's reversal trigger as the mechanism for making it. The one thing that stays unavailable either way is targeting the breaking half at 1.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…ne place The first piece of the Python port. Mirrors the TS, C#, Java and Kotlin files of the same name: a responding prompt is a template.prompt whose @responseRef resolves, and the gate is @responseRef PRESENCE, never a format value. Resolution goes through resolve_payload_vo — the SAME target rule @payloadRef obeys — rather than an any-object lookup. That is deliberate and load-bearing: C# used the any-object resolver here and shipped a parser bound to a record the payload tier refused to emit (CS0246). Writing the later ports through the value-only resolver is what makes them immune by construction. No generator consumes this yet; the three generators plus the payload tier and api-docs are the remaining work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Known coverage regression — not yet fixedFlagged while applying a caution from a parallel session, and it is the same failure class they hit: a shared corpus can lose coverage without any test failing.
This PR strips Fix, for the follow-up: add a responding Scope note: the per-port api-docs accuracy tests were updated for ADR-0052 in Java, C# and Kotlin (each now pins outbound-only for a |
…onseRef The fifth and last port. `template.output` renders OUTBOUND and generates nothing that reads a model's reply; the response record, the FR-010 response-format fragment, the parser-on-receipt and the tolerant extractor all move to a `template.prompt` carrying `@responseRef`. Every one of them now calls through `find_inbound`, which is the direction rule in one place — the gate is `@responseRef` PRESENCE, never a format value. The payload tier gets a SECOND record. Python names records after the TEMPLATE, so a responding prompt needs `<Template>Response` beside `<Template>Payload` — same call Java and Kotlin made, and for the same reason (one naming convention per generator rather than a template-derived name beside a value-object-derived one). It lands in its own `<template>_response.py` module rather than joining the payload module, because strictness is per-module here: a prompt's REQUEST payload emits `extra="forbid"` so a mistyped render slot fails at construction, while a reply record must tolerate unknown fields, and a value-object reachable from both closures could only have one. Every existing model's `<template>_payload.py` stays byte-identical. Emitted file names follow TS and C#: `_output_parser` → `_response_parser`, `_output_prompt` → `_response_format`. ADR-0053 lands in three more places: the strict `parse_*` tier is JSON-only (an XML reply gets the tolerant extract and nothing strict, and the strict record is not even imported — a dead import otherwise); the baked `OutputFormatSpec`'s Format comes from `@responseFormat`; and the TRACE HELPER — a fifth inbound consumer the plan's Phase F list missed — stops deriving the REPLY's parse format from `@format`, the syntax of the prompt BODY. api-docs follows: every template subtype gets a unit now, not just `template.output` (a prompt's `@payloadRef` record was generated and documented nowhere), RENDER is documented only for `template.output` because that is the only subtype the render helper generator emits for, and the inbound symbols hang off `response_shape`. GATES PROVEN TO FAIL, each by removing the behaviour and watching the test go red: binding `@payloadRef` instead of `@responseRef` turns 3 parser tests red including the discriminating round-trip (the request shape is a valid document the parser must REFUSE); forcing the strict tier on turns the XML case red; reading `@format` in the trace helper turns all 3 new cases red; documenting RENDER for a prompt turns the api-docs forward gate red. Every fixture in this commit declares a request shape and a reply shape that share no field name, so binding the wrong ref fails rather than passing by coincidence. Python 1750 pass, 0 fail (was 1733 + 6 fail). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…ing failed
`api-docs-cross-port` had exactly one template — `OrderSummary`, a `template.output`
carrying `@promptStyle: inline` — and that one attribute is what made it exercise the
PROMPT and OUTPUT_PARSER symbol paths in every port's api-docs builder. ADR-0053 makes
`@promptStyle` prompt-only vocabulary, so it had to come off the output, and with it
went every bit of inbound coverage the corpus had.
All five ports stayed green. That is the point: a corpus that stops exercising a code
path emits no diagnostic at all — there is no assertion whose subject has gone missing,
only assertions that quietly cover less. **When a shared corpus loses a case's coverage,
no test fails.** The corpus now carries a README stating which node covers which path,
so an edit that removes one has to remove its stated purpose too.
The model gains `OrderAdvice`, a responding `template.prompt`, with `@format: text` and
`@responseFormat: json` — the discriminating shape — plus disjoint request/reply value
objects so a port binding the wrong ref documents visibly wrong symbols. `OrderSummary`
stays as the outbound control. The byte-gated manifest was regenerated from the TS
oracle in one pass and all five runners re-run.
Adding it surfaced a broken link that predates ADR-0052: the api-docs surface has always
emitted `api/<lang>/<pkg>/<Prompt>.md` for a top-level `template.prompt`, and that page
carries a "Model / metadata" back-link — but `docsFile()` wrote the neutral page for
`template.output` alone, so the link pointed at a page nothing generated, in every doc
tree containing a prompt. ADR-0052 makes a responding prompt the LAST node a doc tree
should omit. Fixed by dropping the subtype filter; no existing golden moved, because no
committed docs fixture had a top-level prompt — which is why it survived.
Also fixes a C# test the branch had already broken: `A_responseRef_that_is_not_a_value_
object_emits_no_parser` asserted `Assert.Empty(load.Errors)` on the premise that the C#
loader validates only `@payloadRef`. The loader gained the `@responseRef` rule two
commits later, and the test's own comment had predicted this ("if the C# loader ever
gains that rule this test would otherwise pass vacuously") — it just went red instead.
Now it asserts BOTH doors: the loader convicts the metadata, and codegen stays
fail-closed anyway, which is the state defence-in-depth exists to survive.
Verified: TS 1383, C# 291+46+905+363, Java+Kotlin reactor BUILD SUCCESS, Python 1750 —
all with the enlarged corpus.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…eFormat The last inbound consumer, and the plan's premise about it was wrong: Java never read `@format` here. It called the 2-arg `MetaObjectExtractor.extract(mo, text)` overload, which hardcodes `Format.JSON`. So a prompt declaring an XML reply got a trace helper that parsed it as JSON — the reply's syntax was INEXPRESSIBLE at this site, not merely mis-read. Same ADR-0053 ruling as TypeScript's and Python's, from the opposite starting point. The format is now baked from `FindInbound.responseFormatOf(prompt)`, so all five of this port's inbound decisions come from one place. GATE PROVEN TO FAIL: restoring the 2-arg call turns 4 of the 7 tests red. The three new ones are discriminating by construction — `@format: text` + `@responseFormat: xml`, its mirror `@format: xml` + `@responseFormat: json`, and the absent-default — so no single attribute read can satisfy them all. Also drops `examples/advanced-modeling/src/generated/ProgramDescriptionOutput.output.ts`. That template declares `@format: markdown`, and the pre-ADR-0052 parser tier applied NO format filter, so the repo shipped an example containing `Schema.parse(JSON.parse(text))` over a rendered marketing description — a generated function that could never work. Under ADR-0052 a `template.output` emits no parser at all and `meta verify --codegen` now reports the file as one a fresh regen would not emit; the example's README and its metadata comment say why. Java reactor BUILD SUCCESS; advanced-modeling drift gate 7 pass / 0 fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…old rule Phase G. `@promptStyle` is prompt-only vocabulary now and the inbound tier keys on `@responseRef`, so a documentation set that says otherwise is not stale prose — it teaches metadata the loader rejects, and file names codegen no longer writes. - **New migration guide** — the two things that change for an existing model (a load error, and a diff), what to do with each `@promptStyle` left on an output, the before/after file names per port, and the one non-mechanical part: where the response RECORD comes from, which differs by port because the ports do not share a naming convention. - **`templates-and-payloads.md`** — the subtype table's axis is direction; `@responseRef` / `@responseFormat` / `@promptStyle` documented as the prompt's inbound half; "Output parsing" → "Response parsing"; the fixture list points at the responding-prompt fixtures instead of `template-output-simple`, which is the outbound control now. - **All five port docs** + the five `metaobjects-prompts` skill references + the skill itself, with the five byte-gated agent-context fixture sets regenerated. A shipped skill teaching illegal metadata is the worst version of this drift: it is read by an agent that will then author it. - **The docs-site fixture** declares `@responseFormat: xml` on its text-bodied prompt — the discriminating shape, and the one its `@xmlText` response member always implied. Its goldens now show `template:@responseFormat` rendered. - **CHANGELOG** — leads with the three ways the old tier had drifted at once, and closes on the corpus lesson rather than the code one. Also updates the plan's STATUS block (all five ports green) and records a finding the plan had listed as work: `verify` needs NO change in any port. Read all three — each keys its render-drift check on `@payloadRef`, which ADR-0052 did not move, and a `@responseRef` shape is parsed rather than rendered, so there is no render drift to check and its target rule is enforced one layer earlier by the loader. sdk + docs-site + advanced-modeling drift: 294 pass, 0 fail. ci-local --quick green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
Only CHANGELOG.md conflicted: main's #320 cut everything under `[Unreleased]` into a `[0.24.0]` section while this branch added its ADR-0052 entry there. Resolved by keeping BOTH, with the ADR-0052 section left under `[Unreleased]`. It is a BREAKING metamodel-vocabulary change and its release slot is still an open human call (before GA or after) — folding it into a cut someone else defined would answer that question silently. Also removes a stray `||||||| constructed merge base` marker that main has been carrying mid-CHANGELOG since an earlier merge. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
…9 → 0.10 move ADR-0052's entry had been parked under `[Unreleased]` to avoid pre-answering "which release carries the break". That placement does not hold anything back: the code merged to `main` in #318, so whatever cuts next from `main` ships it either way. Parked, the only thing it achieved was a `0.24.0` section that omits a breaking change present in its own tarball. So `0.24.0` says what it ships. Its summary previously justified MINOR by two DEFAULT FLIPS alone; it is the pre-1.0 breaking slot, and now reads that way — with the `⚠️ BREAKING FOR METADATA AUTHORS` callout `0.21.0` established, naming the four adopter-visible consequences and pointing at the migration guide. It also folds in the `metamodelVersion` `0.9` → `0.10` move and its gate (#322), placed after the ADR-0052 section it is caused by rather than before it. Per ADR-0035 Amendment 2 a release that moves that number must say so — and this is the first release in the project's history that has one to report. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15
What this is
ADR-0052 makes a template subtype's axis DIRECTION.
template.outputrenders OUTBOUNDand generates nothing that reads a model's reply; the inbound half — the response shape, the
FR-010 response-format fragment, and the parser-on-receipt — moves to a
template.promptcarrying
@responseRef. ADR-0053 adds@responseFormat(json|xml, defaultjson) as thesyntax of the REPLY, distinct from
@format, which is the syntax of the rendered prompt BODY.Breaking: a
@promptStyleleft on atemplate.outputnow fails the load.Status — all five ports, green
scripts/ci-local.sh --quickgreen.The rule lives in one place, per port
FindInbound(TS / C# / Java / Kotlin / Python) is the ADR-0052 direction rule, and everyinbound generator plus each api-docs builder calls through it. The gate is
@responseRefPRESENCE, never a format value.
The old tier had drifted three ways at once, and each way produced generated code that could
not work: the parser applied no format filter (so an
@format: markdowndocument got agenerated
JSON.parseover rendered prose — and this repo shipped one, inexamples/advanced-modeling), while the fragment emitter and the extractor each applied theirown
@format ∈ {json,xml}gate — against the OUTBOUND body's syntax, which says nothing aboutthe reply. A text-bodied prompt asking for a JSON answer, the common case, got a strict parser
and no tolerant extract.
Naming (D4)
Java, Kotlin and Python name records after the TEMPLATE, so a responding prompt needs a second
record:
<Short>Response, beside the existing<Short>Payload. C# diverges correctly —its records are VO-named, so there the response record simply is the VO's record. TypeScript
needs no new record; its payload types come from
entityFile().Python puts its response record in its own module,
<template>_response.py, rather thanjoining the payload module. Strictness is per-module there: a prompt's REQUEST payload emits
extra="forbid"so a mistyped render slot fails at construction, while a reply record musttolerate unknown fields — and a value-object reachable from both closures could carry only one
setting. Every existing model's
<template>_payload.pystays byte-identical.The fragment class is
<Short>ResponseFormat; emitted paths follow the direction everywhere(
.output.*→.response.*,.prompt.*→.responseFormat.*).Five inbound consumers, not three
The plan named three generators. There were five — and the fifth, the trace helper, was
wrong in two different ways:
@format— the syntax of the prompt BODY — to decide how toparse the model's ANSWER.
@formatat all (the plan's premise about it was wrong). It called the2-arg
MetaObjectExtractor.extract(mo, text)overload, which hardcodesFormat.JSON, so anXML reply was inexpressible there rather than merely mis-read.
All three now read
@responseFormat. The Java gate's three new cases are discriminating byconstruction —
@format: text+@responseFormat: xml, its mirror, and the absent-default —so no single attribute read satisfies them all; restoring the old call turns 4 of 7 red.
Two defects found by challenging this branch's own work
A
@responseRefnaming anobject.entityproduced C# that could not compile. Thegenerator resolved it with the any-object resolver while the payload tier resolves value-only,
so the parser emitted
public static Answer Parse(string text)and no such record was emitted— CS0246. Root cause underneath it: only TypeScript validated
@responseRef's target. C#,Java and Python checked
@payloadRefand never@responseRef, so the same metadata failed oneport's load and passed four. Fixed in all ports, gated by a new shared fixture.
A prompt's
@payloadRefgot no record in C# while the other four ports emitted one. Thetest asserting that was mine, written on the reasoning that nothing generated binds it — false:
the binding is hand-written by the adopter. Assertion inverted, not deleted.
The corpus lesson
fixtures/conformance/api-docs-cross-porthad exactly one template, and one@promptStyleonit was the whole reason it exercised the PROMPT and OUTPUT_PARSER paths in every port's
api-docs builder. Removing that attribute — required, since it is prompt-only now — silently
deleted the last inbound coverage in the corpus, and all five ports stayed green. A corpus
that stops exercising a code path emits no diagnostic at all: there is no assertion whose
subject has gone missing, only assertions that quietly cover less.
When a shared corpus loses a case's coverage, no test fails. The corpus now carries a
README naming which case covers which path, so an edit that removes one has to remove its
stated purpose too. It gained
OrderAdvice, a responding prompt with@format: text+@responseFormat: jsonand disjoint request/reply shapes;OrderSummarystays as the outboundcontrol. The byte-gated manifest was regenerated from the TS oracle in one pass and all five
runners re-run.
Adding it surfaced a broken link that predates ADR-0052: api-docs has always emitted
api/<lang>/<pkg>/<Prompt>.mdfor a top-leveltemplate.prompt, and that page carries a"Model / metadata" back-link — but
meta docswrote the neutral page fortemplate.outputalone, so the link pointed at a page nothing generated, in every doc tree containing a prompt.
verifyneeded no change — verified, not assumedThe plan listed
verifyin three ports as remaining work. Reading all three shows they arealready direction-correct: each keys its render-drift check on
@payloadRef, which ADR-0052did not move, and picks body refs per subtype. A
@responseRefshape is parsed rather thanrendered, so there is no render drift to check, and its target rule is enforced one layer
earlier by the loader.
Verification method
Every behavioural claim here was checked by removing the behaviour and confirming the
corresponding test goes red, then restoring it — the three C# gates, the C# fail-closed
resolver, each port's loader rule, and in this session: binding
@payloadRefinstead of@responseRef(3 Python parser tests red, including the discriminating round-trip, where therequest shape is a valid document the parser must REFUSE), forcing the strict tier on (the XML
case red), reading
@formatin either trace helper (all 3 / 4 of 7 red), and documentingRENDER for a prompt (the api-docs forward gate red).
Every fixture touched here declares a request shape and a reply shape that share no field name,
so binding the wrong ref fails rather than passing by coincidence.
Documentation
Migration guide at
docs/features/migrations/template-direction-outbound-vs-inbound.md,plus
templates-and-payloads.md, all five port docs, themetaobjects-promptsskill and itsfive per-port references (with the five byte-gated agent-context fixture sets regenerated), and
the CHANGELOG. A shipped skill teaching illegal metadata is the worst version of this drift: it
is read by an agent that will then author it.
Release slot
ADR-0035 §1 makes a post-1.0 metamodel-vocabulary break a 2.0 event, so a 1.1 MINOR could
never have carried FR-037's
@readOnlyretirement or FR-038's@verifiedByretirement — theroadmap cells that said so were wrong and are fixed. The roadmap now names what the next
breaking MINOR carries, including that
@responseFormatis ADR-0053, so counting the batchfrom ADR-0052 alone under-counts it by one.
Still open, and not a code question: whether that breaking MINOR goes before GA or after.
🤖 Generated with Claude Code
https://claude.ai/code/session_01DhpswkF1NvwxhFWMmdAT15