@metaobjectsdev/codegen-ts 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 (179) hide show
  1. package/dist/column-mapper.d.ts +32 -0
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +91 -8
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/enum-meta.d.ts +20 -0
  6. package/dist/enum-meta.d.ts.map +1 -1
  7. package/dist/enum-meta.js +32 -1
  8. package/dist/enum-meta.js.map +1 -1
  9. package/dist/generator.d.ts +9 -0
  10. package/dist/generator.d.ts.map +1 -1
  11. package/dist/generator.js.map +1 -1
  12. package/dist/generators/api-field-shape.js +1 -1
  13. package/dist/generators/api-field-shape.js.map +1 -1
  14. package/dist/generators/api-model.d.ts.map +1 -1
  15. package/dist/generators/api-model.js +71 -42
  16. package/dist/generators/api-model.js.map +1 -1
  17. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  18. package/dist/generators/docs-data-builder.js +36 -1
  19. package/dist/generators/docs-data-builder.js.map +1 -1
  20. package/dist/generators/docs-data.d.ts +14 -0
  21. package/dist/generators/docs-data.d.ts.map +1 -1
  22. package/dist/generators/docs-file.d.ts.map +1 -1
  23. package/dist/generators/docs-file.js +13 -4
  24. package/dist/generators/docs-file.js.map +1 -1
  25. package/dist/generators/extractor-file.d.ts.map +1 -1
  26. package/dist/generators/extractor-file.js +7 -11
  27. package/dist/generators/extractor-file.js.map +1 -1
  28. package/dist/generators/index.d.ts +4 -0
  29. package/dist/generators/index.d.ts.map +1 -1
  30. package/dist/generators/index.js +5 -0
  31. package/dist/generators/index.js.map +1 -1
  32. package/dist/generators/output-parser-file.d.ts.map +1 -1
  33. package/dist/generators/output-parser-file.js +12 -7
  34. package/dist/generators/output-parser-file.js.map +1 -1
  35. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  36. package/dist/generators/output-prompt-file.js +14 -24
  37. package/dist/generators/output-prompt-file.js.map +1 -1
  38. package/dist/generators/requirement-tests.d.ts +44 -0
  39. package/dist/generators/requirement-tests.d.ts.map +1 -0
  40. package/dist/generators/requirement-tests.js +127 -0
  41. package/dist/generators/requirement-tests.js.map +1 -0
  42. package/dist/generators/requirements-file.d.ts +9 -0
  43. package/dist/generators/requirements-file.d.ts.map +1 -0
  44. package/dist/generators/requirements-file.js +53 -0
  45. package/dist/generators/requirements-file.js.map +1 -0
  46. package/dist/generators/requirements-markdown.d.ts +10 -0
  47. package/dist/generators/requirements-markdown.d.ts.map +1 -0
  48. package/dist/generators/requirements-markdown.js +71 -0
  49. package/dist/generators/requirements-markdown.js.map +1 -0
  50. package/dist/generators/requirements-toon.d.ts +3 -0
  51. package/dist/generators/requirements-toon.d.ts.map +1 -0
  52. package/dist/generators/requirements-toon.js +47 -0
  53. package/dist/generators/requirements-toon.js.map +1 -0
  54. package/dist/generators/requirements-view.d.ts +32 -0
  55. package/dist/generators/requirements-view.d.ts.map +1 -0
  56. package/dist/generators/requirements-view.js +64 -0
  57. package/dist/generators/requirements-view.js.map +1 -0
  58. package/dist/generators/trace-helper-file.d.ts.map +1 -1
  59. package/dist/generators/trace-helper-file.js +19 -11
  60. package/dist/generators/trace-helper-file.js.map +1 -1
  61. package/dist/index.d.ts +13 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +18 -0
  64. package/dist/index.js.map +1 -1
  65. package/dist/metaobjects-config.d.ts +12 -20
  66. package/dist/metaobjects-config.d.ts.map +1 -1
  67. package/dist/metaobjects-config.js +5 -1
  68. package/dist/metaobjects-config.js.map +1 -1
  69. package/dist/orphan-sweep.d.ts +38 -0
  70. package/dist/orphan-sweep.d.ts.map +1 -0
  71. package/dist/orphan-sweep.js +129 -0
  72. package/dist/orphan-sweep.js.map +1 -0
  73. package/dist/overwrite-policy.d.ts +97 -3
  74. package/dist/overwrite-policy.d.ts.map +1 -1
  75. package/dist/overwrite-policy.js +293 -81
  76. package/dist/overwrite-policy.js.map +1 -1
  77. package/dist/projection/build-projection-views.d.ts +8 -0
  78. package/dist/projection/build-projection-views.d.ts.map +1 -1
  79. package/dist/projection/build-projection-views.js +2 -0
  80. package/dist/projection/build-projection-views.js.map +1 -1
  81. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  82. package/dist/projection/extract-view-spec.js +70 -6
  83. package/dist/projection/extract-view-spec.js.map +1 -1
  84. package/dist/reconcile-orphans.d.ts +80 -0
  85. package/dist/reconcile-orphans.d.ts.map +1 -0
  86. package/dist/reconcile-orphans.js +61 -0
  87. package/dist/reconcile-orphans.js.map +1 -0
  88. package/dist/render-engine/embedded-templates.generated.js +1 -1
  89. package/dist/render-engine/embedded-templates.generated.js.map +1 -1
  90. package/dist/requirement-walk.d.ts +57 -0
  91. package/dist/requirement-walk.d.ts.map +1 -0
  92. package/dist/requirement-walk.js +94 -0
  93. package/dist/requirement-walk.js.map +1 -0
  94. package/dist/runner.d.ts +35 -0
  95. package/dist/runner.d.ts.map +1 -1
  96. package/dist/runner.js +217 -21
  97. package/dist/runner.js.map +1 -1
  98. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  99. package/dist/templates/drizzle-schema.js +67 -4
  100. package/dist/templates/drizzle-schema.js.map +1 -1
  101. package/dist/templates/extractor.d.ts +4 -4
  102. package/dist/templates/extractor.d.ts.map +1 -1
  103. package/dist/templates/extractor.js +18 -22
  104. package/dist/templates/extractor.js.map +1 -1
  105. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  106. package/dist/templates/filter-allowlist.js +4 -2
  107. package/dist/templates/filter-allowlist.js.map +1 -1
  108. package/dist/templates/filter-type.d.ts.map +1 -1
  109. package/dist/templates/filter-type.js +6 -2
  110. package/dist/templates/filter-type.js.map +1 -1
  111. package/dist/templates/find-inbound.d.ts +44 -0
  112. package/dist/templates/find-inbound.d.ts.map +1 -0
  113. package/dist/templates/find-inbound.js +69 -0
  114. package/dist/templates/find-inbound.js.map +1 -0
  115. package/dist/templates/output-format-spec-emitter.d.ts.map +1 -1
  116. package/dist/templates/output-format-spec-emitter.js +7 -4
  117. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  118. package/dist/templates/output-parser.d.ts +8 -3
  119. package/dist/templates/output-parser.d.ts.map +1 -1
  120. package/dist/templates/output-parser.js +71 -31
  121. package/dist/templates/output-parser.js.map +1 -1
  122. package/dist/templates/output-prompt.d.ts +6 -5
  123. package/dist/templates/output-prompt.d.ts.map +1 -1
  124. package/dist/templates/output-prompt.js +26 -37
  125. package/dist/templates/output-prompt.js.map +1 -1
  126. package/dist/templates/queries.js +1 -1
  127. package/dist/templates/queries.js.map +1 -1
  128. package/dist/templates/requirement-test.d.ts +12 -0
  129. package/dist/templates/requirement-test.d.ts.map +1 -0
  130. package/dist/templates/requirement-test.js +120 -0
  131. package/dist/templates/requirement-test.js.map +1 -0
  132. package/dist/templates/zod-validators.d.ts.map +1 -1
  133. package/dist/templates/zod-validators.js +32 -15
  134. package/dist/templates/zod-validators.js.map +1 -1
  135. package/package.json +7 -6
  136. package/src/column-mapper.ts +124 -8
  137. package/src/enum-meta.ts +37 -1
  138. package/src/generator.ts +9 -0
  139. package/src/generators/api-field-shape.ts +1 -1
  140. package/src/generators/api-model.ts +69 -44
  141. package/src/generators/docs-data-builder.ts +37 -0
  142. package/src/generators/docs-data.ts +15 -0
  143. package/src/generators/docs-file.ts +13 -4
  144. package/src/generators/extractor-file.ts +7 -11
  145. package/src/generators/index.ts +11 -0
  146. package/src/generators/output-parser-file.ts +12 -7
  147. package/src/generators/output-prompt-file.ts +14 -27
  148. package/src/generators/requirement-tests.ts +203 -0
  149. package/src/generators/requirements-file.ts +71 -0
  150. package/src/generators/requirements-markdown.ts +72 -0
  151. package/src/generators/requirements-toon.ts +64 -0
  152. package/src/generators/requirements-view.ts +93 -0
  153. package/src/generators/trace-helper-file.ts +20 -10
  154. package/src/index.ts +47 -1
  155. package/src/metaobjects-config.ts +17 -22
  156. package/src/orphan-sweep.ts +178 -0
  157. package/src/overwrite-policy.ts +362 -89
  158. package/src/projection/build-projection-views.ts +10 -0
  159. package/src/projection/extract-view-spec.ts +88 -4
  160. package/src/reconcile-orphans.ts +136 -0
  161. package/src/reference/barrel.ts +4 -0
  162. package/src/reference/entity.ts +4 -0
  163. package/src/reference/queries.ts +4 -0
  164. package/src/reference/routes.ts +4 -0
  165. package/src/render-engine/embedded-templates.generated.ts +1 -1
  166. package/src/requirement-walk.ts +124 -0
  167. package/src/runner.ts +266 -27
  168. package/src/templates/drizzle-schema.ts +70 -6
  169. package/src/templates/extractor.ts +19 -24
  170. package/src/templates/filter-allowlist.ts +4 -2
  171. package/src/templates/filter-type.ts +6 -2
  172. package/src/templates/find-inbound.ts +96 -0
  173. package/src/templates/output-format-spec-emitter.ts +7 -4
  174. package/src/templates/output-parser.ts +76 -34
  175. package/src/templates/output-prompt.ts +29 -42
  176. package/src/templates/queries.ts +1 -1
  177. package/src/templates/requirement-test.ts +140 -0
  178. package/src/templates/zod-validators.ts +32 -15
  179. package/templates/docs/entity-page.md.mustache +8 -0
