@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
@@ -0,0 +1,136 @@
1
+ // FR-038 §8 — deletion integrity: what happens to a generated file that is no
2
+ // longer generated.
3
+ //
4
+ // A requirement's stubs become orphans when the requirement is deleted. The
5
+ // obvious move — remove them, as the FR-038 draft said for `abandoned` entries —
6
+ // is safe only for an UNTOUCHED stub. On a filled one it eats assertions somebody
7
+ // wrote, and the whole point of the inversion is that the body is hand-written and
8
+ // survives regeneration.
9
+ //
10
+ // So: remove what the generator wrote, REFUSE what a human changed, and name it.
11
+ // That mirrors the migrate engine refusing a primary-key move rather than emitting
12
+ // something un-appliable (#258) — a refusal is recoverable, a deletion is not.
13
+ //
14
+ // Deliberately PURE: the caller supplies the readers. The decision is the part
15
+ // worth testing, and threading a filesystem through it would make the rule harder
16
+ // to exercise than the plumbing around it.
17
+
18
+ /**
19
+ * A generator's opt-in to orphan reconciliation, and the namespace it applies to.
20
+ *
21
+ * Declaring this is the ONLY way in: `.gen-state/.hashes.json` records paths, not
22
+ * which generator produced each one, so the runner genuinely cannot work out who
23
+ * owns what. Deriving ownership from the output directory instead would be worse
24
+ * than useless — every generator in a single-target project writes to the same
25
+ * `outDir`, so one generator narrowing its output would delete its siblings'
26
+ * files. The generator has to say.
27
+ */
28
+ export interface OrphanPolicy {
29
+ /**
30
+ * True when `relPathInTarget` — a path relative to this generator's own output
31
+ * directory, always `/`-separated — is inside the namespace this generator is
32
+ * the sole producer of.
33
+ *
34
+ * Be narrow. This predicate is the blast radius: every previously-generated
35
+ * path it accepts and this run did not re-emit is a deletion candidate.
36
+ */
37
+ readonly owns: (relPathInTarget: string) => boolean;
38
+ /**
39
+ * Delete a hand-edited orphan instead of refusing it. Default false.
40
+ *
41
+ * The seam exists because §6 requires every default to have one, not because
42
+ * it is advisable: the refusal already names the file and deleting it by hand
43
+ * is a one-line answer, whereas this flag makes the destructive outcome the
44
+ * automatic one.
45
+ */
46
+ readonly force?: boolean;
47
+ }
48
+
49
+ export interface OrphanDecision {
50
+ /** No longer emitted, on disk, and byte-identical to the snapshot we wrote.
51
+ * Safe to delete — the generator is removing only its own untouched output. */
52
+ readonly remove: readonly string[];
53
+ /** No longer emitted and CHANGED since we wrote it (or with no snapshot to
54
+ * compare against). Left alone and reported, never deleted. */
55
+ readonly refused: readonly string[];
56
+ /** No longer emitted and already absent from disk. Nothing to delete; the
57
+ * caller should just drop the stale snapshot. */
58
+ readonly vanished: readonly string[];
59
+ }
60
+
61
+ export interface ReconcileOrphansArgs {
62
+ /** Relative paths this generator emitted on some previous run — in practice the
63
+ * keys of `.gen-state/.hashes.json`. */
64
+ readonly previouslyGenerated: readonly string[];
65
+ /** Relative paths emitted on THIS run. */
66
+ readonly emitted: readonly string[];
67
+ /** Namespace guard. Orphan reconciliation is opt-in and scoped: a generator must
68
+ * never delete another generator's output just because it stopped emitting its
69
+ * own. */
70
+ readonly owns: (relPath: string) => boolean;
71
+ /** Whether the file is still on disk at all. */
72
+ readonly exists: (relPath: string) => boolean;
73
+ /**
74
+ * Whether the file is byte-for-byte what we recorded writing.
75
+ *
76
+ * MUST fail closed — false when it cannot be proven. This is deliberately the
77
+ * same question the write path asks (`isPristineGenerated`), of the same
78
+ * evidence: the committed hash manifest. Before they were unified, one feature
79
+ * refused to DELETE a hand-edited file while silently OVERWRITING one, which is
80
+ * the same uncertainty answered two opposite ways.
81
+ */
82
+ readonly isUntouched: (relPath: string) => boolean;
83
+ }
84
+
85
+ export function reconcileOrphans(args: ReconcileOrphansArgs): OrphanDecision {
86
+ const emitted = new Set(args.emitted);
87
+ const remove: string[] = [];
88
+ const refused: string[] = [];
89
+ const vanished: string[] = [];
90
+
91
+ for (const relPath of args.previouslyGenerated) {
92
+ if (emitted.has(relPath)) continue;
93
+ if (!args.owns(relPath)) continue;
94
+
95
+ if (!args.exists(relPath)) {
96
+ vanished.push(relPath);
97
+ continue;
98
+ }
99
+
100
+ // Fail closed: unless we can prove the file is untouched, guessing wrong
101
+ // deletes someone's work.
102
+ if (!args.isUntouched(relPath)) {
103
+ refused.push(relPath);
104
+ continue;
105
+ }
106
+
107
+ remove.push(relPath);
108
+ }
109
+
110
+ return { remove, refused, vanished };
111
+ }
112
+
113
+ /**
114
+ * The message shown when a hand-edited orphan is refused.
115
+ *
116
+ * It has to say what happened, why nothing was deleted, and what the two ways out
117
+ * are — otherwise the reasonable reaction to an unexplained refusal is to delete
118
+ * the file, which is the outcome the refusal exists to prevent.
119
+ */
120
+ export function refusedOrphanMessage(
121
+ paths: readonly string[],
122
+ /** Which generator's namespace these came from. `orphanPolicy` is a generic
123
+ * `Generator` field and `sweepOrphans`/`OrphanPolicy` are exported precisely so an
124
+ * app can compose its own — so this message must not hardcode `requirement-tests`,
125
+ * naming a generator the project may not even use and a cause ("the requirement was
126
+ * removed") that may not apply. */
127
+ generatorName = "orphan cleanup",
128
+ ): string {
129
+ return (
130
+ `${generatorName}: ${paths.length} generated file(s) are no longer produced by ` +
131
+ `this generator but have been edited by hand, so they were NOT deleted: ` +
132
+ `${paths.join(", ")}. Either what produced them was removed (delete these files ` +
133
+ `yourself if the edits are no longer wanted) or it was renamed (move the edits ` +
134
+ `into the newly generated file first — regeneration cannot follow a rename).`
135
+ );
136
+ }
@@ -2,6 +2,10 @@
2
2
  // Then import it LOCALLY in metaobjects.config.ts instead of from the package:
