@metaobjectsdev/sdk 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/agent-context/skills/metaobjects-audit/SKILL.md +20 -7
  2. package/agent-context/skills/metaobjects-audit/references/capability-checklist.md +64 -18
  3. package/agent-context/skills/metaobjects-audit/references/csharp.md +2 -1
  4. package/agent-context/skills/metaobjects-audit/references/java.md +3 -2
  5. package/agent-context/skills/metaobjects-audit/references/kotlin.md +2 -1
  6. package/agent-context/skills/metaobjects-audit/references/requirements.md +3 -4
  7. package/agent-context/skills/metaobjects-audit/references/typescript.md +1 -1
  8. package/agent-context/skills/metaobjects-authoring/SKILL.md +28 -3
  9. package/agent-context/skills/metaobjects-authoring/references/requirements.md +49 -23
  10. package/agent-context/skills/metaobjects-codegen/SKILL.md +4 -3
  11. package/agent-context/skills/metaobjects-codegen/references/csharp.md +2 -2
  12. package/agent-context/skills/metaobjects-codegen/references/java.md +3 -3
  13. package/agent-context/skills/metaobjects-codegen/references/kotlin.md +4 -4
  14. package/agent-context/skills/metaobjects-codegen/references/python.md +2 -2
  15. package/agent-context/skills/metaobjects-codegen/references/typescript.md +3 -3
  16. package/agent-context/skills/metaobjects-fit-assessment/SKILL.md +4 -4
  17. package/agent-context/skills/metaobjects-prompts/SKILL.md +53 -32
  18. package/agent-context/skills/metaobjects-prompts/references/csharp.md +26 -21
  19. package/agent-context/skills/metaobjects-prompts/references/java.md +24 -18
  20. package/agent-context/skills/metaobjects-prompts/references/kotlin.md +25 -20
  21. package/agent-context/skills/metaobjects-prompts/references/python.md +68 -26
  22. package/agent-context/skills/metaobjects-prompts/references/typescript.md +29 -23
  23. package/agent-context/skills/metaobjects-verify/references/migration.md +1 -1
  24. package/agent-context/skills/metaobjects-verify/references/requirements.md +36 -22
  25. package/agent-context/templates/always-on.md.mustache +3 -1
  26. package/dist/agent-docs/body.d.ts +1 -1
  27. package/dist/agent-docs/body.d.ts.map +1 -1
  28. package/dist/agent-docs/body.js +24 -2
  29. package/dist/agent-docs/body.js.map +1 -1
  30. package/dist/collection.d.ts +71 -0
  31. package/dist/collection.d.ts.map +1 -0
  32. package/dist/collection.js +121 -0
  33. package/dist/collection.js.map +1 -0
  34. package/dist/config.d.ts +51 -21
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +76 -10
  37. package/dist/config.js.map +1 -1
  38. package/dist/discovery.d.ts +43 -0
  39. package/dist/discovery.d.ts.map +1 -0
  40. package/dist/discovery.js +100 -0
  41. package/dist/discovery.js.map +1 -0
  42. package/dist/index.d.ts +10 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +17 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/memory.d.ts +42 -19
  47. package/dist/memory.d.ts.map +1 -1
  48. package/dist/memory.js +38 -95
  49. package/dist/memory.js.map +1 -1
  50. package/dist/metadata-files.d.ts +67 -0
  51. package/dist/metadata-files.d.ts.map +1 -0
  52. package/dist/metadata-files.js +155 -0
  53. package/dist/metadata-files.js.map +1 -0
  54. package/dist/records/any.d.ts +8 -8
  55. package/dist/records/decision.d.ts +2 -2
  56. package/dist/records/glossary.d.ts +4 -4
  57. package/dist/records/principle.d.ts +2 -2
  58. package/dist/scope.d.ts +16 -0
  59. package/dist/scope.d.ts.map +1 -0
  60. package/dist/scope.js +75 -0
  61. package/dist/scope.js.map +1 -0
  62. package/dist/sources.d.ts +86 -0
  63. package/dist/sources.d.ts.map +1 -0
  64. package/dist/sources.js +130 -0
  65. package/dist/sources.js.map +1 -0
  66. package/package.json +2 -2
  67. package/src/agent-docs/body.ts +24 -2
  68. package/src/collection.ts +183 -0
  69. package/src/config.ts +80 -12
  70. package/src/discovery.ts +110 -0
  71. package/src/index.ts +26 -9
  72. package/src/memory.ts +65 -99
  73. package/src/metadata-files.ts +158 -0
  74. package/src/scope.ts +95 -0
  75. package/src/sources.ts +160 -0
@@ -28,7 +28,7 @@ cite. (3) Whether anyone fills the ledger in is itself untested; advertising it
28
28
  is the brochure failure this skill exists to avoid.
29
29
 
30
30
  TRIGGER to revisit — the release carrying `requirement.*` has shipped, AND either:
