@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
@@ -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
  }
@@ -0,0 +1,203 @@
1
+ // FR-038 §5/§6 — generate test stubs from `requirement.*` nodes.
2
+ //
3
+ // THE DIVISION OF LABOUR: this package owns the MECHANISM; the application owns
4
+ // the POLICY. Which requirements get tests, at which levels, in what style, or
5
+ // none at all, is the app's decision — the library ships defaults and
6
+ // recommendations, never rules.
7
+ //
8
+ // The filter IS the policy declaration, which is why there is no opt-out
9
+ // vocabulary: a requirement matched by no generator expects no stub by
10
+ // construction, so `verify --codegen` has nothing to drift against. An `@noTest`
11
+ // attribute would have to clear ADR-0023's can't-be-computed bar, and it cannot —
12
+ // the generator config already says it.
13
+ //
14
+ // Every default below has an override seam. The rule: if an application can hit a
15
+ // decision and cannot change it, that decision becomes a bug report against this
16
+ // package.
17
+
18
+ import {
19
+ REQUIREMENT_SUBTYPE_FUNCTIONAL,
20
+ REQUIREMENT_LINK_FLOOR_LEVEL,
21
+ REQUIREMENT_ATTR_STATEMENT,
22
+ REQUIREMENT_ATTR_COUNTEREXAMPLE,
23
+ } from "@metaobjectsdev/metadata";
24
+ import type { Generator, EmittedFile, GenContext } from "../generator.js";
25
+ import {
26
+ walkRequirements,
27
+ groupByConcern,
28
+ NO_CONCERN,
29
+ type RequirementView,
30
+ } from "../requirement-walk.js";
31
+ import {
32
+ renderRequirementTest,
33
+ type RequirementTestArgs,
34
+ } from "../templates/requirement-test.js";
35
+
36
+ export type RequirementTestRenderer = (a: RequirementTestArgs) => string;
37
+
38
+ export interface RequirementTestsOpts {
39
+ /** Generator name — surfaces in diagnostics and drift logs. */
40
+ name?: string;
41
+ /** WHICH requirements get stubs. This is the app's policy declaration. */
42
+ filter?: (r: RequirementView) => boolean;
43
+ /** Renderer per concern key: exact `type.subType`, `type.*`, or `*`. */
44
+ renderers?: Record<string, RequirementTestRenderer>;
45
+ /** Full control over renderer selection — beats `renderers` when it returns one. */
46
+ resolveRenderer?: (concern: string) => RequirementTestRenderer | undefined;
47
+ /** Where each stub lands. */
48
+ path?: (view: RequirementView, concern: string) => string;
49
+ /** Named output target (registry key). */
50
+ target?: string;
51
+ /** Name requirements no filter covered. Default true; never fails the build. */
52
+ warnUncovered?: boolean;
53
+ /**
54
+ * FR-038 §8 — which emitted paths this generator is the sole producer of, so
55
+ * the runner may remove a stub whose requirement was deleted. Given a path
56
+ * relative to this generator's output directory, `/`-separated.
57
+ *
58
+ * Defaults to the namespace `defaultPath` writes into. Supply this whenever you
59
+ * supply `path`: the two describe the same namespace from opposite directions
60
+ * and only you can keep them in agreement.
61
+ */
62
+ owns?: (relPathInTarget: string) => boolean;
63
+ /**
64
+ * Delete a hand-edited orphan rather than refusing it. Default false.
65
+ *
66
+ * A refusal already names the file and tells you the two ways out; this makes
67
+ * the destructive one automatic. It exists so the decision is yours, not
68
+ * because it is a good default.
69
+ */
70
+ forceOrphanDelete?: boolean;
71
+ /** Turn orphan reconciliation off entirely. Default true — a stub whose
72
+ * requirement is gone is drift, and leaving it is how a deleted claim keeps a
73
+ * green test. */
74
+ reconcileOrphans?: boolean;
75
+ }
76
+
77
+ /** How many uncovered requirements to name before "…and N more". Mirrors the
78
+ * runner's MAX_NAMED — the same cutoff, so every wall-avoiding message in the
79
+ * feature reads the same shape. */
80
+ const MAX_NAMED_UNCOVERED = 5;
81
+
82
+ /** The directory `defaultPath` writes into — and therefore the namespace the
83
+ * default policy claims. Kept beside it so the pair cannot drift. */
84
+ const DEFAULT_STUB_DIR = "requirements/";
85
+
86
+ /**
87
+ * RECOMMENDATION, not a rule: functional requirements at or below the link floor.
88
+ *
89
+ * Architectural requirements are excluded by default because `verify`'s
90
+ * universality check already proves them structurally, so a test there is usually
91
+ * redundant — usually, not never, which is exactly why this is overridable.
92
+ */
93
+ const defaultFilter = (r: RequirementView): boolean =>
94
+ r.subType === REQUIREMENT_SUBTYPE_FUNCTIONAL &&
95
+ (r.level ?? 0) >= REQUIREMENT_LINK_FLOOR_LEVEL;
96
+
97
+ const defaultPath = (view: RequirementView, concern: string): string =>
98
+ concern === NO_CONCERN
99
+ ? `${DEFAULT_STUB_DIR}${view.path}.test.ts`
100
+ : `${DEFAULT_STUB_DIR}${view.path}.${concern}.test.ts`;
101
+
102
+ /** Exact concern → `type.*` → `*` → the built-in renderer. */
103
+ function pickRenderer(
104
+ concern: string,
105
+ opts: RequirementTestsOpts,
106
+ ): RequirementTestRenderer {
107
+ const viaFn = opts.resolveRenderer?.(concern);
108
+ if (viaFn !== undefined) return viaFn;
109
+ const map = opts.renderers ?? {};
110
+ const typeOnly = `${concern.split(".")[0] ?? ""}.*`;
111
+ return map[concern] ?? map[typeOnly] ?? map[NO_CONCERN] ?? renderRequirementTest;
112
+ }
113
+
114
+ function attrString(node: { attr: (n: string) => unknown }, name: string): string {
115
+ const v = node.attr(name);
116
+ return typeof v === "string" ? v : "";
117
+ }
118
+
119
+ export function requirementTests(opts: RequirementTestsOpts = {}): Generator {
120
+ const filter = opts.filter ?? defaultFilter;
121
+ const toPath = opts.path ?? defaultPath;
122
+ // A custom `path` with no custom `owns` leaves the default namespace pointing
123
+ // somewhere the generator no longer writes, so reconciliation matches nothing.
124
+ // That degrades safely — it can only ever delete less — but silently, and a
125
+ // policy nobody can tell isn't running is the failure mode §5 is about.
126
+ const namespaceUnknown = opts.path !== undefined && opts.owns === undefined;
127
+
128
+ const generator: Generator = {
129
+ name: opts.name ?? "requirement-tests",
130
+ generate: (ctx: GenContext): EmittedFile[] => {
131
+ const files: EmittedFile[] = [];
132
+ const uncovered: string[] = [];
133
+
134
+ if (namespaceUnknown && opts.reconcileOrphans !== false) {
135
+ ctx.warn(
136
+ `a custom 'path' was supplied without a matching 'owns', so a stub left ` +
137
+ `behind by a deleted requirement will NOT be cleaned up. Supply 'owns' ` +
138
+ `to describe where 'path' writes, or set reconcileOrphans: false.`,
139
+ );
140
+ }
141
+
142
+ for (const walked of walkRequirements(ctx.loadedRoot)) {
143
+ if (!filter(walked.view)) {
144
+ uncovered.push(walked.view.path);
145
+ continue;
146
+ }
147
+ for (const [concern, targets] of groupByConcern(walked)) {
148
+ const args: RequirementTestArgs = {
149
+ view: walked.view,
150
+ concern,
151
+ targets,
152
+ statement: attrString(walked.node, REQUIREMENT_ATTR_STATEMENT),
153
+ counterexample: attrString(walked.node, REQUIREMENT_ATTR_COUNTEREXAMPLE),
154
+ disposition: walked.node.disposition(),
155
+ trackedBy: walked.node.trackedBy(),
156
+ };
157
+ files.push({
158
+ path: toPath(walked.view, concern),
159
+ content: pickRenderer(concern, opts)(args),
160
+ });
161
+ }
162
+ }
163
+
164
+ // Policy living only in config means an uncovered requirement is
165
+ // indistinguishable from a deliberate exclusion. One warning, never failing,
166
+ // so "no tests here" is a visible choice rather than silence.
167
+ if ((opts.warnUncovered ?? true) && uncovered.length > 0) {
168
+ // CAPPED, matching the runner's refusal message. The default filter excludes
169
+ // every architectural node and every L1-L3 functional one, so on the ledger
170
+ // shapes this repo's own docs describe — dozens to hundreds of entries — an
171
+ // uncapped list is a wall of dotted paths with the one actionable sentence
172
+ // buried at the end of it. This is a generator meant to be the feature's first
173
+ // contact; burying the opt-out is how it gets switched off wholesale.
174
+ const shown = uncovered.slice(0, MAX_NAMED_UNCOVERED).join(", ");
175
+ const more =
176
+ uncovered.length > MAX_NAMED_UNCOVERED
177
+ ? `, and ${uncovered.length - MAX_NAMED_UNCOVERED} more`
178
+ : "";
179
+ ctx.warn(
180
+ `${uncovered.length} requirement(s) matched no filter and get no stub. ` +
181
+ `If that is deliberate, set warnUncovered: false to silence this. ` +
182
+ `Uncovered: ${shown}${more}.`,
183
+ );
184
+ }
185
+
186
+ return files;
187
+ },
188
+ };
189
+
190
+ if (opts.target !== undefined) generator.target = opts.target;
191
+ if (opts.reconcileOrphans !== false) {
192
+ generator.orphanPolicy = {
193
+ // With a custom `path` and no `owns`, claim NOTHING rather than guess: the
194
+ // default namespace would be a claim over a directory this generator does
195
+ // not write to, and a wrong claim deletes another generator's files.
196
+ owns: opts.owns ?? (namespaceUnknown
197
+ ? () => false
198
+ : (relPath) => relPath.startsWith(DEFAULT_STUB_DIR)),
199
+ ...(opts.forceOrphanDelete === true && { force: true }),
200
+ };
201
+ }
202
+ return generator;
203
+ }
@@ -0,0 +1,71 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/requirements-file.ts
2
+ //
3
+ // The `requirements` docs surface generator — emits the ledger as documentation.
4
+ //
5
+ // Design: docs/superpowers/specs/2026-08-21-requirements-doc-surface-design.md
6
+ //
7
+ // METADATA ALONE. `meta docs` guarantees output "from metadata ALONE — no gen config, no
8
+ // codegen pipeline" (cli/src/commands/docs.ts). This generator reads `ctx.loadedRoot` and
9
+ // NOTHING else — no filesystem walk, no config read, no test-source scan. That sentence
10
+ // is what rules out reviving the retired `@verifiedBy` scan here, and it only keeps being
11
+ // true if nobody adds a second input.
12
+ //
13
+ // AN EMPTY LEDGER EMITS ZERO FILES, not an empty page. That is a contract with the
14
+ // surface wiring, not a cosmetic choice: `requirements` defaults to ON, and this is the
15
+ // only reason that is a no-op for every project without a ledger.
16
+ //
17
+ // BOTH FILES, UNCONDITIONALLY — deliberately not behind a `--format` flag. `meta docs`
18
+ // writes FILES, while `--format` on gen/migrate selects a STDOUT encoding; reusing the
19
+ // name would make it mean something materially different on this command. A drift gate
20
+ // also wants the machine-readable artifact committed regardless of who ran it or from
21
+ // what kind of terminal.
22
+
23
+ import {
24
+ type EmittedFile,
25
+ type Generator,
26
+ type GeneratorFactory,
27
+ oncePerRun,
28
+ } from "../generator.js";
29
+ import { renderRequirementsMarkdown } from "./requirements-markdown.js";
30
+ import { renderRequirementsToon } from "./requirements-toon.js";
31
+ import { requirementRows } from "./requirements-view.js";
32
+
33
+ /** The human-facing index. */
34
+ const MARKDOWN_FILENAME = "requirements.md";
35
+ /** The machine-facing artifact — see requirements-toon.ts for why TOON. */
36
+ const TOON_FILENAME = "requirements.toon";
37
+
38
+ export interface RequirementsFileOpts {
39
+ /** Output directory prefix relative to the target's outDir. Default: "" (root). */
40
+ outDir?: string;
41
+ /** Optional named output target (registry key). Defaults to "default". */
42
+ target?: string;
43
+ }
44
+
45
+ export const requirementsFile = function requirementsFile(
46
+ opts?: RequirementsFileOpts,
47
+ ): Generator {
48
+ const dirPrefix = opts?.outDir ? `${opts.outDir.replace(/\/$/, "")}/` : "";
49
+
50
+ const generator: Generator = {
51
+ name: "requirements-file",
52
+ generate: oncePerRun((_entities, ctx) => {
53
+ const rows = requirementRows(ctx.loadedRoot);
54
+ // The one early return that matters. Both renderers also return "" for an empty
55
+ // ledger, so this is belt-and-braces — but the guarantee callers depend on is
56
+ // "zero FILES", which only this line can provide.
57
+ if (rows.length === 0) return [];
58
+
59
+ const files: EmittedFile[] = [
60
+ { path: `${dirPrefix}${MARKDOWN_FILENAME}`, content: renderRequirementsMarkdown(rows) },
61
+ { path: `${dirPrefix}${TOON_FILENAME}`, content: renderRequirementsToon(rows) },
62
+ ];
63
+ return files;
64
+ }),
65
+ };
66
+
67
+ if (opts?.target) {
68
+ generator.target = opts.target;
69
+ }
70
+ return generator;
71
+ } as GeneratorFactory<RequirementsFileOpts>;
@@ -0,0 +1,72 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/requirements-markdown.ts
2
+ //
3
+ // The human-facing half of the `requirements` docs surface — shape A, one index page.
4
+ //
5
+ // Design: docs/superpowers/specs/2026-08-21-requirements-doc-surface-design.md §4, §5
6
+ //
7
+ // ONE PAGE, NOT ONE PER NODE. Shape B (a page per top-level requirement) was measured
8
+ // and dominated: 19 files and ~30.5K tokens against ~11.9K for the index, with one page
9
+ // at 86KB. An 86KB page is not a documentation page.
10
+ //
11
+ // NO TEST LINK IS RENDERED, under any input (§3). With `@verifiedBy` retired there is no
12
+ // join key, so there is nothing honest to print — and a derived path would read as MORE
13
+ // authoritative than an author-chosen string, not less. Silence here is consistent with
14
+ // five shipped statements that a requirement carrying no test link is a legitimate
15
+ // declared state; it is not a gap waiting to be filled by whoever notices it next.
16
+ //
17
+ // PROSE LIVES HERE, STRUCTURE LIVES IN THE TOON SIBLING. TOON quotes every comma-bearing
18
+ // string, so statements and violations dilute it badly. The split is not stylistic.
19
+
20
+ import type { RequirementRow } from "./requirements-view.js";
21
+
22
+ /** `##` for a root-level requirement, deepening with nesting. `#` is the page title. */
23
+ function heading(depth: number): string {
24
+ return "#".repeat(Math.min(depth + 2, 6));
25
+ }
26
+
27
+ /** The one-line fact bar: the attrs that are scannable rather than readable. */
28
+ function facts(r: RequirementRow): string {
29
+ const parts: string[] = [`\`${r.subType}\``];
30
+ if (r.level !== undefined) parts.push(`**L${r.level}**`);
31
+ if (r.status !== undefined) parts.push(`status: \`${r.status}\``);
32
+ // Absent `@disposition` means UNDECIDED, which is a real state and not the same as
33
+ // "no gap" — so it is omitted rather than rendered as a default.
34
+ if (r.disposition !== undefined) parts.push(`disposition: \`${r.disposition}\``);
35
+ if (r.trackedBy.length > 0) parts.push(`tracked: ${r.trackedBy.join(", ")}`);
36
+ return parts.join(" · ");
37
+ }
38
+
39
+ function renderOne(r: RequirementRow): string {
40
+ const out: string[] = [`${heading(r.depth)} ${r.path}`, "", facts(r), ""];
41
+
42
+ if (r.statement !== undefined) out.push(r.statement, "");
43
+ // The prescriptive pair. A requirement MUST be violable, so the counterexample is not
44
+ // decoration — it is what makes the statement checkable, and it reads beside it.
45
+ if (r.counterexample !== undefined) out.push(`**Counterexample:** ${r.counterexample}`, "");
46
+ if (r.description !== undefined) out.push(r.description, "");
47
+
48
+ if (r.implementedBy.length > 0) {
49
+ out.push(`**Implemented by:** ${r.implementedBy.map((c) => `\`${c}\``).join(", ")}`, "");
50
+ }
51
+
52
+ return out.join("\n");
53
+ }
54
+
55
+ /**
56
+ * Render the ledger as one markdown index.
57
+ *
58
+ * Returns the EMPTY STRING for an empty ledger rather than a headed-but-empty page.
59
+ * The generator keys on that to emit no file at all, which is in turn what lets the
60
+ * surface default to on without changing output for any project lacking a ledger.
61
+ */
62
+ export function renderRequirementsMarkdown(rows: readonly RequirementRow[]): string {
63
+ if (rows.length === 0) return "";
64
+
65
+ return [
66
+ "# Requirements",
67
+ "",
68
+ `${rows.length} declared requirement${rows.length === 1 ? "" : "s"}, in declaration order.`,
69
+ "",
70
+ ...rows.map(renderOne),
71
+ ].join("\n");
72
+ }
@@ -0,0 +1,64 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/requirements-toon.ts
2
+ //
3
+ // The machine-facing half of the `requirements` docs surface.
4
+ //
5
+ // Design: docs/superpowers/specs/2026-08-21-requirements-doc-surface-design.md §5
6
+ //
7
+ // CHOSEN FOR THE DECLARED-COUNT HEADER, NOT THE TOKEN SAVING. Measured with this same
8
+ // encoder on 321 rows, TOON is 42.1% smaller than JSON but only 7.3% smaller than a
9
+ // markdown table — a table is already header-once, so compression is close to a
10
+ // non-argument. What it buys is the header:
11
+ //
12
+ // requirements[321]{path,level,status,claims,statement}:
13
+ //
14
+ // A reader can verify it received all 321 rows. A documentation generator that silently
15
+ // drops rows produces a document that looks complete and is not, and this is the one
16
+ // cheap defence against that.
17
+ //
18
+ // PROSE IS DELIBERATELY THIN HERE. TOON quotes every comma-bearing string, so statements
19
+ // dilute the format badly. The readable half lives in the markdown sibling; this file
20
+ // carries structure. That split is the reason both are emitted rather than one.
21
+ //
22
+ // `notes` NEVER APPEARS — RequirementRow does not carry it (documentation.json charters
23
+ // it internal-only). Nothing to strip, by construction.
24
+
25
+ import { encode } from "@toon-format/toon";
26
+ import type { RequirementRow } from "./requirements-view.js";
27
+
28
+ /** The row shape the artifact declares. Field ORDER is the wire contract — a reader
29
+ * diffing two runs sees a reordering as noise, so it is fixed here rather than left
30
+ * to object-literal order elsewhere. */
31
+ interface ToonRow {
32
+ readonly path: string;
33
+ readonly subType: string;
34
+ readonly level: number | string;
35
+ readonly status: string;
36
+ readonly disposition: string;
37
+ readonly claims: string;
38
+ readonly statement: string;
39
+ }
40
+
41
+ /** TOON is tabular: a field absent on one row and present on another breaks the uniform
42
+ * header. So every optional value collapses to "" rather than being omitted — an empty
43
+ * cell reads as absent without costing the header its shape. */
44
+ function cell(v: string | undefined): string {
45
+ return v ?? "";
46
+ }
47
+
48
+ export function renderRequirementsToon(rows: readonly RequirementRow[]): string {
49
+ if (rows.length === 0) return "";
50
+
51
+ const table: ToonRow[] = rows.map((r) => ({
52
+ path: r.path,
53
+ subType: r.subType,
54
+ level: r.level ?? "",
55
+ status: cell(r.status),
56
+ disposition: cell(r.disposition),
57
+ // Joined rather than nested: a nested array per row would cost the tabular header,
58
+ // which is the entire reason this format was picked.
59
+ claims: r.implementedBy.join(" "),
60
+ statement: cell(r.statement),
61
+ }));
62
+
63
+ return encode({ requirements: table });
64
+ }
@@ -0,0 +1,93 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/requirements-view.ts
2
+ //
3
+ // The projected row the `requirements` docs surface renders from.
4
+ //
5
+ // Design: docs/superpowers/specs/2026-08-21-requirements-doc-surface-design.md
6
+ //
7
+ // PROJECTS OVER `walkRequirements()`, NEVER RE-WALKS. That walk already resolves the
8
+ // depth-first traversal, the dotted `path` (hierarchy IS nesting), and the
9
+ // `@implementedBy` targets, and it is what `requirementTests()` is built on. Sharing it
10
+ // means the doc view and the generated stubs agree about what the ledger contains by
11
+ // construction; two walks kept in step by hand is exactly the drift this surface exists
12
+ // to remove.
13
+ //
14
+ // `notes` IS DELIBERATELY ABSENT FROM THIS TYPE, not merely unread. `documentation.json`
15
+ // charters it internal-only, never emitted to a user-facing doc surface, and the
16
+ // original ask this design replaced wanted a "stripper" for precisely this. A renderer
17
+ // cannot leak a field the projection never carries — which is a stronger guarantee than
18
+ // asking every renderer to remember.
19
+ //
20
+ // RESOLVING ACCESSORS ONLY (ADR-0039). Every read below goes through MetaRequirement's
21
+ // resolving accessors or the resolving `attr()`, so a requirement that `extends` an
22
+ // abstract parent inherits its `@level` / `@status` / statement rather than projecting
23
+ // them as undefined. No `own*()` call belongs in this file.
24
+
25
+ import {
26
+ DOC_ATTR_DESCRIPTION,
27
+ REQUIREMENT_ATTR_STATEMENT,
28
+ REQUIREMENT_ATTR_COUNTEREXAMPLE,
29
+ } from "@metaobjectsdev/metadata";
30
+ import type { MetaData } from "@metaobjectsdev/metadata";
31
+ import { walkRequirements } from "../requirement-walk.js";
32
+
33
+ /** One requirement, projected for rendering. Ordered depth-first, declaration order. */
34
+ export interface RequirementRow {
35
+ /** Dotted child-name path from the root — the same address every other node uses. */
36
+ readonly path: string;
37
+ /** 0 for a root-level requirement. Derived from `path`, so it cannot disagree with it. */
38
+ readonly depth: number;
39
+ /** "functional" | "architectural" — the check-polarity axis. */
40
+ readonly subType: string;
41
+ /** Undefined on an unlevelled architectural requirement (the flat policy form). */
42
+ readonly level: number | undefined;
43
+ readonly status: string | undefined;
44
+ readonly disposition: string | undefined;
45
+ readonly trackedBy: readonly string[];
46
+ readonly statement: string | undefined;
47
+ readonly counterexample: string | undefined;
48
+ /** Chartered user-facing. `notes` has no counterpart here, by design. */
49
+ readonly description: string | undefined;
50
+ /** `@implementedBy` exactly as authored — provenance a reader can grep for. */
51
+ readonly implementedBy: readonly string[];
52
+ /** DISTINCT `type.subType` concerns among the targets that actually resolved. */
53
+ readonly claimedConcerns: readonly string[];
54
+ }
55
+
56
+ /** Reads a resolving string attr, normalising "declared but empty" to undefined so a
57
+ * renderer has one absent-case to handle rather than two. */
58
+ function stringAttr(node: MetaData, name: string): string | undefined {
59
+ // `attr()` is the RESOLVING accessor in TypeScript (ADR-0039). Note the cross-port
60
+ // naming inversion this repo records: Python's `attr()` is own-only, TS's resolves.
61
+ const raw = node.attr(name);
62
+ if (typeof raw !== "string") return undefined;
63
+ return raw.length > 0 ? raw : undefined;
64
+ }
65
+
66
+ /**
67
+ * Project every `requirement.*` node — nested ones included — into render-ready rows.
68
+ *
69
+ * Returns `[]` for a model declaring no requirements, which is what lets the surface
70
+ * emit NOTHING rather than an empty page, and is in turn what makes turning the surface
71
+ * on by default a no-op for every project without a ledger.
72
+ */
73
+ export function requirementRows(root: MetaData): RequirementRow[] {
74
+ return walkRequirements(root).map((walked) => {
75
+ const { node, view, targets } = walked;
76
+ return {
77
+ path: view.path,
78
+ // Derived rather than threaded: a separately-tracked depth could drift from the
79
+ // path that renders beside it, and there is only one right answer.
80
+ depth: view.path.split(".").length - 1,
81
+ subType: view.subType,
82
+ level: view.level,
83
+ status: view.status,
84
+ disposition: node.disposition(),
85
+ trackedBy: node.trackedBy(),
86
+ statement: stringAttr(node, REQUIREMENT_ATTR_STATEMENT),
87
+ counterexample: stringAttr(node, REQUIREMENT_ATTR_COUNTEREXAMPLE),
88
+ description: stringAttr(node, DOC_ATTR_DESCRIPTION),
89
+ implementedBy: node.implementedBy(),
90
+ claimedConcerns: [...new Set(targets.map((t) => t.concern))],
91
+ };
92
+ });
93
+ }