@@ -0,0 +1,96 @@
1
+ // server/typescript/packages/codegen-ts/src/templates/find-inbound.ts
2
+ //
3
+ // The ADR-0052 direction rule, in ONE place.
4
+ //
5
+ // A template subtype's axis is DIRECTION: `template.output` renders outbound (a
6
+ // document or an email) and generates no parser; the inbound half — the response
7
+ // shape, the FR-010 response-format fragment, and the parser-on-receipt — belongs
8
+ // to a `template.prompt` that declares `@responseRef`.
9
+ //
10
+ // Every inbound generator calls through here rather than re-deriving "which
11
+ // templates have a response". Three call sites each deciding for themselves is
12
+ // exactly how the pre-ADR-0052 tier drifted: the parser had no `@kind` filter at
13
+ // all, so an email template generated a parser for text the system had just
14
+ // rendered, while the extractor and the fragment emitter each applied a different
15
+ // json/xml gate.
16
+
17
+ import {
18
+ type MetaData,
19
+ TYPE_TEMPLATE,
20
+ TEMPLATE_SUBTYPE_PROMPT,
21
+ TEMPLATE_ATTR_RESPONSE_REF,
22
+ TEMPLATE_ATTR_RESPONSE_FORMAT,
23
+ RESPONSE_FORMAT_DEFAULT,
24
+ RESPONSE_FORMAT_XML,
25
+ type ResponseFormat,
26
+ resolveObjectRef,
27
+ } from "@metaobjectsdev/metadata";
28
+
29
+ /** What an inbound generator needs about one responding prompt. */
30
+ export interface InboundShape {
31
+ /** The resolved response value-object — the shape a reply is parsed INTO. */
32
+ readonly vo: MetaData;
33
+ /** The `@responseRef` string as authored (bare or fully-qualified). */
34
+ readonly ref: string;
35
+ /**
36
+ * The syntax of the REPLY (ADR-0053) — never the template's `@format`, which is
37
+ * the syntax of the rendered prompt BODY. The two genuinely differ.
38
+ */
39
+ readonly format: ResponseFormat;
40
+ }
41
+
42
+ /**
43
+ * Every `template.prompt` that declares a response shape, in declaration order.
44
+ *
45
+ * The gate is `@responseRef` PRESENCE, not a format value: declaring a response
46
+ * shape is the request for a parser. Gating on `@format` was what let a `text`
47
+ * template get a strict parser but no tolerant extract, and — because `@format`
48
+ * defaults to `text` — would silently emit nothing at all after the re-homing.
49
+ *
50
+ * ADR-0039: resolving accessors throughout. A root has no super, but a template
51
+ * may inherit `@responseRef` from an abstract base via `extends`, and three
52
+ * shipped fixtures rely on exactly that.
53
+ */
54
+ export function inboundTemplates(root: MetaData): MetaData[] {
55
+ return root
56
+ .children()
57
+ .filter(
58
+ (c) =>
59
+ c.type === TYPE_TEMPLATE &&
60
+ c.subType === TEMPLATE_SUBTYPE_PROMPT &&
61
+ typeof c.attr(TEMPLATE_ATTR_RESPONSE_REF) === "string",
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Resolve a prompt's response value-object and reply syntax.
67
+ *
68
+ * Returns `undefined` when the template declares no `@responseRef` or the ref does
69
+ * not resolve — callers skip rather than throw, matching the pre-ADR-0052 contract
70
+ * for an unresolvable payload ref.
71
+ */
72
+ export function responseShape(root: MetaData, tmpl: MetaData): InboundShape | undefined {
73
+ // ADR-0039: resolving — @responseRef may be inherited via extends.
74
+ const ref = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
75
+ if (typeof ref !== "string") return undefined;
76
+ // ADR-0042: a bare @responseRef resolves package-locally, then root-level.
77
+ const vo = resolveObjectRef(root, ref, tmpl.package ?? tmpl.fileDefaultPackage ?? "").node;
78
+ if (!vo) return undefined;
79
+ return { vo, ref, format: responseFormatOf(tmpl) };
80
+ }
81
+
82
+ /**
83
+ * The declared reply syntax, defaulted per ADR-0053.
84
+ *
85
+ * The default is `json` because that reproduces the trace helper's pre-ADR-0053
86
+ * fallback exactly (anything that was not `"xml"` was treated as JSON), which is
87
+ * what makes the attribute's introduction behaviour-preserving rather than a new
88
+ * policy.
89
+ */
90
+ export function responseFormatOf(tmpl: MetaData): ResponseFormat {
91
+ // ADR-0039: resolving — @responseFormat may be inherited via extends.
92
+ const raw = tmpl.attr(TEMPLATE_ATTR_RESPONSE_FORMAT);
93
+ return typeof raw === "string" && raw.toLowerCase() === RESPONSE_FORMAT_XML
94
+ ? RESPONSE_FORMAT_XML
95
+ : RESPONSE_FORMAT_DEFAULT;
96
+ }
@@ -14,11 +14,12 @@ import {
14
14
  FIELD_ATTR_EXAMPLE,
15
15
  FIELD_ATTR_INSTRUCTION,
16
16
  FIELD_ATTR_ENUM_DOC,
17
- TEMPLATE_ATTR_FORMAT,
18
17
  TEMPLATE_ATTR_PROMPT_STYLE,
19
18
  PROMPT_STYLE_INLINE,
20
19
  PROMPT_STYLE_EXAMPLE_ONLY,
20
+ RESPONSE_FORMAT_XML,
21
21
  } from "@metaobjectsdev/metadata";
22
+ import { responseFormatOf } from "./find-inbound.js";
22
23
  import {
23
24
  fields,
24
25
  isRequired,
@@ -42,9 +43,11 @@ export function specLiteral(vo: MetaData, template: MetaData, rootName: string):
42
43
  }
43
44
 
44
45
  function resolveFormat(template: MetaData): string {
45
- // ADR-0039: resolving a template may inherit @format via extends.
46
- const f = template.attr(TEMPLATE_ATTR_FORMAT);
47
- return typeof f === "string" && f.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
46
+ // ADR-0053: the fragment describes the REPLY, so its syntax is @responseFormat
47
+ // NOT @format, which is the syntax of the rendered prompt BODY. Reading @format
48
+ // here typed the instruction "produce your answer like this" off the format of
49
+ // the question.
50
+ return responseFormatOf(template) === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON";
48
51
  }
49
52
 
50
53
  function resolvePromptStyle(template: MetaData): string {
@@ -13,13 +13,15 @@ import {
13
13
  type MetaData,
14
14
  TYPE_FIELD,
15
15
  TYPE_TEMPLATE,
16
- TEMPLATE_SUBTYPE_OUTPUT,
16
+ TEMPLATE_SUBTYPE_PROMPT,
17
17
  FIELD_SUBTYPE_OBJECT,
18
18
  FIELD_ATTR_OBJECT_REF,
19
- TEMPLATE_ATTR_PAYLOAD_REF,
20
- TEMPLATE_ATTR_FORMAT,
19
+ TEMPLATE_ATTR_RESPONSE_REF,
20
+ RESPONSE_FORMAT_XML,
21
21
  resolveObjectRef,
22
22
  } from "@metaobjectsdev/metadata";
23
+ import { responseShape } from "./find-inbound.js";
24
+ import { isRequired } from "./fr010-field-mapping.js";
23
25
  import {
24
26
  nestedMirrorInterfaces,
25
27
  nestedMappers,
@@ -53,7 +55,19 @@ function findTemplate(root: MetaData, name: string): MetaData | undefined {
53
55
  return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
54
56
  }
55
57
 
56
- /** Render the Zod expression for a single field; recurses on @objectRef. */
58
+ /** Render the Zod expression for a single field; recurses on @objectRef.
59
+ *
60
+ * Optionality comes from `@required`, via the SAME `isRequired` predicate the
61
+ * tolerant tier uses — so the two tiers in this file cannot disagree about what
62
+ * the contract is. They used to: this schema emitted every field as mandatory
63
+ * and never read `@required`, so `parse<Name>` threw on a reply that correctly
64
+ * omitted a declared-optional field, while the tolerant extract accepted it.
65
+ *
66
+ * Every other port reuses the payload VO, which #309 made `@required`-correct.
67
+ * TypeScript re-derives its schema inline here, which is how it drifted — the
68
+ * same "payload tier disagrees with the rest of the toolchain" shape ADR-0052
69
+ * came out of.
70
+ */
57
71
  function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, depth: number): string {
58
72
  // isArray is a native (reserved) property on MetaData, not an attr.
59
73
  const isArray = field.resolvedIsArray();
@@ -73,7 +87,10 @@ function fieldZod(field: MetaData, root: MetaData, seen: ReadonlySet<string>, de
73
87
  } else {
74
88
  base = SCALAR_ZOD[field.subType] ?? "z.unknown()";
75
89
  }
76
- return isArray ? `z.array(${base})` : base;
90
+ const shaped = isArray ? `z.array(${base})` : base;
91
+ // `.optional()` wraps the ARRAY, not its element: an absent list and a list of
92
+ // absent things are different claims.
93
+ return isRequired(field) ? shaped : `${shaped}.optional()`;
77
94
  }
78
95
 
79
96
  /** Render a `z.object({ ... })` for an object.value node.
@@ -90,28 +107,34 @@ function renderObjectSchema(vo: MetaData, root: MetaData, seen: ReadonlySet<stri
90
107
  }
91
108
 
92
109
  /**
93
- * Render the full output-parser file for one `template.output` node.
94
- * Throws if the template isn't found, isn't a template.output, or its
95
- * @payloadRef doesn't resolve to an object.value.
110
+ * Render the full response-parser file for one responding `template.prompt`.
111
+ * Throws if the template isn't found, isn't a template.prompt, or its
112
+ * @responseRef is missing / doesn't resolve to an object.value.
113
+ *
114
+ * ADR-0052: the shape parsed INTO is `@responseRef`, not `@payloadRef` —
115
+ * `@payloadRef` types the REQUEST rendered outbound, which is the distinction the
116
+ * trace helper has always drawn ("@responseRef types the result; @payloadRef types
117
+ * the request") and the inbound tier used to ignore.
96
118
  */
97
119
  export function renderOutputParser(root: MetaData, templateName: string, ctx?: RenderContext): string {
98
120
  const tmpl = findTemplate(root, templateName);
99
121
  if (!tmpl) {
100
122
  throw new Error(`template "${templateName}" not found in metadata root`);
101
123
  }
102
- if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
103
- throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
104
- }
105
- // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
106
- const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
107
- if (typeof payloadRef !== "string") {
108
- throw new Error(`template "${templateName}" missing @payloadRef`);
124
+ if (tmpl.subType !== TEMPLATE_SUBTYPE_PROMPT) {
125
+ throw new Error(`template "${templateName}" is not a template.prompt (got subtype "${tmpl.subType}")`);
109
126
  }
110
- // ADR-0042: a bare @payloadRef resolves in the template's package.
111
- const vo = findObject(root, payloadRef, tmpl.package ?? tmpl.fileDefaultPackage ?? "");
112
- if (!vo) {
113
- throw new Error(`template "${templateName}" @payloadRef "${payloadRef}" not found in metadata root`);
127
+ const shape = responseShape(root, tmpl);
128
+ if (!shape) {
129
+ // ADR-0039: resolving — @responseRef may be inherited via extends.
130
+ const declared = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
131
+ throw new Error(
132
+ typeof declared === "string"
133
+ ? `template "${templateName}" @responseRef "${declared}" not found in metadata root`
134
+ : `template "${templateName}" missing @responseRef`,
135
+ );
114
136
  }
137
+ const { vo, ref: payloadRef } = shape;
115
138
 
116
139
  const schema = renderObjectSchema(vo, root, new Set([payloadRef]), 0);
117
140
  const schemaName = `${templateName}Schema`;
@@ -120,13 +143,31 @@ export function renderOutputParser(root: MetaData, templateName: string, ctx?: R
120
143
  const parseName = `parse${templateName}`;
121
144
  const safeParseName = `safeParse${templateName}`;
122
145
 
123
- // FR-010: emit the tolerant extract() API alongside the strict Zod parser when the
124
- // template targets json/xml. The @payloadRef already resolved to a value-object above,
125
- // so a ExtractSchema can always be baked. text-format outputs get no extract.
126
- // ADR-0039: resolving a template may inherit its @* refs/format/kind via extends.
127
- const format = (tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text";
128
- const lc = format.toLowerCase();
129
- const emitExtractLenient = lc === "json" || lc === "xml";
146
+ // FR-010: emit the tolerant extract() API alongside the strict Zod parser.
147
+ //
148
+ // ADR-0052/0053: a declared @responseRef ALWAYS gets the tolerant path, and the
149
+ // syntax comes from @responseFormat (json|xml, default json) never from
150
+ // @format, which is the syntax of the rendered prompt BODY. The old json/xml
151
+ // gate on @format is what made a text-bodied prompt with a JSON reply emit a
152
+ // strict parser and no extract at all.
153
+ const format = shape.format;
154
+
155
+ // The strict Zod tier is JSON-ONLY, by construction.
156
+ //
157
+ // Its body is `Schema.parse(JSON.parse(text))`, and there is no XML equivalent to
158
+ // generate. Not because no XML reader exists — `render/src/extract/
159
+ // xml-forgiving-reader.ts` ships one — but because that reader is FORGIVING by
160
+ // design, and strict all-or-nothing semantics layered over a repairing parser is
161
+ // incoherent: it would throw or accept based on how much repair happened, which
162
+ // is not a contract anyone can reason about. The JS runtime's `JSON.parse` is an
163
+ // exact parser, which is what makes the strict tier meaningful for JSON and only
164
+ // for JSON. Before this, an XML template got `JSON.parse` anyway — a generated
165
+ // function that could never work.
166
+ //
167
+ // So an XML reply gets the tolerant extract and nothing else. Its typed shape is
168
+ // `<Name>Extracted` — a nullable mirror, which is the honest type for a
169
+ // best-effort parse of model output.
170
+ const emitStrict = format !== RESPONSE_FORMAT_XML;
130
171
 
131
172
  const strictBody = `const ${schemaName} = ${schema};
132
173
 
@@ -164,15 +205,14 @@ export function ${safeParseName}(
164
205
  }
165
206
  `;
166
207
 
167
- if (!emitExtractLenient) {
168
- return `import { z } from "zod";\n\n${strictBody}`;
169
- }
170
-
171
- // ---- FR-010 tolerant extract block (json/xml only) ----
208
+ // ---- FR-010 tolerant extract block ----
209
+ // Unconditional since ADR-0052: a declared @responseRef IS the request for the
210
+ // tolerant path, and @responseFormat is a closed json|xml set, so there is no
211
+ // longer a third case to gate on.
172
212
  const extractedName = `${templateName}Extracted`;
173
213
  const extractLenientWithName = `extractLenient${templateName}WithLoader`;
174
214
  const payloadFqnConst = `${templateName.toUpperCase()}_PAYLOAD_NAME`;
175
- const formatEnum = format.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
215
+ const formatEnum = format === RESPONSE_FORMAT_XML ? "Format.XML" : "Format.JSON";
176
216
 
177
217
  // The nullable mirror is the return shape of the delegating extract. Use the nested-aware
178
218
  // emitter so the payload mirror's nested-object / array-of-object components are typed (not
@@ -262,13 +302,15 @@ export function ${extractLenientWithName}(
262
302
  : `import type { MetaRoot } from "@metaobjectsdev/metadata";\n`;
263
303
  const runtimeImport = `import { extractObject } from "@metaobjectsdev/runtime-ts";\n`;
264
304
 
305
+ // `zod` is imported only when the strict tier is emitted — an XML reply's file
306
+ // would otherwise carry an unused import (tsc noUnusedLocals-unsafe).
265
307
  return (
266
- `import { z } from "zod";\n` +
308
+ (emitStrict ? `import { z } from "zod";\n` : "") +
267
309
  `import {\n ${renderImports.join(",\n ")},\n} from "@metaobjectsdev/render";\n` +
268
310
  metadataImport +
269
311
  runtimeImport +
270
312
  `\n` +
271
- `${strictBody}\n` +
313
+ (emitStrict ? `${strictBody}\n` : "") +
272
314
  `${delegating}`
273
315
  );
274
316
  }
@@ -1,11 +1,11 @@
1
1
  // server/typescript/packages/codegen-ts/src/templates/output-prompt.ts
2
2
  //
3
- // Per-template renderer for the FR-010 artifact-1 output-format prompt fragment
4
- // ("produce your answer like this"). For each json/xml `template.output` whose
5
- // @payloadRef resolves to a value-object, emits a `<TemplateName>.prompt.ts` file
6
- // exporting `render<TemplateName>Format(overrides?)` backed by the render engine's
7
- // renderOutputFormat(). The baked OutputFormatSpec's rootName is the payload name,
8
- // so the prompt fragment and the extract() codegen agree on the root name.
3
+ // Per-template renderer for the FR-010 artifact-1 response-format fragment
4
+ // ("produce your answer like this"). For each `template.prompt` whose @responseRef
5
+ // resolves to a value-object, emits a `<PromptName>.responseFormat.ts` file
6
+ // exporting `render<PromptName>Format(overrides?)` backed by the render engine's
7
+ // renderOutputFormat(). The baked OutputFormatSpec's rootName is the RESPONSE VO's
8
+ // name, so the fragment and the extract() codegen agree on the root name.
9
9
  //
10
10
  // Mirrors the C# OutputPromptGenerator + OutputFormatSpecEmitter (split into a
11
11
  // generator factory + this pure renderer, matching the TS output-parser shape).
@@ -13,59 +13,46 @@
13
13
  import {
14
14
  type MetaData,
15
15
  TYPE_TEMPLATE,
16
- TEMPLATE_SUBTYPE_OUTPUT,
17
- TEMPLATE_ATTR_PAYLOAD_REF,
18
- TEMPLATE_ATTR_FORMAT,
19
- resolveObjectRef,
16
+ TEMPLATE_SUBTYPE_PROMPT,
17
+ TEMPLATE_ATTR_RESPONSE_REF,
20
18
  } from "@metaobjectsdev/metadata";
21
19
  import { specLiteral } from "./output-format-spec-emitter.js";
22
-
23
- // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
24
- // ADR-0042: resolveObjectRef gives package-local-before-root-level precedence for a bare ref, FQN-exact otherwise.
25
- function findObject(root: MetaData, name: string, referrerPkg = ""): MetaData | undefined {
26
- return resolveObjectRef(root, name, referrerPkg).node;
27
- }
20
+ import { responseShape } from "./find-inbound.js";
28
21
 
29
22
  // ADR-0039: resolving — root has no super (children()==ownChildren()); a top-level object/template may itself extend, so resolve rather than work-by-accident.
30
23
  function findTemplate(root: MetaData, name: string): MetaData | undefined {
31
24
  return root.children().find((c) => c.type === TYPE_TEMPLATE && c.name === name);
32
25
  }
33
26
 
34
- /** True iff the template.output's @format is json or xml (the renderable structured formats). */
35
- export function templateSupportsPrompt(tmpl: MetaData): boolean {
36
- // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
37
- const f = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
38
- return f === "json" || f === "xml";
39
- }
40
-
41
27
  /**
42
- * Render the full output-prompt file for one json/xml `template.output` node.
43
- * Throws if the template isn't found, isn't a template.output, isn't json/xml,
44
- * or its @payloadRef doesn't resolve to an object.value.
28
+ * Render the full response-format fragment file for one responding
29
+ * `template.prompt`. Throws if the template isn't found, isn't a template.prompt,
30
+ * or its @responseRef is missing / doesn't resolve to an object.value.
31
+ *
32
+ * ADR-0052: the fragment describes the shape named by `@responseRef` — the reply —
33
+ * not `@payloadRef`, which is the request this prompt renders outbound.
45
34
  */
46
35
  export function renderOutputPrompt(root: MetaData, templateName: string): string {
47
36
  const tmpl = findTemplate(root, templateName);
48
37
  if (!tmpl) {
49
38
  throw new Error(`template "${templateName}" not found in metadata root`);
50
39
  }
51
- if (tmpl.subType !== TEMPLATE_SUBTYPE_OUTPUT) {
52
- throw new Error(`template "${templateName}" is not a template.output (got subtype "${tmpl.subType}")`);
53
- }
54
- if (!templateSupportsPrompt(tmpl)) {
55
- throw new Error(`template "${templateName}" @format is not json/xml — no prompt fragment`);
56
- }
57
- // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
58
- const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
59
- if (typeof payloadRef !== "string") {
60
- throw new Error(`template "${templateName}" missing @payloadRef`);
40
+ if (tmpl.subType !== TEMPLATE_SUBTYPE_PROMPT) {
41
+ throw new Error(`template "${templateName}" is not a template.prompt (got subtype "${tmpl.subType}")`);
61
42
  }
62
- // ADR-0042: a bare @payloadRef resolves in the template's package.
63
- const vo = findObject(root, payloadRef, tmpl.package ?? tmpl.fileDefaultPackage ?? "");
64
- if (!vo) {
65
- throw new Error(`template "${templateName}" @payloadRef "${payloadRef}" not found in metadata root`);
43
+ const shape = responseShape(root, tmpl);
44
+ if (!shape) {
45
+ // ADR-0039: resolving — @responseRef may be inherited via extends.
46
+ const declared = tmpl.attr(TEMPLATE_ATTR_RESPONSE_REF);
47
+ throw new Error(
48
+ typeof declared === "string"
49
+ ? `template "${templateName}" @responseRef "${declared}" not found in metadata root`
50
+ : `template "${templateName}" missing @responseRef`,
51
+ );
66
52
  }
53
+ const { vo, ref: payloadRef } = shape;
67
54
 
68
- // rootName == payload name so the prompt fragment and extract() agree.
55
+ // rootName == response VO name so the fragment and extract() agree.
69
56
  const spec = specLiteral(vo, tmpl, payloadRef);
70
57
  const specName = `${templateName}FormatSpec`;
71
58
  const fnName = `render${templateName}Format`;
@@ -82,7 +69,7 @@ export function renderOutputPrompt(root: MetaData, templateName: string): string
82
69
  const ${specName}: OutputFormatSpec = ${spec};
83
70
 
84
71
  /**
85
- * The output-format instruction fragment for the ${templateName} template.output
72
+ * The response-format instruction fragment for the ${templateName} template.prompt
86
73
  * ("produce your answer like this"). Comment-free — guidance lives in prose /
87
74
  * inline placeholders / a filled skeleton. Pass \`overrides\` to override the style
88
75
  * or per-field example/instruction at render time.
@@ -130,7 +130,7 @@ export function renderUpdateFn(entity: MetaObject, ctx: RenderContext): Code {
130
130
  const fnName = updateFnName(entityName);
131
131
  const findByIdFn = findByIdFnName(entityName);
132
132
  // PATCH contract (FR-035): validate the caller's assignments against the
133
- // UPDATE schema (all-optional, PK/@readOnly excluded, no insert-time transforms
133
+ // UPDATE schema (all-optional; PK, @mutability readOnly AND writeOnce excluded; no insert-time transforms
134
134
  // like @autoSet-onCreate → now() or the InsertSchema's discriminator handling)
135
135
  // — NOT `InsertSchema.partial()`. The typed `<Entity>Patch` param makes a
136
136
  // renamed/dropped field a compile error at every call site (PATCH-1..4);
@@ -0,0 +1,140 @@
1
+ // FR-038 §4 — the DEFAULT requirement-test renderer.
2
+ //
3
+ // "Default" is the operative word: an application owns its testing style, so this
4
+ // is what it gets when it registers no renderer of its own. The library supplies
5
+ // DATA (statement, counterexample, status, claimed refs); a renderer supplies SYNTAX.
6
+ // That separation is what keeps bun / vitest / jest / pytest / JUnit / xUnit from
7
+ // each being an upstream change to this package.
8
+ //
9
+ // The load-bearing rule: an empty generated stub must NOT pass. A `live` entry
10
+ // claims the capability works, so an empty green test asserts the opposite of the
11
+ // claim — the original defect recreated in a new place.
12
+
13
+ import { GENERATED_HEADER } from "../constants.js";
14
+ import type { RequirementView, ResolvedClaim } from "../requirement-walk.js";
15
+
16
+ export interface RequirementTestArgs {
17
+ readonly view: RequirementView;
18
+ readonly concern: string;
19
+ readonly statement: string;
20
+ readonly counterexample: string;
21
+ readonly targets: readonly ResolvedClaim[];
22
+ readonly disposition?: string | undefined;
23
+ readonly trackedBy?: readonly string[] | undefined;
24
+ }
25
+
26
+ /**
27
+ * Statuses whose stub is SKIPPED rather than failing.
28
+ *
29
+ * The rule is "does this entry claim the capability works right now?" — only `live`
30
+ * and `partial` do. `planned` is intended-not-built; `abandoned` and `superseded`
31
+ * describe a capability deliberately retired, whose `@implementedBy` is SUPPOSED to
32
+ * dangle. Emitting a failing stub for any of the three reddens an application's suite
33
+ * forever for something nobody intends to build, which is the noise an app silences
34
+ * wholesale — taking the `live` stubs with it.
35
+ *
36
+ * (FR-038 §4 proposes retiring `abandoned`/`superseded` from the vocabulary entirely.
37
+ * Until that breaking cut lands they are legal `@status` values, so the renderer has
38
+ * to handle them.)
39
+ */
40
+ const SKIPPED_STATUSES: ReadonlySet<string> = new Set([
41
+ "planned",
42
+ "abandoned",
43
+ "superseded",
44
+ ]);
45
+
46
+ /**
47
+ * Escape an author-supplied value for a double-quoted TS string literal.
48
+ *
49
+ * Unescaped, a quote closes the literal and a newline breaks it — emitting a stub that
50
+ * does not parse, which `meta gen` still reports as written because nothing here
51
+ * compiles it.
52
+ *
53
+ * Applied to EVERY value that reaches a literal, not only the two obviously-prose ones.
54
+ * `@statement` and `@counterexample` are the fields that look dangerous, but the requirement
55
+ * path and the concern land in the same two literals, and escaping only what looks like
56
+ * prose leaves the identical hole open one line down.
57
+ */
58
+ function forStringLiteral(s: string): string {
59
+ return s
60
+ .replace(/\\/g, "\\\\")
61
+ .replace(/"/g, '\\"')
62
+ .replace(/\r?\n/g, "\\n")
63
+ .replace(/\r/g, "\\r");
64
+ }
65
+
66
+ /**
67
+ * Make an author-supplied value safe inside a JSDoc block, preserving what was written.
68
+ *
69
+ * A comment-terminator would close the block early and spill the remainder into code; a
70
+ * newline needs its own continuation marker or the block's shape breaks. The terminator
71
+ * is separated by a space rather than deleted, so the sentence still reads — and
72
+ * deliberately NOT by a zero-width space, which would keep the text pixel-identical at
73
+ * the cost of putting an invisible character into generated source that nobody can see
74
+ * when debugging it.
75
+ *
76
+ * Every value interpolated into the block goes through this, including the ones whose
77
+ * shape the loader constrains. `@trackedBy` is the one that makes the rule non-optional:
78
+ * it is registered free-form ON PURPOSE — `verify` never resolves it, because which
79
+ * sprint owns a gap belongs in the tracker — so its contract invites arbitrary text.
80
+ */
81
+ function forDocComment(s: string): string {
82
+ return s.replace(/\*\//g, "* /").replace(/\r?\n/g, "\n * ");
83
+ }
84
+
85
+ function claimLines(targets: readonly ResolvedClaim[]): string {
86
+ if (targets.length === 0) {
87
+ return " * (none — this requirement names no model nodes)";
88
+ }
89
+ return targets
90
+ .map((t) => ` * - ${forDocComment(t.ref)} (${forDocComment(t.concern)})`)
91
+ .join("\n");
92
+ }
93
+
94
+ function gapLine(a: RequirementTestArgs): string {
95
+ const tracked = a.trackedBy ?? [];
96
+ if (a.disposition === undefined && tracked.length === 0) return "";
97
+ const decided = forDocComment(a.disposition ?? "undecided");
98
+ const refs = tracked.length > 0 ? ` — ${tracked.map(forDocComment).join(", ")}` : "";
99
+ return `\n *\n * Known gap: ${decided}${refs}`;
100
+ }
101
+
102
+ export function renderRequirementTest(a: RequirementTestArgs): string {
103
+ const skipped = a.view.status !== undefined && SKIPPED_STATUSES.has(a.view.status);
104
+ const statement = forDocComment(a.statement);
105
+ const counterexample = forDocComment(a.counterexample);
106
+ const runner = skipped ? "test.skip" : "test";
107
+ // The test NAME is the link between the ledger entry and the assertion, so it is
108
+ // built from the same two values every time — but it is still a string literal, and
109
+ // an unescaped quote in either closes it.
110
+ const testName = `${forStringLiteral(a.view.path)} [${forStringLiteral(a.concern)}]`;
111
+
112
+ // A `live` or `partial` stub asserts FAILURE until someone writes the real
113
+ // assertion over it. `expect.unreachable` names the requirement in the failure
114
+ // message, so a red run says which claim is unproven rather than just "failed".
115
+ const body = skipped
116
+ ? ` // Intended, not built. Write the assertion when this becomes live.`
117
+ : ` expect.unreachable(\n` +
118
+ ` "unimplemented requirement stub: ${testName} — " +\n` +
119
+ ` "replace this with an assertion that fails when: ${forStringLiteral(a.counterexample)}",\n` +
120
+ ` );`;
121
+
122
+ return (
123
+ `// ${GENERATED_HEADER}.\n` +
124
+ `// The test IDENTITY is generated from the requirement; the BODY below is yours\n` +
125
+ `// and survives regeneration. Do not rename the test — the name is the link.\n` +
126
+ `import { test, expect } from "bun:test";\n` +
127
+ `\n` +
128
+ `/**\n` +
129
+ ` * ${statement}\n` +
130
+ ` *\n` +
131
+ ` * Counterexample: ${counterexample}${gapLine(a)}\n` +
132
+ ` *\n` +
133
+ ` * Claims:\n` +
134
+ `${claimLines(a.targets)}\n` +
135
+ ` */\n` +
136
+ `${runner}("${testName}", () => {\n` +
137
+ `${body}\n` +
138
+ `});\n`
139
+ );
140
+ }