3
3
  // import { barrel } from "./codegen/generators/barrel.js";
4
4
  //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`. Use `node:` builtins instead.
5
9
  // use-when: you want a single `index.ts` re-exporting every generated entity module.
6
10
  // emits: <target>/index.ts with one `export * from "./<Entity>"` per entity, alphabetical.
7
11
  // customize: the export form (star vs named), ordering, grouping by package, what to include/exclude.
@@ -2,6 +2,10 @@
2
2
  // Then import it LOCALLY in metaobjects.config.ts:
3
3
  // import { entityFile } from "./codegen/generators/entity.js";
4
4
  //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`. Use `node:` builtins instead.
5
9
  // use-when: ALWAYS — this is the entity-module generator. It owns the shape of each
6
10
  // generated <Entity>.ts (the Drizzle table, Zod schemas, inferred types,
7
11
  // constants, filter allowlists). Start here and adapt the assembly.
@@ -2,6 +2,10 @@
2
2
  // Then import it LOCALLY in metaobjects.config.ts:
3
3
  // import { queriesFile } from "./codegen/generators/queries.js";
4
4
  //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`. Use `node:` builtins instead.
5
9
  // use-when: you want generated typed CRUD finders (find<E>ById, list<E>s, create/update/delete)
6
10
  // over Drizzle. Drop it if you hand-write your data access.
7
11
  // emits: <target>/<Entity>.queries.ts per source-backed object (any source.rdb kind,
@@ -2,6 +2,10 @@
2
2
  // Then import it LOCALLY in metaobjects.config.ts:
3
3
  // import { routesFile } from "./codegen/generators/routes.js";
4
4
  //
5
+ // RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
6
+ // shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
7
+ // reach for `Bun.*` globals here; they are undefined and take the whole run down with
8
+ // `Bun is not defined`. Use `node:` builtins instead.
5
9
  // use-when: you want generated Fastify REST routes per entity. Drop it and hand-write routes
6
10
  // if you need bespoke endpoints — or keep it and add handlers via <Entity>.extra.ts.
7
11
  // emits: <target>/<Entity>.routes.ts — full CRUD for write-through entities, read-only
