@metaobjectsdev/codegen-ts 0.23.2 → 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 (153) hide show
  1. package/dist/generator.d.ts +9 -0
  2. package/dist/generator.d.ts.map +1 -1
  3. package/dist/generator.js.map +1 -1
  4. package/dist/generators/api-field-shape.js +1 -1
  5. package/dist/generators/api-field-shape.js.map +1 -1
  6. package/dist/generators/api-model.d.ts.map +1 -1
  7. package/dist/generators/api-model.js +71 -42
  8. package/dist/generators/api-model.js.map +1 -1
  9. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  10. package/dist/generators/docs-data-builder.js +36 -1
  11. package/dist/generators/docs-data-builder.js.map +1 -1
  12. package/dist/generators/docs-data.d.ts +14 -0
  13. package/dist/generators/docs-data.d.ts.map +1 -1
  14. package/dist/generators/docs-file.d.ts.map +1 -1
  15. package/dist/generators/docs-file.js +13 -4
  16. package/dist/generators/docs-file.js.map +1 -1
  17. package/dist/generators/extractor-file.d.ts.map +1 -1
  18. package/dist/generators/extractor-file.js +7 -11
  19. package/dist/generators/extractor-file.js.map +1 -1
  20. package/dist/generators/index.d.ts +4 -0
  21. package/dist/generators/index.d.ts.map +1 -1
  22. package/dist/generators/index.js +5 -0
  23. package/dist/generators/index.js.map +1 -1
  24. package/dist/generators/output-parser-file.d.ts.map +1 -1
  25. package/dist/generators/output-parser-file.js +12 -7
  26. package/dist/generators/output-parser-file.js.map +1 -1
  27. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  28. package/dist/generators/output-prompt-file.js +14 -24
  29. package/dist/generators/output-prompt-file.js.map +1 -1
  30. package/dist/generators/requirement-tests.d.ts +44 -0
  31. package/dist/generators/requirement-tests.d.ts.map +1 -0
  32. package/dist/generators/requirement-tests.js +127 -0
  33. package/dist/generators/requirement-tests.js.map +1 -0
  34. package/dist/generators/requirements-file.d.ts +9 -0
  35. package/dist/generators/requirements-file.d.ts.map +1 -0
  36. package/dist/generators/requirements-file.js +53 -0
  37. package/dist/generators/requirements-file.js.map +1 -0
  38. package/dist/generators/requirements-markdown.d.ts +10 -0
  39. package/dist/generators/requirements-markdown.d.ts.map +1 -0
  40. package/dist/generators/requirements-markdown.js +71 -0
  41. package/dist/generators/requirements-markdown.js.map +1 -0
  42. package/dist/generators/requirements-toon.d.ts +3 -0
  43. package/dist/generators/requirements-toon.d.ts.map +1 -0
  44. package/dist/generators/requirements-toon.js +47 -0
  45. package/dist/generators/requirements-toon.js.map +1 -0
  46. package/dist/generators/requirements-view.d.ts +32 -0
  47. package/dist/generators/requirements-view.d.ts.map +1 -0
  48. package/dist/generators/requirements-view.js +64 -0
  49. package/dist/generators/requirements-view.js.map +1 -0
  50. package/dist/generators/trace-helper-file.d.ts.map +1 -1
  51. package/dist/generators/trace-helper-file.js +19 -11
  52. package/dist/generators/trace-helper-file.js.map +1 -1
  53. package/dist/index.d.ts +13 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +18 -0
  56. package/dist/index.js.map +1 -1
  57. package/dist/metaobjects-config.d.ts +12 -20
  58. package/dist/metaobjects-config.d.ts.map +1 -1
  59. package/dist/metaobjects-config.js +5 -1
  60. package/dist/metaobjects-config.js.map +1 -1
  61. package/dist/orphan-sweep.d.ts +38 -0
  62. package/dist/orphan-sweep.d.ts.map +1 -0
  63. package/dist/orphan-sweep.js +129 -0
  64. package/dist/orphan-sweep.js.map +1 -0
  65. package/dist/overwrite-policy.d.ts +97 -3
  66. package/dist/overwrite-policy.d.ts.map +1 -1
  67. package/dist/overwrite-policy.js +293 -81
  68. package/dist/overwrite-policy.js.map +1 -1
  69. package/dist/projection/build-projection-views.d.ts +8 -0
  70. package/dist/projection/build-projection-views.d.ts.map +1 -1
  71. package/dist/projection/build-projection-views.js +2 -0
  72. package/dist/projection/build-projection-views.js.map +1 -1
  73. package/dist/reconcile-orphans.d.ts +80 -0
  74. package/dist/reconcile-orphans.d.ts.map +1 -0
  75. package/dist/reconcile-orphans.js +61 -0
  76. package/dist/reconcile-orphans.js.map +1 -0
  77. package/dist/render-engine/embedded-templates.generated.js +1 -1
  78. package/dist/render-engine/embedded-templates.generated.js.map +1 -1
  79. package/dist/requirement-walk.d.ts +57 -0
  80. package/dist/requirement-walk.d.ts.map +1 -0
  81. package/dist/requirement-walk.js +94 -0
  82. package/dist/requirement-walk.js.map +1 -0
  83. package/dist/runner.d.ts +35 -0
  84. package/dist/runner.d.ts.map +1 -1
  85. package/dist/runner.js +217 -21
  86. package/dist/runner.js.map +1 -1
  87. package/dist/templates/extractor.d.ts +4 -4
  88. package/dist/templates/extractor.d.ts.map +1 -1
  89. package/dist/templates/extractor.js +18 -22
  90. package/dist/templates/extractor.js.map +1 -1
  91. package/dist/templates/find-inbound.d.ts +44 -0
  92. package/dist/templates/find-inbound.d.ts.map +1 -0
  93. package/dist/templates/find-inbound.js +69 -0
  94. package/dist/templates/find-inbound.js.map +1 -0
  95. package/dist/templates/output-format-spec-emitter.d.ts.map +1 -1
  96. package/dist/templates/output-format-spec-emitter.js +7 -4
  97. package/dist/templates/output-format-spec-emitter.js.map +1 -1
  98. package/dist/templates/output-parser.d.ts +8 -3
  99. package/dist/templates/output-parser.d.ts.map +1 -1
  100. package/dist/templates/output-parser.js +71 -31
  101. package/dist/templates/output-parser.js.map +1 -1
  102. package/dist/templates/output-prompt.d.ts +6 -5
  103. package/dist/templates/output-prompt.d.ts.map +1 -1
  104. package/dist/templates/output-prompt.js +26 -37
  105. package/dist/templates/output-prompt.js.map +1 -1
  106. package/dist/templates/queries.js +1 -1
  107. package/dist/templates/queries.js.map +1 -1
  108. package/dist/templates/requirement-test.d.ts +12 -0
  109. package/dist/templates/requirement-test.d.ts.map +1 -0
  110. package/dist/templates/requirement-test.js +120 -0
  111. package/dist/templates/requirement-test.js.map +1 -0
  112. package/dist/templates/zod-validators.d.ts.map +1 -1
  113. package/dist/templates/zod-validators.js +32 -15
  114. package/dist/templates/zod-validators.js.map +1 -1
  115. package/package.json +7 -6
  116. package/src/generator.ts +9 -0
  117. package/src/generators/api-field-shape.ts +1 -1
  118. package/src/generators/api-model.ts +69 -44
  119. package/src/generators/docs-data-builder.ts +37 -0
  120. package/src/generators/docs-data.ts +15 -0
  121. package/src/generators/docs-file.ts +13 -4
  122. package/src/generators/extractor-file.ts +7 -11
  123. package/src/generators/index.ts +11 -0
  124. package/src/generators/output-parser-file.ts +12 -7
  125. package/src/generators/output-prompt-file.ts +14 -27
  126. package/src/generators/requirement-tests.ts +203 -0
  127. package/src/generators/requirements-file.ts +71 -0
  128. package/src/generators/requirements-markdown.ts +72 -0
  129. package/src/generators/requirements-toon.ts +64 -0
  130. package/src/generators/requirements-view.ts +93 -0
  131. package/src/generators/trace-helper-file.ts +20 -10
  132. package/src/index.ts +47 -1
  133. package/src/metaobjects-config.ts +17 -22
  134. package/src/orphan-sweep.ts +178 -0
  135. package/src/overwrite-policy.ts +362 -89
  136. package/src/projection/build-projection-views.ts +10 -0
  137. package/src/reconcile-orphans.ts +136 -0
  138. package/src/reference/barrel.ts +4 -0
  139. package/src/reference/entity.ts +4 -0
  140. package/src/reference/queries.ts +4 -0
  141. package/src/reference/routes.ts +4 -0
  142. package/src/render-engine/embedded-templates.generated.ts +1 -1
  143. package/src/requirement-walk.ts +124 -0
  144. package/src/runner.ts +266 -27
  145. package/src/templates/extractor.ts +19 -24
  146. package/src/templates/find-inbound.ts +96 -0
  147. package/src/templates/output-format-spec-emitter.ts +7 -4
  148. package/src/templates/output-parser.ts +76 -34
  149. package/src/templates/output-prompt.ts +29 -42
  150. package/src/templates/queries.ts +1 -1
  151. package/src/templates/requirement-test.ts +140 -0
  152. package/src/templates/zod-validators.ts +32 -15
  153. package/templates/docs/entity-page.md.mustache +8 -0