31
- Arm A a dogfooded project's `abandoned`/`superseded` entries are shown, retrospectively,
31
+ Arm A a dogfooded project's retired capabilities are shown, retrospectively,
32
32
  to have been discoverable PRE-adoption from repo evidence at file:line standard
33
33
  (removal commit, dead flag, do-not-reintroduce comment);
34
34
  Arm B a team that adopted via this assessment hits a resurrection the ledger would have
@@ -267,8 +267,8 @@ signature class. Hunt all ten classes:
267
267
  @agg/@of/@via`, row-scoped with `@filter`), a predicate quantifier (`origin.aggregate
268
268
  @agg: any|all`), an array rollup (`origin.aggregate @agg: collect`), a non-aggregate
269
269
  derived scalar (`origin.computed @expr`), an argmax-style "one related row's column"
270
- pick (`origin.first @via` — covers the common `DISTINCT ON` / lateral-join shape), a
271
- child collection (`origin.collection`), a soft-delete/status/type row-scope (an
270
+ pick (`origin.first @via` — covers the common `DISTINCT ON` / lateral-join shape),
271
+ a soft-delete/status/type row-scope (an
272
272
  object-level `@filter` on `object.projection`), or `extends`-borrowed — and joins
273
273
  follow declared relationships/`identity.reference` FKs. Expressible → projection
274
274
  candidate (note: an unmodeled hand view is *unmanaged* — invisible to `verify --db`;
@@ -407,7 +407,7 @@ promising it or counting it in benefits:
407
407
  - hand `COUNT/SUM` subqueries, read-model SQL → `object.projection` + `origin.passthrough`
408
408
  / `origin.aggregate` (`count`/`sum`/`avg`/`min`/`max`, +`@filter` for scoped
409
409
  aggregates, +`any`/`all` predicate quantifiers, +`collect` array rollups) /
410
- `origin.collection` / `origin.computed` (a non-aggregate derived scalar) /
410
+ `origin.computed` (a non-aggregate derived scalar) /
411
411
  `origin.first` (an argmax-style "one related row's column" pick — the usual
412
412
  `DISTINCT ON`/lateral-join case) — plus a soft-delete/status/type view via the
413
413
  object-level `@filter` on `object.projection` instead of a hand `WHERE`.
@@ -19,10 +19,13 @@ language lives in a reference fragment (pointed to at the bottom).
19
19
  A **template** is a typed pair: a logical reference to external text + a payload
20
20
  value-object declaring exactly what data the text expects.
21
21
 
22
- | Subtype | Use | Extra attrs |
23
- |---|---|---|
24
- | `template.prompt` | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@requiredTags`, `@model`, `@responseRef` |
25
- | `template.output` | email / docs / config / export | `@kind: document \| email` (default `document`), `@promptStyle`, `@requiredTags`; `@kind: email` adds `@subjectRef` / `@htmlBodyRef` / `@textBodyRef` |
22
+ A template subtype's axis is **DIRECTION** which way the text travels, not what it
23
+ is about (ADR-0052).
24
+
25
+ | Subtype | Direction | Use | Extra attrs |
26
+ |---|---|---|---|
27
+ | `template.prompt` | outbound, and optionally inbound | LLM-targeted | `@maxTokens`, `@requiredSlots`, `@requiredTags`, `@model`, `@responseRef`, `@responseFormat`, `@promptStyle` |
28
+ | `template.output` | outbound ONLY | email / docs / config / export | `@kind: document \| email` (default `document`), `@requiredTags`; `@kind: email` adds `@subjectRef` / `@htmlBodyRef` / `@textBodyRef` |
26
29
 
27
30
  Both carry the generic attrs:
28
31
 
@@ -36,13 +39,22 @@ Both carry the generic attrs:
36
39
  `template.output @kind: email` renders a structured `EmailDocument` (subject + HTML
37
40
  body + optional plain-text body) instead of one string — the TS render helper emits