@@ -9,6 +9,6 @@ export const EMBEDDED_FRAMEWORK_TEMPLATES: Record<string, string> = {
9
9
  "api/agent-api.md": "{{{generatedMarker}}}\n\n# {{title}}\n\nGenerated API reference for {{project}}; call these exactly as written. {{importNote}}\n{{#hasSetup}}\n\n## Setup\n{{#setup}}\n- `{{handle}}` — {{{note}}} `{{{snippetInline}}}`\n{{/setup}}\n{{/hasSetup}}\n{{#units}}\n\n## {{node}}\n{{#groups}}\n\n`{{importHeader}}`\n{{#symbols}}\n- `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}\n{{/symbols}}\n{{/groups}}\n{{#example}}\n\nExample:\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/units}}\n",
10
10
  "api/entity-api.md": "{{{generatedMarker}}}\n\n# {{node}} API\n{{#modelPageHref}}\n\n**Model / metadata:** [{{node}}]({{modelPageHref}})\n{{/modelPageHref}}\n\n> Import paths are relative to your generated-output directory.\n{{#hasSetup}}\n\n## Setup\n\nObtain the runtime handles the calls below need:\n{{#setup}}\n\n- `{{handle}}` — {{{note}}}\n\n```ts\n{{{snippet}}}\n```\n{{/setup}}\n{{/hasSetup}}\n{{#unitExample}}\n\n## Example\n\n```ts\n{{{unitExample}}}\n```\n{{/unitExample}}\n{{#sections}}\n\n## {{heading}}\n{{#symbols}}\n\n### `{{signature}}`\n\n{{usage}}\n\n```ts\n{{importLine}}\n```\n{{#hasFields}}\n\n{{fieldsCaption}}:\n\n| Field | Type | Required | Notes |\n|---|---|---|---|\n{{#fieldRows}}\n| `{{field}}` | `{{{type}}}` | {{required}} | {{notes}} |\n{{/fieldRows}}\n{{/hasFields}}\n{{#mountNote}}\n\nMount: {{{mountNote}}}\n{{/mountNote}}\n{{#throws}}\n\nThrows: {{throws}}\n{{/throws}}\n{{#example}}\n\n```ts\n{{{example}}}\n```\n{{/example}}\n{{/symbols}}\n{{/sections}}\n",
11
11
  "api/index.md": "{{{generatedMarker}}}\n\n# {{title}}\n\n{{intro}}\n{{#hasEntities}}\n\n## Entities\n\n{{#entities}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/entities}}\n{{/hasEntities}}\n{{#hasTemplates}}\n\n## Templates\n\n{{#templates}}\n- [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})\n{{/templates}}\n{{/hasTemplates}}\n",
12
- "docs/entity-page.md": "{{{generatedMarker}}}\n\n# {{entity.name}}\n{{#summaryLead}}\n\n{{{.}}}\n{{/summaryLead}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n{{#apiRefs.0}}\n\n**API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}\n{{/apiRefs.0}}\n\n{{{preambleHeader}}}\n{{#hasIdentities}}\n\n## Identity\n\n{{#identities}}\n- {{{bullet}}}\n{{/identities}}\n{{/hasIdentities}}\n{{#hasNeighborhoodEr}}\n\n## In context\n\n{{{neighborhoodErBlock}}}\n{{/hasNeighborhoodEr}}\n{{#fields.hasFields}}\n\n## Fields\n\n| Field | Type | Required | Column | Rules |\n|---|---|---|---|---|\n{{#fields.rows}}\n| {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |\n{{/fields.rows}}\n{{/fields.hasFields}}\n{{#fieldDetails.hasDetails}}\n\n## Field details\n\n{{#fieldDetails.rows}}\n{{{block}}}\n\n{{/fieldDetails.rows}}\n{{/fieldDetails.hasDetails}}\n{{#hasRelationships}}\n\n## Relationships\n\n{{#relationships}}\n- {{{bullet}}}\n{{/relationships}}\n{{/hasRelationships}}\n{{#hasUsedBy}}\n\n## Used by\n\n{{#usedBy}}\n- {{{bullet}}}\n{{/usedBy}}\n{{/hasUsedBy}}\n",
12
+ "docs/entity-page.md": "{{{generatedMarker}}}\n\n# {{entity.name}}\n{{#summaryLead}}\n\n{{{.}}}\n{{/summaryLead}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n{{#apiRefs.0}}\n\n**API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}\n{{/apiRefs.0}}\n\n{{{preambleHeader}}}\n{{#hasIdentities}}\n\n## Identity\n\n{{#identities}}\n- {{{bullet}}}\n{{/identities}}\n{{/hasIdentities}}\n{{#hasNeighborhoodEr}}\n\n## In context\n\n{{{neighborhoodErBlock}}}\n{{/hasNeighborhoodEr}}\n{{#fields.hasFields}}\n\n## Fields\n\n| Field | Type | Required | Column | Rules |\n|---|---|---|---|---|\n{{#fields.rows}}\n| {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |\n{{/fields.rows}}\n{{/fields.hasFields}}\n{{#fieldDetails.hasDetails}}\n\n## Field details\n\n{{#fieldDetails.rows}}\n{{{block}}}\n\n{{/fieldDetails.rows}}\n{{/fieldDetails.hasDetails}}\n{{#hasRelationships}}\n\n## Relationships\n\n{{#relationships}}\n- {{{bullet}}}\n{{/relationships}}\n{{/hasRelationships}}\n{{#hasUsedBy}}\n\n## Used by\n\n{{#usedBy}}\n- {{{bullet}}}\n{{/usedBy}}\n{{/hasUsedBy}}\n{{#hasClaimedBy}}\n\n## Required by\n\n{{#claimedBy}}\n- {{{bullet}}}\n{{/claimedBy}}\n{{/hasClaimedBy}}\n",
13
13
  "docs/template-page.md": "{{{generatedMarker}}}\n\n# {{name}}\n{{#descriptionQuote}}\n\n{{{.}}}\n{{/descriptionQuote}}\n\n**Kind:** {{kind}}\n\n## Output\n{{^isEmail}}\n\n- Format: `{{format}}`\n{{/isEmail}}\n{{#isEmail}}\n\nMultipart email — rendered as the following parts:\n\n| Part | Source | Format | Escaping |\n|---|---|---|---|\n{{#parts}}\n| {{label}} | `{{ref}}` | `{{format}}` | {{#escaped}}escaped{{/escaped}}{{^escaped}}raw{{/escaped}} |\n{{/parts}}\n{{/isEmail}}\n\n## Input\n\n- Payload: [`{{payload.name}}`]({{payload.link}})\n{{#hasRequiredTags}}\n- Required fields:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Render contract\n\n- Every field referenced by the template is validated against the payload at generation time; an unknown field fails generation.\n{{#maxChars}}\n- Maximum length: {{.}} characters (rendering longer output fails).\n{{/maxChars}}\n{{#hasRequiredTags}}\n- Required tags must be present:{{#requiredTags}} `{{.}}`{{/requiredTags}}\n{{/hasRequiredTags}}\n\n## Source\n\n{{#sourceRefs}}\n- `{{.}}`\n{{/sourceRefs}}\n{{#templateSourceSection}}\n\n{{{.}}}\n{{/templateSourceSection}}\n\n## Capability\n\n{{capability}}\n",
14
14
  };