@@ -107,6 +107,7 @@ import {
107
107
  variableNameFromEntity,
108
108
  } from "../naming.js";
109
109
  import { getPkInfo } from "../templates/queries.js";
110
+ import { responseShape } from "../templates/find-inbound.js";
110
111
  import { isTphSubtype } from "../templates/zod-validators.js";
111
112
  import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
112
113
  import { isCallableEntity } from "../templates/callable-file.js";
@@ -823,50 +824,21 @@ function templateOutputs(root: MetaRoot): MetaData[] {
823
824
  function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout): ApiUnitDoc {
824
825
  const name = tmpl.name;
825
826
  const symbols: ApiSymbol[] = [];
826
- // extractor + render-helper generators emit FLAT `<Name>.extractor.ts` /
827
- // `<Name>.render.ts` (no package folding), so importPath ignores layout.
828
- const extractorMod = templateModulePath(`${name}.extractor`);
827
+ // The render-helper generator emits a FLAT `<Name>.render.ts` (no package
828
+ // folding), so importPath ignores layout.
829
829
  const renderMod = templateModulePath(`${name}.render`);
830
830
 
831
- // ADR-0039: resolving — a template may inherit @payloadRef/@format/@kind via extends.
831
+ // ADR-0039: resolving — a template may inherit @payloadRef/@kind via extends.
832
832
  const payloadRef = tmpl.attr(TEMPLATE_ATTR_PAYLOAD_REF);
833
833
  const payload = typeof payloadRef === "string" ? payloadRef : undefined;
834
- const format = ((tmpl.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
835
834
  const kind = ((tmpl.attr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
836
835
 
837
- // --- extractor: only json/xml output-parsers expose the extract API (matches
838
- // extractor-file.ts's `if (format !== "json" && format !== "xml") continue`). ---
839
- if (payload && (format === "json" || format === "xml")) {
840
- const extract = `extract${name}`;
841
- const extractLenient = `extractLenient${name}`;
842
- // The extractor's strict return IS the @payloadRef value-object's interface —
843
- // document its field shape (same VO field walk the payload interface emitter
844
- // uses) so an agent sees what `extract<Name>` yields, not just the type name.
845
- const payloadShape = payloadFieldShapes(root, payload);
846
- const extractSym: ApiSymbol = {
847
- name: extract,
848
- kind: "extractor",
849
- importPath: extractorMod,
850
- signature: `${extract}(root: MetaRoot, text: string): ${payload}`,
851
- params: [`root: MetaRoot`, `text: string`],
852
- returns: payload,
853
- throws: `Error when a @required field is lost (the strict opt-in gate).`,
854
- usage: `Parse dirty LLM ${format} text into a strict, fully-typed ${payload} graph.`,
855
- };
856
- if (payloadShape !== undefined) extractSym.fields = payloadShape;
857
- symbols.push(
858
- extractSym,
859
- {
860
- name: extractLenient,
861
- kind: "extractor",
862
- importPath: extractorMod,
863
- signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
864
- params: [`root: MetaRoot`, `text: string`],
865
- returns: `ExtractionResult<${name}Extracted>`,
866
- usage: `Never-throwing extract; inspect report for lost/defaulted fields.`,
867
- },
868
- );
869
- }
836
+ // ADR-0052: a template.output documents its RENDER and nothing else. The
837
+ // extractor symbols used to be built here, gated on @format {json,xml} and
838
+ // typed on @payloadRef so the reference documented functions this subtype no
839
+ // longer emits, named the REQUEST shape as the parse result, and said nothing
840
+ // about the prompts that actually do emit them. They now live in
841
+ // buildPromptUnit, keyed on @responseRef.
870
842
 
871
843
  // --- render: render<Name>; document → string, email → EmailDocument
872
844
  // (matches render-helper.ts's @kind branch). Render is emitted for any
@@ -875,7 +847,7 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
875
847
  const render = `render${name}`;
876
848
  const isEmail = kind === TEMPLATE_KIND_EMAIL;
877
849
  const returns = isEmail ? "EmailDocument" : "string";
878
- symbols.push({
850
+ const renderSym: ApiSymbol = {
879
851
  name: render,
880
852
  kind: "render",
881
853
  importPath: renderMod,
@@ -885,7 +857,14 @@ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout
885
857
  usage: isEmail
886
858
  ? `Render the ${name} email (subject + bodies) from a typed ${payload} payload.`
887
859
  : `Render the ${name} document from a typed ${payload} payload.`,
888
- });
860
+ };
861
+ // The payload shape used to reach this unit only via the extractor symbol.
862
+ // ADR-0052 moved that away, which left the render example with no field shape
863
+ // to build from — it degraded to `render<Name>({}, provider)`. Attach it here,
864
+ // where it always belonged: this is the payload the render handle takes.
865
+ const renderShape = payloadFieldShapes(root, payload);
866
+ if (renderShape !== undefined) renderSym.fields = renderShape;
867
+ symbols.push(renderSym);
889
868
  }
890
869
 
891
870
  const unit: ApiUnitDoc = {
@@ -950,12 +929,57 @@ function buildPromptUnit(tmpl: MetaData, root: MetaRoot): ApiUnitDoc {
950
929
  symbols.push(sym);
951
930
  }
952
931
 
953
- return {
932
+ // --- ADR-0052 inbound half: a prompt declaring @responseRef also owns the
933
+ // parser-on-receipt and the tolerant extract. Gated on @responseRef
934
+ // PRESENCE, matching extractor-file.ts / output-parser-file.ts — never on
935
+ // a format value, and never on @payloadRef, which types the REQUEST. ---
936
+ const shape = responseShape(root, tmpl);
937
+ if (shape) {
938
+ const { vo, ref: responseRef, format } = shape;
939
+ // The extractor generator emits a FLAT `<Name>.extractor.ts`.
940
+ const extractorMod = templateModulePath(`${name}.extractor`);
941
+ const extract = `extract${name}`;
942
+ const extractLenient = `extractLenient${name}`;
943
+ // The strict return IS the @responseRef value-object's interface — document
944
+ // its field shape so an agent sees what `extract<Name>` yields, not just a
945
+ // type name. `vo` is the resolved node; `responseRef` is the authored ref.
946
+ void vo;
947
+ const responseFieldShape = payloadFieldShapes(root, responseRef);
948
+ const extractSym: ApiSymbol = {
949
+ name: extract,
950
+ kind: "extractor",
951
+ importPath: extractorMod,
952
+ signature: `${extract}(root: MetaRoot, text: string): ${responseRef}`,
953
+ params: [`root: MetaRoot`, `text: string`],
954
+ returns: responseRef,
955
+ throws: `Error when a @required field is lost (the strict opt-in gate).`,
956
+ usage: `Parse the model's ${format} reply to ${name} into a strict, fully-typed ${responseRef} graph.`,
957
+ };
958
+ if (responseFieldShape !== undefined) extractSym.fields = responseFieldShape;
959
+ symbols.push(extractSym, {
960
+ name: extractLenient,
961
+ kind: "extractor",
962
+ importPath: extractorMod,
963
+ signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
964
+ params: [`root: MetaRoot`, `text: string`],
965
+ returns: `ExtractionResult<${name}Extracted>`,
966
+ usage: `Never-throwing extract of the ${name} reply; inspect report for lost/defaulted fields.`,
967
+ });
968
+ }
969
+
970
+ const unit: ApiUnitDoc = {
954
971
  node: name,
955
972
  package: effectivePackage(tmpl),
956
973
  nodeKind: "template",
957
974
  symbols,
958
975
  };
976
+ // ADR-0052: the extractor symbols moved here, so their worked example must move
977
+ // with them. Without this the page documents `extract<Name>(root, …)` but the
978
+ // setup preamble never introduces `root`, because the preamble derives its
979
+ // handles from the rendered EXAMPLE text.
980
+ const example = templateExample(name, symbols);
981
+ if (example !== undefined) unit.example = example;
982
+ return unit;
959
983
  }
960
984
 
961
985
  // ---------------------------------------------------------------------------
@@ -1069,9 +1093,10 @@ function templateExample(name: string, symbols: ApiSymbol[]): UnitExample | unde
1069
1093
  if (renderSym !== undefined) {
1070
1094
  picks.push({ name: renderSym.name, importPath: renderSym.importPath });
1071
1095
  // Render's payload object literal comes from the @payloadRef VO shape the
1072
- // render symbol returns/consumes; reuse the extractor's documented payload
1073
- // fields when present so the example body is real.
1074
- const payloadFields = extract?.fields;
1096
+ // render symbol consumes. Prefer the render symbol's OWN fields — it borrowed
1097
+ // the extractor's before ADR-0052, which silently degraded the example to
1098
+ // `render<Name>({}, provider)` the moment the extractor moved to the prompt.
1099
+ const payloadFields = renderSym.fields ?? extract?.fields;
1075
1100
  const payloadLit = objectLiteralFromFields(payloadFields);
1076
1101
  body.push(`const output = ${renderSym.name}(${payloadLit}, provider);`);
1077
1102
  }
@@ -11,7 +11,9 @@ import {
11
11
  type MetaRoot,
12
12
  TYPE_TEMPLATE,
13
13
  TEMPLATE_ATTR_PAYLOAD_REF,
14
+ REQUIREMENT_ATTR_STATEMENT,
14
15
  OBJECT_SUBTYPE_VALUE,
16
+ OBJECT_SUBTYPE_ENTITY,
15
17
  IDENTITY_SUBTYPE_PRIMARY,
16
18
  IDENTITY_SUBTYPE_SECONDARY,
17
19
  IDENTITY_SUBTYPE_REFERENCE,
@@ -52,6 +54,9 @@ import { enumValues } from "../enum-meta.js";
52
54
  import { hasWritableRdbSource } from "../source-detect.js";
53
55
  import { GENERATED_HEADER } from "../constants.js";
54
56
  import { renderEntityNeighborhoodErBlock } from "../templates/mermaid-er.js";
57
+ // Shape C reuses the SAME walk the stub generator and the requirements index use, so
58
+ // all three agree about what the ledger contains by construction.
59
+ import { walkRequirements } from "../requirement-walk.js";
55
60
  import type {
56
61
  EntityDocData,
57
62
  StorageFieldDoc,
@@ -684,6 +689,34 @@ export function buildEntityDocData(
684
689
  }
685
690
  const usedBy = usedByMatches.length > 0 ? usedByMatches : undefined;
686
691
 
692
+ // ---- ClaimedBy (shape C) — the requirements whose `@implementedBy` resolves HERE.
693
+ //
694
+ // Matched on the RESOLVED NODE, never on the reference string: `@implementedBy` may
695
+ // be authored fully-qualified or bare, and a bare ref binds package-locally under
696
+ // ADR-0042. `walkRequirements` already did that resolution (it is what the stub
697
+ // generator uses), so comparing nodes inherits the correct binding rather than
698
+ // re-deriving it here and drifting from it.
699
+ //
700
+ // ENTITY-GRAIN. Object coverage is entity-grain, so a claimed value/projection gets
701
+ // nothing — surfacing one would imply a coverage rule the ledger does not have.
702
+ const claimedByMatches: UsedByDoc[] = [];
703
+ if (entity.subType === OBJECT_SUBTYPE_ENTITY) {
704
+ for (const walked of walkRequirements(root)) {
705
+ if (!walked.targets.some((t) => t.node === entity)) continue;
706
+ const v = walked.view;
707
+ const level = v.level === undefined ? "" : ` · **L${v.level}**`;
708
+ const status = v.status === undefined ? "" : ` · status: \`${v.status}\``;
709
+ const statement = walked.node.attr(REQUIREMENT_ATTR_STATEMENT);
710
+ const said = typeof statement === "string" && statement.length > 0 ? ` — ${statement}` : "";
711
+ claimedByMatches.push({
712
+ bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${said}`,
713
+ });
714
+ }
715
+ }
716
+ // ABSENT, not empty: the Mustache gate must not render for an unclaimed entity, so
717
+ // its page stays byte-identical to its pre-feature output.
718
+ const claimedBy = claimedByMatches.length > 0 ? claimedByMatches : undefined;
719
+
687
720
  // Preamble header — built up exactly as the legacy emitter did.
688
721
  const preambleLines: string[] = [];
689
722
  const typeStr = `${entity.type}.${entity.subType}`;
@@ -761,6 +794,10 @@ export function buildEntityDocData(
761
794
  data.usedBy = usedBy;
762
795
  data.hasUsedBy = true;
763
796
  }
797
+ if (claimedBy !== undefined) {
798
+ data.claimedBy = claimedBy;
799
+ data.hasClaimedBy = true;
800
+ }
764
801
  // Cross-link to the api surfaces — present ONLY when the caller computed the
765
802
  // hrefs (api surfaces emitted alongside model); model-only runs stay identical.
766
803
  // `last` flags the final ref so the template renders an inline ` · ` separator.
@@ -256,6 +256,21 @@ export interface EntityDocData {
256
256
  /** Present-and-non-empty flag for the usedBy section. */
257
257
  hasUsedBy?: boolean;
258
258
 
259
+ /** "Required by" (shape C) — the `requirement.*` nodes whose `@implementedBy`
260
+ * resolves to THIS entity. Same list+flag pattern as `usedBy`.
261
+ *
262
+ * ABSENT — not empty, not false — when nothing claims the entity, so the
263
+ * Mustache section does not render and an unclaimed entity's page stays
264
+ * BYTE-IDENTICAL to its pre-feature output. That is the no-churn contract:
265
+ * a project with no ledger, or an entity nothing claims, must see no diff.
266
+ *
267
+ * ENTITY-GRAIN ONLY. Object coverage is entity-grain (`spec/capability-ledger.md`),
268
+ * so a claimed `object.value` / `object.projection` gets nothing here — surfacing
269
+ * one would imply a coverage rule the ledger does not actually have. */
270
+ claimedBy?: UsedByDoc[];
271
+ /** Present-and-non-empty flag for the claimedBy section. */
272
+ hasClaimedBy?: boolean;
273
+
259
274
  /** Present flag for the storage section. */
260
275
  hasStorage?: boolean;
261
276
 
@@ -19,7 +19,7 @@
19
19
  // breaks, the refactor is the bug, not the fixture.
20
20
 
21
21
  import type { MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
22
- import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_OUTPUT } from "@metaobjectsdev/metadata";
22
+ import { TYPE_TEMPLATE } from "@metaobjectsdev/metadata";
23
23
  import { render } from "@metaobjectsdev/render";
24
24
  import type { Provider } from "@metaobjectsdev/render";
25
25
  import type { Generator, GeneratorFactory, EmittedFile } from "../generator.js";
@@ -132,12 +132,21 @@ export const docsFile = function docsFile(opts?: DocsFileOpts): Generator {
132
132
  return { path, content: renderDocPage(TEMPLATE_REF, payload, provider, path) };
133
133
  });
134
134
 
135
- // ALSO emit one NEUTRAL render-contract page per `template.output` node
136
- // a sibling artifact, distinct from the entity page. Raw node name → file
135
+ // ALSO emit one NEUTRAL render-contract page per top-level `template.*` node
136
+ // a sibling artifact, distinct from the entity page. Raw node name → file
137
137
  // (`<name>.md`), agreeing with the entity Used-by back-link target.
138
+ //
139
+ // EVERY template subtype, not just `template.output`: the api-docs surface
140
+ // has always emitted `api/<lang>/<pkg>/<Prompt>.md` for a top-level
141
+ // `template.prompt` (it documents the prompt-render handle), and that page
142
+ // carries a "Model / metadata" back-link to `<pkg>/<Prompt>.md`. While this
143
+ // loop filtered to outputs, that link pointed at a page nothing wrote — a
144
+ // broken link in every shipped doc tree containing a prompt. ADR-0052 makes
145
+ // it worse rather than better: a responding prompt now carries the whole
146
+ // inbound tier, so it is the LAST node a doc tree should omit.
138
147
  // ADR-0039: resolving — root has no super (children()==ownChildren()).
139
148
  for (const child of ctx.loadedRoot.children()) {
140
- if (child.type !== TYPE_TEMPLATE || child.subType !== TEMPLATE_SUBTYPE_OUTPUT) continue;
149
+ if (child.type !== TYPE_TEMPLATE) continue;
141
150
  const node = docPageNode(child);
142
151
  templateNodes.push(node);
143
152
  const path = docPageOutputPath(layout, node);
@@ -4,7 +4,7 @@
4
4
  // node whose @format is json/xml. Wraps renderExtractor() from templates/extractor.ts.
5
5
  //
6
6
  // The emitted extractor sits over the output-parser's nested-capable extract and turns dirty LLM
7
- // text into the strict typed payload graph. It imports from the sibling <Name>.output.ts (the
7
+ // text into the strict typed payload graph. It imports from the sibling <Name>.response.ts (the
8
8
  // output-parser) and from each payload value-object's own entity module (<VO>.ts, emitted by
9
9
  // entityFile), so run it alongside outputParser() + entityFile().
10
10
  //
@@ -14,13 +14,13 @@
14
14
  // Custom output directory:
15
15
  // generators: [..., extractor({ outDir: "src/generated/outputs" })]
16
16
 
17
- import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_OUTPUT, TEMPLATE_ATTR_FORMAT } from "@metaobjectsdev/metadata";
18
17
  import {
19
18
  type EmittedFile,
20
19
  type Generator,
21
20
  type GeneratorFactory,
22
21
  oncePerRun,
23
22
  } from "../generator.js";
23
+ import { inboundTemplates, responseShape } from "../templates/find-inbound.js";
24
24
  import { renderExtractor } from "../templates/extractor.js";
25
25
 
26
26
  export interface ExtractorOpts {
@@ -35,16 +35,12 @@ export const extractor = function extractor(opts?: ExtractorOpts): Generator {
35
35
  const generator: Generator = {
36
36
  name: "extractor",
37
37
  generate: oncePerRun((_entities, ctx) => {
38
- // ADR-0039: resolving — root has no super (children()==ownChildren()).
39
- const outputs = ctx.loadedRoot
40
- .children()
41
- .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
42
38
  const files: EmittedFile[] = [];
43
- for (const t of outputs) {
44
- // The extract tier requires the extract API, which only json/xml output-parsers emit.
45
- // ADR-0039: resolving a template may inherit @format via extends.
46
- const format = ((t.attr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
47
- if (format !== "json" && format !== "xml") continue;
39
+ for (const t of inboundTemplates(ctx.loadedRoot)) {
40
+ // The extract tier sits over the parser's extract API. Since ADR-0052 every
41
+ // responding prompt emits one (@responseFormat is a closed json|xml set), so
42
+ // the only remaining skip is an unresolvable @responseRef.
43
+ if (!responseShape(ctx.loadedRoot, t)) continue;
48
44
  files.push({
49
45
  path: `${dirPrefix}${t.name}.extractor.ts`,
50
46
  // ADR-0044/#228: thread ctx.renderContext (when present — runGen always supplies it;
@@ -44,3 +44,14 @@ export type {
44
44
  export { buildEntityDocData } from "./docs-data-builder.js";
45
45
  export type { TemplateDocData, TemplateOutputPart } from "./template-doc-data.js";
46
46
  export { buildTemplateDocData } from "./template-doc-builder.js";
47
+
48
+ // FR-038 — requirement-derived test stubs. The factory is a convenience; the
49
+ // primitives beside it are the real escape hatch, so an app needing different
50
+ // behaviour composes its own generator instead of filing an issue here.
51
+ export { requirementTests } from "./requirement-tests.js";
52
+ export { requirementsFile } from "./requirements-file.js";
53
+ export type { RequirementRow } from "./requirements-view.js";
54
+ export type {
55
+ RequirementTestsOpts,
56
+ RequirementTestRenderer,
57
+ } from "./requirement-tests.js";
@@ -1,7 +1,13 @@
1
1
  // server/typescript/packages/codegen-ts/src/generators/output-parser-file.ts
2
2
  //
3
- // Stock generator that emits one <TemplateName>.output.ts file per declared
4
- // template.output node. Wraps renderOutputParser() from templates/output-parser.ts.
3
+ // Stock generator that emits one <PromptName>.response.ts file per declared
4
+ // `template.prompt` carrying `@responseRef`. Wraps renderOutputParser() from
5
+ // templates/output-parser.ts.
6
+ //
7
+ // ADR-0052: this tier is INBOUND — it reads a model's reply. It used to key on
8
+ // `template.output` with no `@kind` filter at all, so an email template generated
9
+ // a parser for text the system had just rendered. `template.output` is outbound
10
+ // only and emits nothing here.
5
11
  //
6
12
  // Consumer wiring (metaobjects.config.ts):
7
13
  // generators: [..., promptRender(), outputParser()]
@@ -9,8 +15,7 @@
9
15
  // Custom output directory:
10
16
  // generators: [..., outputParser({ outDir: "src/generated/outputs" })]
11
17
 
12
- import { TEMPLATE_SUBTYPE_OUTPUT } from "@metaobjectsdev/metadata";
13
- import { findTemplates } from "../templates/find-templates.js";
18
+ import { inboundTemplates } from "../templates/find-inbound.js";
14
19
  import {
15
20
  type EmittedFile,
16
21
  type Generator,
@@ -31,11 +36,11 @@ export const outputParser = function outputParser(opts?: OutputParserOpts): Gene
31
36
  const generator: Generator = {
32
37
  name: "output-parser",
33
38
  generate: oncePerRun((_entities, ctx) => {
34
- const outputs = findTemplates(ctx.loadedRoot, TEMPLATE_SUBTYPE_OUTPUT);
39
+ const responders = inboundTemplates(ctx.loadedRoot);
35
40
  const files: EmittedFile[] = [];
36
- for (const t of outputs) {
41
+ for (const t of responders) {
37
42
  files.push({
38
- path: `${dirPrefix}${t.name}.output.ts`,
43
+ path: `${dirPrefix}${t.name}.response.ts`,
39
44
  // ADR-0044/#228: thread ctx.renderContext (when present — runGen always supplies it;
40
45
  // a hand-rolled GenContext in a unit test may omit it, falling back to bare naming) so
41
46
  // a payload/nested value-object whose bare name collides across packages emits the
@@ -1,10 +1,13 @@
1
1
  // server/typescript/packages/codegen-ts/src/generators/output-prompt-file.ts
2
2
  //
3
- // FR-010 stock generator that emits one <TemplateName>.prompt.ts file per json/xml
4
- // template.output node — the output-format prompt fragment ("produce your answer
5
- // like this"). Wraps renderOutputPrompt() from templates/output-prompt.ts. Skips
6
- // text-format outputs and outputs whose @payloadRef doesn't resolve to a value-object
7
- // (same contract as the output-parser generator).
3
+ // FR-010 stock generator that emits one <PromptName>.responseFormat.ts file per
4
+ // responding `template.prompt` — the response-format fragment ("produce your answer
5
+ // like this"). Wraps renderOutputPrompt() from templates/output-prompt.ts.
6
+ //
7
+ // ADR-0052: gated on `@responseRef` presence, like every other inbound generator.
8
+ // It previously keyed on `template.output` + a json/xml `@format` gate, which is
9
+ // how a fragment that instructs an LLM came to be emitted from the subtype defined
10
+ // as "every rendered artifact other than an LLM prompt".
8
11
  //
9
12
  // Consumer wiring (metaobjects.config.ts):
10
13
  // generators: [..., promptRender(), outputParser(), outputPrompt()]
@@ -12,19 +15,14 @@
12
15
  // Custom output directory:
13
16
  // generators: [..., outputPrompt({ outDir: "src/generated/outputs" })]
14
17
 
15
- import {
16
- TYPE_TEMPLATE,
17
- TEMPLATE_SUBTYPE_OUTPUT,
18
- TEMPLATE_ATTR_PAYLOAD_REF,
19
- resolveObjectRef,
20
- } from "@metaobjectsdev/metadata";
21
18
  import {
22
19
  type EmittedFile,
23
20
  type Generator,
24
21
  type GeneratorFactory,
25
22
  oncePerRun,
26
23
  } from "../generator.js";
27
- import { renderOutputPrompt, templateSupportsPrompt } from "../templates/output-prompt.js";
24
+ import { inboundTemplates, responseShape } from "../templates/find-inbound.js";
25
+ import { renderOutputPrompt } from "../templates/output-prompt.js";
28
26
 
29
27
  export interface OutputPromptOpts {
30
28
  /** Output directory prefix relative to the target's outDir. Default: "" (root). */
@@ -39,23 +37,12 @@ export const outputPrompt = function outputPrompt(opts?: OutputPromptOpts): Gene
39
37
  name: "output-prompt",
40
38
  generate: oncePerRun((_entities, ctx) => {
41
39
  const root = ctx.loadedRoot;
42
- // ADR-0039: resolving — root has no super (children()==ownChildren()).
43
- const outputs = root
44
- .children()
45
- .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
46
40
  const files: EmittedFile[] = [];
47
- for (const t of outputs) {
48
- // Only json/xml outputs get a renderable prompt fragment.
49
- if (!templateSupportsPrompt(t)) continue;
50
- // @payloadRef must resolve to a value-object (same contract as the parser).
51
- // ADR-0039: resolving — a template may inherit its @* refs/format/kind via extends.
52
- const payloadRef = t.attr(TEMPLATE_ATTR_PAYLOAD_REF);
53
- if (typeof payloadRef !== "string") continue;
54
- // ADR-0042: a bare @payloadRef resolves in the template's package.
55
- const vo = resolveObjectRef(root, payloadRef, t.package ?? t.fileDefaultPackage ?? "").node;
56
- if (!vo) continue;
41
+ for (const t of inboundTemplates(root)) {
42
+ // @responseRef must resolve to a value-object (same contract as the parser).
43
+ if (!responseShape(root, t)) continue;
57
44
  files.push({
58
- path: `${dirPrefix}${t.name}.prompt.ts`,
45
+ path: `${dirPrefix}${t.name}.responseFormat.ts`,
59
46
  content: renderOutputPrompt(root, t.name),
60
47
  });
61
48
  }