38
41
  an `EmailDocument`-returning function for it (see the `render-example-email`
39
- conformance fixture). `@promptStyle` (`guide` / `inline` / `exampleOnly`, FR-010)
40
- selects how the output-format prompt fragment presents the payload shape to an LLM
41
- (see "the output-format prompt fragment" below); `@requiredTags` names output tags
42
- the rendered text must contain (`verify` checks it) on both subtypes.
43
- `template.prompt` additionally carries `@responseRef` naming the response
44
- shape (an `object.value` or sourceless `object.projection`, #210) the prompt
45
- expects, for typed LLM-call trace derivation.
42
+ conformance fixture). `@requiredTags` names output tags the rendered text must contain
43
+ (`verify` checks it) on both subtypes.
44
+
45
+ **The INBOUND half belongs to `template.prompt` alone.** `@responseRef` names the
46
+ response shape (an `object.value` or sourceless `object.projection`, #210) a model's
47
+ reply is parsed into, and its PRESENCE is what asks for the whole inbound tier: the
48
+ response record, the response-format fragment, the parser-on-receipt and the tolerant
49
+ extractor. `@responseFormat` (`json` default / `xml`, ADR-0053) is the syntax of that
50
+ REPLY; `@promptStyle` (`guide` / `inline` / `exampleOnly`, FR-010) selects how the
51
+ fragment presents the shape.
52
+
53
+ > **`@format` and `@responseFormat` are different facts.** `@format` is the syntax of
54
+ > the BODY you render; `@responseFormat` is the syntax of the answer you expect. A
55
+ > plain-text prompt asking for a JSON object is the common case. Putting `@promptStyle`
56
+ > or `@responseFormat` on a `template.output` is a LOAD ERROR — an output renders a
57
+ > document and nothing reads a reply to it.
46
58
 
47
59
  A third, structurally different subtype is also registered core vocabulary:
48
60
  **`template.toolcall`** (`@toolName` + `@payloadRef`, ADR-0011) — a vendor-agnostic
@@ -63,7 +75,7 @@ declared `field.<subType>` + `isArray` + `@objectRef`, and a nested payload is a
63
75
  declared `field.object @objectRef` to another `object.value` (`isArray: true` for a
64
76
  list — nested targets stay value-only, loader-enforced). The caller supplies the
65
77
  field values at render time. An `origin.*` child on a payload field is IGNORED for
66
- typing — and the assembly origins (`aggregate` / `collection` / `computed` /
78
+ typing — and the assembly origins (`aggregate` / `computed` /
67
79
  `first`) are ILLEGAL on an `object.value` host (`ERR_SUBTYPE_RULE_VIOLATION`, #210):
68
80
  an origin-derived payload lives on the sourceless projection, which `@payloadRef`
69
81
  accepts. Projections generally are covered by the `metaobjects-authoring` skill and
@@ -192,33 +204,42 @@ For every template, the verify step resolves the text, parses each `{{...}}`
192
204
  reference, and checks it exists on the payload VO. If the text references
193
205
  `{{authorName}}` but the payload only has `displayName`, **the build fails.** This
194
206
  is the prompt-vs-payload drift gate — run it in CI. It walks both `template.prompt`
195
- and `template.output` nodes the same way.
207
+ and `template.output` nodes the same way (both RENDER; only the direction of what comes
208
+ back differs).
209
+
210
+ ## A RESPONDING `template.prompt` generates a parser-on-receipt
211
+
212
+ For every `template.prompt` declaring `@responseRef`, codegen emits a **typed parser**
213
+ that turns a model's reply back into that shape. It binds `@responseRef`, never
214
+ `@payloadRef` — `@payloadRef` types the request the prompt renders outbound, and the
215
+ question and the answer are usually different shapes. Each port emits the parser
216
+ idiomatically: a throw-on-invalid parse plus, where the language has the precedent, a
217
+ Result-style "safe" variant that doesn't throw.
196
218
 
197
- ## `template.output` also generates a parser-on-receipt
219
+ **A `template.output` gets no parser, ever.** Nothing reads a reply to a document. (Before
220
+ ADR-0052 it did, with no format filter at all — so an `@format: markdown` document got a
221
+ generated `JSON.parse` over rendered prose.)
198
222
 
199
- For every `template.output`, codegen emits a **typed parser** that turns an LLM/raw
200
- response back into the `@payloadRef` value-object the reverse direction, reusing
201
- the same payload VO (no new authoring). Each port emits it idiomatically: a
202
- throw-on-invalid parse plus, where the language has the precedent, a Result-style
203
- "safe" variant that doesn't throw. The parser file is a companion to the payload-VO
204
- file; `verify` catches payload-VO ↔ parser drift at build time too.
223
+ The strict tier is JSON-only: an `@responseFormat: xml` reply gets the tolerant extract
224
+ and nothing strict, because strict all-or-nothing semantics layered over a REPAIRING XML
225
+ reader would raise or accept based on how much repair happened.
205
226
 
206
227
  The three-step consumer pattern is identical everywhere: render the prompt → call
207
- your LLM client → parse the response with the generated parser.
228
+ your LLM client → parse the reply with the generated parser.
208
229
 
209
- ## `template.output` also generates the output-format prompt fragment (FR-010)
230
+ ## A RESPONDING `template.prompt` generates the response-format fragment (FR-010)
210
231
 
211
- For every **json/xml-format** `template.output` whose `@payloadRef` resolves to a
212
- value-object, codegen additionally emits an `output-prompt` artifact: a
213
- `render<Name>Format(...)`-shaped function backed by the render engine's
214
- output-format renderer the "produce your answer like this" instruction fragment
215
- you splice into the prompt text so the model returns exactly the shape the parser
216
- above expects. It's generated only for `json`/`xml` outputs (`text`/`html`/`csv`/
217
- `markdown`/`spreadsheet` don't get a fragment) and skipped under the same
218
- unresolved-`@payloadRef` rule as the parser; the fragment and the parser's
219
- `extract()` codegen agree on the same root name.
232
+ For every `template.prompt` whose `@responseRef` resolves, codegen additionally emits a
233
+ `render<Name>Format(...)`-shaped function backed by the render engine's output-format
234
+ renderer the "produce your answer like this" instruction fragment you splice into the
235
+ prompt text so the model returns exactly the shape the parser above expects. The gate is
236
+ `@responseRef` PRESENCE, not a format value: the old `@format {json,xml}` gate read the
237
+ syntax of the OUTBOUND body to decide whether to instruct the model about the syntax of
238
+ its REPLY, so a text-bodied prompt asking for a JSON answer got no fragment at all.
239
+ `@responseFormat` selects which syntax the fragment teaches; the fragment and the
240
+ extractor agree on the same root name.
220
241
 
221
- `@promptStyle` on the `template.output` controls the fragment's presentation
242
+ `@promptStyle` on the `template.prompt` controls the fragment's presentation
222
243
  (default `guide`):
223
244
 
224
245
  | `@promptStyle` | Presentation |
@@ -1,15 +1,18 @@
1
1
  # C# parser-on-receipt
2
2
 
3
- For every `template.output`, `MetaObjects.Codegen`'s `OutputParserGenerator` emits a
4
- **typed parser** that validates an LLM/raw response against the template's
5
- `@payloadRef` payload record. This is the receive side only codegen emits **no**
6
- provider/LLM-call layer; you compose the call yourself. The payload record comes from
7
- the payload generator, so the parser and the payload VO can't silently drift.
3
+ For every RESPONDING `template.prompt` — one declaring `@responseRef`
4
+ `MetaObjects.Codegen`'s `OutputParserGenerator` emits a **typed parser** that validates
5
+ a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never
6
+ `@payloadRef` (which types the request the prompt renders outbound), and a
7
+ `template.output` gets no parser at all. This is the receive side only codegen emits
8
+ **no** provider/LLM-call layer; you compose the call yourself. C# names records after the
9
+ resolved VALUE OBJECT, so the response record simply IS that VO's record — no second
10
+ naming convention, and the parser and the record can't silently drift.
8
11
 
9
12
  ## Contents
10
13
  - Wire the generator
11
14
  - What it emits
12
- - The output-format prompt fragment (FR-010)
15
+ - The response-format prompt fragment (FR-010)
13
16
  - The three-step consumer pattern
14
17
  - Recommended LLM caller (bring-your-own)
15
18
  - Consumer dependency
@@ -27,12 +30,14 @@ dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
27
30
 
28
31
  ## What it emits
29
32
 
30
- Per `template.output`, `dotnet meta gen` writes one `<TemplateName>.output.cs` with a
31
- static `<TemplateName>Parser` following the .NET BCL `Parse`/`TryParse` dual API —
33
+ Per responding `template.prompt`, `dotnet meta gen` writes one
34
+ `<PromptName>.response.cs` with a static `<PromptName>Parser` following the .NET BCL
35
+ `Parse`/`TryParse` dual API. The strict tier is JSON-only — an `@responseFormat: xml`
36
+ reply gets the tolerant extract and neither `Parse` nor `TryParse` —
32
37
  `Parse` throws, `TryParse` returns a bool plus an out-error:
33
38
 
34
39
  ```csharp
35
- // generated <TemplateName>.output.cs (shape)
40
+ // generated <PromptName>.response.cs (shape)
36
41
  public static class NpcResponseParser
37
42
  {
38
43
  /// <exception cref="JsonException">malformed JSON or schema mismatch.</exception>
@@ -53,25 +58,25 @@ generator also emits a tolerant `Extract(string[, ExtractOptions])` (self-contai
53
58
  components) returning an `ExtractionResult` with a nullable `<Payload>Extracted` mirror
54
59
  — a classified per-field report rather than a throw.
55
60
 
56
- ## The output-format prompt fragment (FR-010)
61
+ ## The response-format prompt fragment (FR-010)
57
62
 
58
- For every json/xml-format `template.output`, `MetaObjects.Codegen`'s
59
- `OutputPromptGenerator` (stable name `output-prompt-generator`) emits a
60
- `<TemplateName>.prompt.cs` declaring a static `<TemplateName>Prompt` class with a
61
- `RenderFormat()` / `RenderFormat(PromptOverrides)` pair, backed by the render
62
- engine's `OutputFormatRenderer` — the "produce your answer like this" fragment for
63
- the model. It runs as part of the same `dotnet meta gen` invocation as the payload
63
+ For every responding `template.prompt`, `MetaObjects.Codegen`'s `OutputPromptGenerator`
64
+ (stable name `output-prompt-generator`) emits a `<PromptName>.responseFormat.cs`
65
+ declaring a static `<PromptName>ResponseFormat` class with a `RenderFormat()` /
66
+ `RenderFormat(PromptOverrides)` pair, backed by the render engine's
67
+ `OutputFormatRenderer` — the "produce your answer like this" fragment for the model. It runs as part of the same `dotnet meta gen` invocation as the payload
64
68
  and parser generators:
65
69
 
66
70
  ```bash
67
71
  dotnet meta gen ./metadata --out ./Generated --namespace Acme.Blog
68
72
  ```
69
73
 
70
- `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
71
- controls the fragment's presentation; guidance is never emitted as comments. Skipped
72
- for `template.prompt` nodes, non-json/xml `@format`, and an unresolved
73
- `@payloadRef` the same skip contract as the parser generator. The baked spec's
74
- root name is the payload class name, agreeing with the parser's root.
74
+ `@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
75
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped for
76
+ `template.output` nodes and an unresolved `@responseRef` the same skip contract as the
77
+ parser generator. There is NO format gate: the old `@format {json,xml}` test read the
78
+ syntax of the outbound body to decide whether to describe the reply. The baked spec's
79
+ root name is the response record's, agreeing with the parser's root.
75
80
 
76
81
  ## The three-step consumer pattern
77
82
 
@@ -1,14 +1,16 @@
1
1
  # Java parser-on-receipt
2
2
 
3
- For every `template.output`, `codegen-spring`'s `SpringOutputParserGenerator` emits
4
- a **typed parser** that validates an LLM/raw response against the template's
5
- `@payloadRef` payload record. This is the receive side only codegen emits **no**
6
- provider/LLM-call layer; you compose the call yourself.
3
+ For every RESPONDING `template.prompt` — one declaring `@responseRef`
4
+ `codegen-spring`'s `SpringOutputParserGenerator` emits a **typed parser** that validates
5
+ a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never
6
+ `@payloadRef` (which types the request the prompt renders outbound), and a
7
+ `template.output` gets no parser at all. This is the receive side only — codegen emits
8
+ **no** provider/LLM-call layer; you compose the call yourself.
7
9
 
8
10
  ## Contents
9
11
  - Wire the generator
10
12
  - What it emits
11
- - The output-format prompt fragment (FR-010)
13
+ - The response-format prompt fragment (FR-010)
12
14
  - The three-step consumer pattern
13
15
  - Recommended LLM caller (bring-your-own)
14
16
  - Drift gate
@@ -31,17 +33,20 @@ the payload record it parses into) to the Maven plugin's `<generators>` list:
31
33
 
32
34
  ## What it emits
33
35
 
34
- Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser` class with a static
35
- `parse` method returning the `@payloadRef` payload record. The strict path throws
36
- `com.fasterxml.jackson.core.JsonProcessingException` on malformed input:
36
+ Per responding `template.prompt`, `mvn metaobjects:generate` writes a `<Name>Parser`
37
+ class with a static `parse` method returning the `<Name>Response` record this port's
38
+ records are TEMPLATE-named, so a responding prompt gets a SECOND record beside
39
+ `<Name>Payload`. The strict path throws
40
+ `com.fasterxml.jackson.core.JsonProcessingException` on malformed input, and is
41
+ JSON-only: an `@responseFormat: xml` reply gets the tolerant extract and no `parse`:
37
42
 
38
43
  ```java
39
44
  // generated <Name>Parser.java (shape)
40
45
  public final class NpcResponseParser {
41
46
  private NpcResponseParser() { } // no instances
42
47
 
43
- public static NpcResponsePayload parse(String text) throws JsonProcessingException {
44
- // Jackson-backed: validates the text against the payload record
48
+ public static NpcResponseResponse parse(String text) throws JsonProcessingException {
49
+ // Jackson-backed: validates the text against the @responseRef record
45
50
  }
46
51
  }
47
52
  ```
@@ -61,10 +66,10 @@ reference) return `MetaObjectAware` instances instead, and those need
61
66
  `JsonObjectWriter`/`MetaObjectSerializer` — not a bare mapper — to serialize
62
67
  correctly (see the codegen reference's "Serializing generated objects" section).
63
68
 
64
- ## The output-format prompt fragment (FR-010)
69
+ ## The response-format prompt fragment (FR-010)
65
70
 
66
- For every json/xml-format `template.output`, `codegen-spring`'s
67
- `SpringOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt` class with a
71
+ For every responding `template.prompt`, `codegen-spring`'s
72
+ `SpringOutputPromptGenerator` emits a `<PromptShortName>ResponseFormat` class with a
68
73
  static `renderFormat()` / `renderFormat(PromptOverrides)` pair, backed by
69
74
  `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
70
75
  answer like this" fragment for the model. Wire it alongside
@@ -77,11 +82,12 @@ answer like this" fragment for the model. Wire it alongside
77
82
  </generator>
78
83
  ```
79
84
 
80
- `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
81
- controls the fragment's presentation; guidance is never emitted as comments. Skipped
82
- for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
83
- the same skip contract as the parser generator. The `SPEC`'s root name is the
84
- capitalized payload class name, agreeing with the parser's extract-codegen root.
85
+ `@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
86
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped for
87
+ `template.output` nodes and an unresolved `@responseRef` — the same skip contract as the
88
+ parser generator. There is NO format gate: the old `@format {json,xml}` test read the
89
+ syntax of the outbound body to decide whether to describe the reply. The `SPEC`'s root
90
+ name is the response record's, agreeing with the parser's extract-codegen root.
85
91
 
86
92
  ## The three-step consumer pattern
87
93
 
@@ -1,16 +1,19 @@
1
1
  # Kotlin parser-on-receipt
2
2
 
3
- For every `template.output`, `codegen-kotlin`'s `KotlinOutputParserGenerator` emits
4
- a **typed parser** that validates an LLM/raw response against the template's
5
- `@payloadRef` payload data class. This is the receive side only — codegen emits
6
- **no** provider/LLM-call layer; you compose the call yourself. The payload data
7
- class itself comes from `KotlinPayloadGenerator` (a `@Serializable data class`), so
8
- the parser and the payload VO can't silently drift.
3
+ For every RESPONDING `template.prompt` — one declaring `@responseRef`
4
+ `codegen-kotlin`'s `KotlinOutputParserGenerator` emits a **typed parser** that validates
5
+ a model's reply against that shape. ADR-0052: the tier binds `@responseRef`, never
6
+ `@payloadRef` (which types the request the prompt renders outbound), and a
7
+ `template.output` gets no parser at all. This is the receive side only — codegen emits
8
+ **no** provider/LLM-call layer; you compose the call yourself. The data class itself comes
9
+ from `KotlinPayloadGenerator` (a `@Serializable data class`) — this port's classes are
10
+ TEMPLATE-named, so a responding prompt gets a SECOND one, `<Name>Response`, beside
11
+ `<Name>Payload`, and the parser and the record can't silently drift.
9
12
 
10
13
  ## Contents
11
14
  - Wire the generators
12
15
  - What it emits
13
- - The output-format prompt fragment (FR-010)
16
+ - The response-format prompt fragment (FR-010)
14
17
  - The three-step consumer pattern
15
18
  - Consumer dependency
16
19
  - Recommended LLM caller (bring-your-own)
@@ -34,9 +37,10 @@ the payload it parses into) to the Maven plugin's `<generators>` list:
34
37
 
35
38
  ## What it emits
36
39
 
37
- Per `template.output`, `mvn metaobjects:generate` writes a `<Name>Parser.kt`
38
- `object` with a dual API matching kotlinx.serialization's exception model plus the
39
- Kotlin stdlib `Result<T>` convention:
40
+ Per responding `template.prompt`, `mvn metaobjects:generate` writes a
41
+ `<Name>Parser.kt` `object` with a dual API matching kotlinx.serialization's exception
42
+ model plus the Kotlin stdlib `Result<T>` convention. The strict tier is JSON-only — an
43
+ `@responseFormat: xml` reply gets the tolerant extract and neither strict function:
40
44
 
41
45
  ```kotlin
42
46
  // generated <Name>Parser.kt (shape)
@@ -44,8 +48,8 @@ object NpcResponseParser {
44
48
  private val json: Json = Json { ignoreUnknownKeys = false }
45
49
 
46
50
  /** Throws kotlinx.serialization.SerializationException on bad input. */
47
- fun parseNpcResponse(text: String): NpcResponsePayload =
48
- json.decodeFromString<NpcResponsePayload>(text)
51
+ fun parseNpcResponse(text: String): NpcResponseResponse =
52
+ json.decodeFromString<NpcResponseResponse>(text)
49
53
 
50
54
  /** Result-style — does not throw. */
51
55
  fun safeParseNpcResponse(text: String): Result<NpcResponsePayload> =
@@ -63,10 +67,10 @@ null) and a `extractLenient(loader, text)` overload that delegates to the runtim
63
67
  components. The lenient mirror type (`<Name>Extracted`) uses nullable fields per
64
68
  the Kotlin null-safety port — a missing/malformed component is `null`, not a throw.
65
69
 
66
- ## The output-format prompt fragment (FR-010)
70
+ ## The response-format prompt fragment (FR-010)
67
71
 
68
- For every json/xml-format `template.output`, `codegen-kotlin`'s
69
- `KotlinOutputPromptGenerator` emits a `<TemplateShortName>OutputPrompt.kt` `object`
72
+ For every responding `template.prompt`, `codegen-kotlin`'s
73
+ `KotlinOutputPromptGenerator` emits a `<PromptShortName>ResponseFormat.kt` `object`
70
74
  with `renderFormat()` / `renderFormat(overrides: PromptOverrides)`, backed by
71
75
  `OutputFormatRenderer` from the `metaobjects-render` module — the "produce your
72
76
  answer like this" fragment for the model. Wire it alongside
@@ -79,11 +83,12 @@ answer like this" fragment for the model. Wire it alongside
79
83
  </generator>
80
84
  ```
81
85
 
82
- `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
83
- controls the fragment's presentation; guidance is never emitted as comments. Skipped
84
- for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
85
- the same skip contract as the parser generator. The `SPEC`'s root name is the
86
- capitalized payload class name, agreeing with the parser's extract-codegen root.
86
+ `@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
87
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped for
88
+ `template.output` nodes and an unresolved `@responseRef` — the same skip contract as the
89
+ parser generator. There is NO format gate: the old `@format {json,xml}` test read the
90
+ syntax of the outbound body to decide whether to describe the reply. The `SPEC`'s root
91
+ name is the response class's, agreeing with the parser's extract-codegen root.
87
92
 
88
93
  ## The three-step consumer pattern
89
94
 
@@ -1,11 +1,13 @@
1
1
  # Python parser-on-receipt
2
2
 
3
- For every `template.output`, the `output-parser` generator (run via `metaobjects gen`)
4
- emits a **typed parser** that validates an LLM/raw response against the template's
5
- `@payloadRef` payload Pydantic model. This is the receive side only codegen emits
6
- **no** provider/LLM-call layer; you compose the call yourself. The payload class comes
7
- from the sibling `payload` generator, so the parser and the payload VO can't silently
8
- drift.
3
+ For every RESPONDING `template.prompt` one declaring `@responseRef` — the
4
+ `output-parser` generator (run via `metaobjects gen`) emits a **typed parser** that
5
+ validates a model's reply against that shape's Pydantic model. ADR-0052: the tier binds
6
+ `@responseRef`, never `@payloadRef` (which types the request the prompt renders
7
+ outbound), and a `template.output` gets no parser at all. This is the receive side only
8
+ codegen emits **no** provider/LLM-call layer; you compose the call yourself. The record
9
+ class comes from the sibling `payload` generator, so the parser and the record can't
10
+ silently drift.
9
11
 
10
12
  ## Contents
11
13
  - Wire the generators
@@ -18,7 +20,7 @@ drift.
18
20
 
19
21
  ## Wire the generators
20
22
 
21
- Select `output-parser` (the `payload` generator that emits the `<Name>Payload` it
23
+ Select `output-parser` (the `payload` generator that emits the `<Name>Response` it
22
24
  parses into runs alongside it):
23
25
 
24
26
  ```bash
@@ -30,17 +32,17 @@ metaobjects gen ./metadata --out ./generated --generators payload,output-parser
30
32
 
31
33
  ## What it emits
32
34
 
33
- Per `template.output`, `metaobjects gen` writes one `<template_name>_output_parser.py`
34
- with a single throw-only entry point. Python uses one API (not TS's
35
+ Per responding `template.prompt`, `metaobjects gen` writes one
36
+ `<template_name>_response_parser.py` with a single throw-only entry point. Python uses one API (not TS's
35
37
  `parse`/`safeParse`) because raising `pydantic.ValidationError` is the idiomatic
36
38
  failure — the pydantic / Instructor / FastAPI norm; a Result-style wrapper would be
37
39
  un-Pythonic.
38
40
 
39
41
  ```python
40
- # generated <template_name>_output_parser.py (shape)
41
- from .npc_response_payload import NpcResponsePayload # the @payloadRef VO (Pydantic v2 BaseModel)
42
+ # generated <template_name>_response_parser.py (shape)
43
+ from .npc_response_response import NpcResponseResponse # the @responseRef record (Pydantic v2 BaseModel)
42
44
 
43
- def parse_npc_response(text: str) -> NpcResponsePayload:
45
+ def parse_npc_response(text: str) -> NpcResponseResponse:
44
46
  """Validates text against the payload model.
45
47
 
46
48
  Raises:
@@ -49,16 +51,20 @@ def parse_npc_response(text: str) -> NpcResponsePayload:
49
51
  ...
50
52
  ```
51
53
 
52
- For `@format: json|xml` outputs the generator additionally emits a **tolerant**
53
- best-effort variant — `extract_lenient_<name>(text) -> ExtractionResult[<Name>PayloadExtracted]`
54
+ Every responding prompt ALSO gets a **tolerant** best-effort variant —
55
+ `extract_lenient_<name>_with_loader(root, text) -> ExtractionResult[<Name>ResponseExtracted]`
54
56
  (from the `metaobjects` render `extract` engine) for cases where you want a classified
55
- per-field report rather than a raise. The lenient mirror (`<Name>PayloadExtracted`)
56
- uses `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
57
+ per-field report rather than a raise. The lenient mirror (`<Name>ResponseExtracted`) uses
58
+ `Optional[...]` fields — a missing/malformed component is `None`, not a raise.
57
59
 
58
- ## The output-format prompt fragment (FR-010)
60
+ The STRICT `parse_*` is JSON-only (ADR-0053): an `@responseFormat: xml` reply gets the
61
+ tolerant path and no `parse_*` at all, because strict all-or-nothing semantics layered
62
+ over a REPAIRING XML reader would raise or accept based on how much repair happened.
59
63
 
60
- For every json/xml-format `template.output`, the `output-prompt` generator (run via
61
- `metaobjects gen`) emits one `<template_name>_output_prompt.py` module exposing
64
+ ## The response-format prompt fragment (FR-010)
65
+
66
+ For every responding `template.prompt`, the `output-prompt` generator (run via
67
+ `metaobjects gen`) emits one `<template_name>_response_format.py` module exposing
62
68
  `render_<name>_format(overrides=None) -> str`, backed by the render engine's
63
69
  `render_output_format()` — the "produce your answer like this" fragment for the
64
70
  model:
@@ -67,11 +73,13 @@ model:
67
73
  metaobjects gen ./metadata --out ./generated --generators payload,output-prompt
68
74
  ```
69
75
 
70
- `@promptStyle` on the `template.output` (`guide` default / `inline` / `exampleOnly`)
71
- controls the fragment's presentation; guidance is never emitted as comments. Skipped
72
- for `template.prompt` nodes, non-json/xml `@format`, and unresolved `@payloadRef` —
73
- the same skip contract as the `output-parser` generator. The baked spec's root name
74
- is the payload class name, agreeing with the parser's `extract_<name>()` root.
76
+ `@promptStyle` on the `template.prompt` (`guide` default / `inline` / `exampleOnly`)
77
+ controls the fragment's presentation; guidance is never emitted as comments. Skipped for
78
+ `template.output` nodes and an unresolved `@responseRef` — the same skip contract as the
79
+ `output-parser` generator. There is NO format gate: the old `@format ∈ {json,xml}` test
80
+ read the syntax of the outbound body to decide whether to describe the reply, so a
81
+ text-bodied prompt asking for a JSON answer got no fragment. The baked spec's root name
82
+ is the response class name, agreeing with the parser's `extract_<name>()` root.
75
83
 
76
84
  ## The three-step consumer pattern
77
85
 
@@ -80,7 +88,7 @@ here) → parse the response with the generated parser:
80
88
 
81
89
  ```python
82
90
  from pydantic import ValidationError
83
- from .npc_response_output_parser import parse_npc_response
91
+ from .npc_response_response_parser import parse_npc_response
84
92
 
85
93
  text = my_llm_client.complete(prompt_text) # YOUR code — no generated provider
86
94
  try:
@@ -119,7 +127,41 @@ one boundary, not both.
119
127
  > generator emits a `record_<entity>(recorder, input, redact=None)` helper (per
120
128
  > concrete entity extending `LlmCallBase` with a `@responseRef`/`@payloadRef`-carrying
121
129
  > `template.prompt`) that tolerantly extracts the typed response, builds the base
122
- > trace row, and persists it once. What's still TS-only is the **`call<Entity>`
130
+ > trace row, and persists it once.
131
+ >
132
+ > `LlmCallBase` is shipped metadata, not something you author: opt in when you load,
133
+ > then inherit from it. Without the opt-in the `extends` fails with
134
+ > `ERR_UNRESOLVED_SUPER`.
135
+ >
136
+ > ```python
137
+ > load_directory("metadata/", libraries=["ai"]) # metaobjects::ai::LlmCallBase
138
+ > ```
139
+ > For `metaobjects gen` / `verify`, declare it in `metaobjects.config.yaml` instead —
140
+ > the CLI reads the same opt-in from there:
141
+ > ```yaml
142
+ > metadata: metadata/
143
+ > libraries: ["ai"]
144
+ > ```
145
+ > ```yaml
146
+ > - object.entity:
147
+ > name: AssistantCall
148
+ > extends: metaobjects::ai::LlmCallBase
149
+ > children:
150
+ > - source.rdb: { table: assistant_call, role: primary }
151
+ > - identity.primary: { name: id, fields: ["spanId"] }
152
+ > # Typed columns are AUTHORED, never derived (ADR-0024 amendment).
153
+ > # Declare BOTH: the generated record_<entity> writes voRequest and
154
+ > # voResponse unconditionally, and any key the entity does not declare
155
+ > # raises "no field '<name>' in metadata" on the first persist.
156
+ > - field.object: { name: voRequest, objectRef: MyRequestVO, storage: jsonb }
157
+ > - field.object: { name: voResponse, objectRef: MyResponseVO, storage: jsonb }
158
+ > ```
159
+ >
160
+ > Note the opt-in also brings in the library's own concrete `LlmCall` entity
161
+ > (table `llm_call`) alongside the abstract base — it will appear in codegen output
162
+ > and in a schema diff unless you filter it.
163
+ >
164
+ > What's still TS-only is the **`call<Entity>`
123
165
  > render→call→record convenience loop** — Python intentionally does not emit it,
124
166
  > because the `LlmClient` seam it wraps is BYO / vendor-neutral here (ADR-0024). So
125
167
  > you compose render → your LLM call → the generated `record_<entity>(...)` yourself;