@@ -0,0 +1,124 @@
1
+ // FR-038 — walking `requirement.*` nodes and projecting them for downstream filters.
2
+ //
3
+ // WHY A WALK RATHER THAN ctx.matches: the `Generator` contract is entity-shaped —
4
+ // `GenContext.entities` is `MetaObject[]` and `filter` is
5
+ // `(entity: MetaObject) => boolean` — so a requirement-driven generator cannot use
6
+ // it and must walk `loadedRoot` itself. Generalising `Generator` over any node kind
7
+ // is the principled fix and is deliberately out of scope: it is a core contract
8
+ // change touching every existing generator in five ports.
9
+ //
10
+ // WHY A PROJECTION rather than the raw node: an application's filter is app-owned
11
+ // policy (FR-038 §5), and handing it a `MetaData` would bind adopter code to
12
+ // metamodel internals and export the ADR-0039 own-vs-resolving accessor trap. The
13
+ // projection is additive — it can grow, but it never hands out the node.
14
+
15
+ import { TYPE_REQUIREMENT, resolveClaim } from "@metaobjectsdev/metadata";
16
+ import type { MetaData, MetaRequirement } from "@metaobjectsdev/metadata";
17
+
18
+ /** The shape an app's `filter` receives. Never the node itself. */
19
+ export interface RequirementView {
20
+ /** "functional" | "architectural" — the check-polarity axis. */
21
+ readonly subType: string;
22
+ /** 1 solution · 2 segment · 3 service · 4 object · 5 member. Undefined on an
23
+ * unlevelled architectural requirement (the original flat policy form). */
24
+ readonly level: number | undefined;
25
+ readonly status: string | undefined;
26
+ /** Dotted path from the root through nesting ancestors — hierarchy is nesting. */
27
+ readonly path: string;
28
+ /** DISTINCT `type.subType` concerns among the resolved targets. */
29
+ readonly implementedByTypes: readonly string[];
30
+ }
31
+
32
+ export interface ResolvedClaim {
33
+ /** The reference exactly as authored, for the doc comment. */
34
+ readonly ref: string;
35
+ readonly node: MetaData;
36
+ readonly concern: string;
37
+ }
38
+
39
+ export interface WalkedRequirement {
40
+ readonly node: MetaRequirement;
41
+ readonly view: RequirementView;
42
+ readonly targets: readonly ResolvedClaim[];
43
+ }
44
+
45
+ /** `<type>.<subType>` — the key a renderer map is looked up by. */
46
+ export function concernOf(node: MetaData): string {
47
+ return `${node.type}.${node.subType}`;
48
+ }
49
+
50
+ /**
51
+ * The concern key for a requirement that resolves NO targets.
52
+ *
53
+ * Doubles as the catch-all renderer key, deliberately: a requirement with nothing
54
+ * to fan out over falls through to whatever the app registered as its default.
55
+ */
56
+ export const NO_CONCERN = "*";
57
+
58
+ /**
59
+ * Depth-first walk of every `requirement.*` node, nested ones included.
60
+ *
61
+ * Unresolvable `@implementedBy` references are skipped rather than thrown on —
62
+ * resolution severity is `meta verify`'s job (it depends on `@status`, which is why
63
+ * it cannot live in the loader), and codegen must not fail a build over a
64
+ * diagnostic another command owns.
65
+ */
66
+ export function walkRequirements(root: MetaData): WalkedRequirement[] {
67
+ const out: WalkedRequirement[] = [];
68
+
69
+ const visit = (node: MetaData, prefix: string): void => {
70
+ if (node.type !== TYPE_REQUIREMENT) return;
71
+ const path = prefix === "" ? node.name : `${prefix}.${node.name}`;
72
+ const req = node as MetaRequirement;
73
+ // Same referrer-package basis the CLI's checks use, so a bare reference binds
74
+ // package-locally under the ADR-0042 contract.
75
+ const referrerPkg = node.package ?? node.fileDefaultPackage ?? "";
76
+
77
+ const targets: ResolvedClaim[] = [];
78
+ for (const ref of req.implementedBy()) {
79
+ const target = resolveClaim(root, ref, referrerPkg);
80
+ if (target === undefined) continue;
81
+ targets.push({ ref, node: target, concern: concernOf(target) });
82
+ }
83
+
84
+ out.push({
85
+ node: req,
86
+ view: {
87
+ subType: node.subType,
88
+ level: req.level(),
89
+ status: req.status(),
90
+ path,
91
+ implementedByTypes: [...new Set(targets.map((t) => t.concern))],
92
+ },
93
+ targets,
94
+ });
95
+
96
+ for (const child of node.children()) visit(child, path);
97
+ };
98
+
99
+ for (const child of root.children()) visit(child, "");
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Group a requirement's targets by distinct concern — the fan-out unit.
105
+ *
106
+ * One entry per distinct `type.subType`, NOT one per target: a single architectural
107
+ * requirement claimed by 123 entities must emit one stub, not 123, which is the
108
+ * hostile-first-contact outcome FR-038 §10 exists to avoid.
109
+ *
110
+ * A requirement resolving NO targets still yields exactly ONE group. That is not a
111
+ * degenerate case: `REQUIREMENT_LINK_FLOOR_LEVEL` forbids `@implementedBy` below L4,
112
+ * so every L1–L3 requirement resolves nothing — and an application that chooses to
113
+ * cover L3 would otherwise get silence instead of a stub.
114
+ */
115
+ export function groupByConcern(w: WalkedRequirement): Map<string, ResolvedClaim[]> {
116
+ const groups = new Map<string, ResolvedClaim[]>();
117
+ for (const t of w.targets) {
118
+ const existing = groups.get(t.concern);
119
+ if (existing === undefined) groups.set(t.concern, [t]);
120
+ else existing.push(t);
121
+ }
122
+ if (groups.size === 0) groups.set(NO_CONCERN, []);
123
+ return groups;
124
+ }