@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
@@ -32,6 +32,8 @@ import {
32
32
  FILTER_OP_LT,
33
33
  FILTER_OP_LTE,
34
34
  FILTER_OP_IS_NULL,
35
+ FILTER_OP_LIKE,
36
+ FIELD_SUBTYPE_ENUM,
35
37
  FILTER_COMPOSE_AND,
36
38
  FILTER_COMPOSE_OR,
37
39
  SORT_ORDER_DESC,
@@ -47,6 +49,7 @@ import {
47
49
  type AggregateFunction,
48
50
  } from "@metaobjectsdev/metadata";
49
51
  import { type MetaData, type MetaField, type MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
52
+ import { intValueMapOf } from "../enum-meta.js";
50
53
  import {
51
54
  columnNameFromField,
52
55
  viewNameFromProjection,
@@ -126,13 +129,84 @@ function resolveAggregateFilter(
126
129
  kind: "cmp",
127
130
  ref: `${alias}.${sourceColumnNameFor(field, ctx)}`,
128
131
  op,
129
- value: opObj[op],
132
+ // Same int-backed-enum encoding as the row-scope @filter below: this scoping
133
+ // filter renders as a SQL literal too (FILTER (WHERE …) / CASE WHEN), so a
134
+ // member symbol would land unencoded in an integer comparison.
135
+ value: encodeIntEnumFilterValue(
136
+ opObj[op],
137
+ op,
138
+ field.subType === FIELD_SUBTYPE_ENUM ? intValueMapOf(field) : undefined,
139
+ key,
140
+ entity.name,
141
+ ),
130
142
  });
131
143
  }
132
144
  if (clauses.length === 0) return undefined;
133
145
  return clauses.length === 1 ? clauses[0]! : { kind: "and", clauses };
134
146
  }
135
147
 
148
+ /**
149
+ * The `@intValueMap` of every int-backed `field.enum` the projection declares, keyed
150
+ * by field name. Only int-backed enums appear, so a lookup miss means "no encoding".
151
+ *
152
+ * `fields()` (effective) and `intValueMapOf` (which reads `attr`, RESOLVING) — a
153
+ * projection's fields are bound through `extends` to the base entity's, and post-#246
154
+ * the map itself commonly lives one hop further up on a shared abstract declaration.
155
+ * Own-only at either hop would silently emit the member symbol into an integer column
156
+ * (ADR-0039).
157
+ */
158
+ function intEnumMapsOf(projection: MetaObject): ReadonlyMap<string, Record<string, number>> {
159
+ const out = new Map<string, Record<string, number>>();
160
+ for (const f of projection.fields()) {
161
+ if (f.subType !== FIELD_SUBTYPE_ENUM) continue;
162
+ const map = intValueMapOf(f);
163
+ if (map !== undefined) out.set(f.name, map);
164
+ }
165
+ return out;
166
+ }
167
+
168
+ /**
169
+ * Lower a filter value for an int-backed `field.enum` from its member SYMBOL to the
170
+ * INTEGER it persists as. A no-op for every other field (`intMap` undefined), so a
171
+ * string-backed enum's SQL is byte-identical.
172
+ *
173
+ * `isNull` is skipped — its value is a boolean, not a member. `like` is unreachable:
174
+ * `opsForField` removes it from an int-backed enum's band, so the loader rejects it
175
+ * before codegen; the explicit throw makes that a loud failure rather than a
176
+ * `LIKE NaN`. An unmapped member is likewise loader-unreachable (the key set is
177
+ * pinned equal to `@values`) and throws for the same reason — silently emitting the
178
+ * symbol would produce DDL that fails only at apply time, against a live database.
179
+ */
180
+ function encodeIntEnumFilterValue(
181
+ value: unknown,
182
+ op: string,
183
+ intMap: Record<string, number> | undefined,
184
+ fieldName: string,
185
+ projectionName: string,
186
+ ): unknown {
187
+ if (intMap === undefined) return value;
188
+ if (op === FILTER_OP_IS_NULL) return value;
189
+ if (op === FILTER_OP_LIKE) {
190
+ throw new Error(
191
+ `Projection ${projectionName}: view @filter uses "like" on "${fieldName}", an ` +
192
+ `int-backed field.enum (@intValueMap) — it stores as an integer column, so a ` +
193
+ `substring match is not expressible. Use eq/ne/in.`,
194
+ );
195
+ }
196
+ const encode = (v: unknown): unknown => {
197
+ if (typeof v !== "string") return v;
198
+ const n = intMap[v];
199
+ if (typeof n !== "number") {
200
+ throw new Error(
201
+ `Projection ${projectionName}: view @filter value "${v}" for "${fieldName}" has no ` +
202
+ `entry in @intValueMap.`,
203
+ );
204
+ }
205
+ return n;
206
+ };
207
+ return Array.isArray(value) ? value.map(encode) : encode(value);
208
+ }
209
+
136
210
  /**
137
211
  * #207 — resolve a projection's row-scope `@filter` (the desugared canonical
138
212
  * `{ field: { op: value }, and?, or? }`) into a {@link ViewFilterClause} whose
@@ -152,13 +226,14 @@ function resolveViewFilter(
152
226
  filter: unknown,
153
227
  columnsByField: ReadonlyMap<string, SelectColumn>,
154
228
  projectionName: string,
229
+ intMapsByField: ReadonlyMap<string, Record<string, number>>,
155
230
  ): ViewFilterClause | undefined {
156
231
  if (typeof filter !== "object" || filter === null || Array.isArray(filter)) return undefined;
157
232
  const clauses: ViewFilterClause[] = [];
158
233
  for (const [key, val] of Object.entries(filter as Record<string, unknown>)) {
159
234
  if (key === FILTER_AND || key === FILTER_OR) {
160
235
  const subs = (Array.isArray(val) ? val : [])
161
- .map((s) => resolveViewFilter(s, columnsByField, projectionName))
236
+ .map((s) => resolveViewFilter(s, columnsByField, projectionName, intMapsByField))
162
237
  .filter((c): c is ViewFilterClause => c !== undefined);
163
238
  if (subs.length > 0) clauses.push({ kind: key === FILTER_AND ? "and" : "or", clauses: subs });
164
239
  continue;
@@ -177,7 +252,14 @@ function resolveViewFilter(
177
252
  // becomes its own comparison, AND-composed (dropping all-but-the-first would silently
178
253
  // widen the exposed row set). The loader has already validated every op for this
179
254
  // field's subtype.
180
- for (const [op, value] of Object.entries(desugarClause(val))) {
255
+ for (const [op, rawValue] of Object.entries(desugarClause(val))) {
256
+ // An INT-BACKED field.enum (@intValueMap, design D5) stores as an INTEGER
257
+ // column, so the authored member SYMBOL must become its integer before it is
258
+ // rendered as a SQL literal. The Drizzle customType handles the runtime query
259
+ // path, but view DDL is emitted as literal SQL text and never touches Drizzle.
260
+ const value = encodeIntEnumFilterValue(
261
+ rawValue, op, intMapsByField.get(key), key, projectionName,
262
+ );
181
263
  if (col.kind === "passthrough") {
182
264
  clauses.push({ kind: "cmp", ref: `${col.sourceAlias}.${col.sourceColumn}`, op, value });
183
265
  } else if (col.kind === "computed") {
@@ -990,7 +1072,9 @@ export function extractViewSpec(
990
1072
  const columnsByField = new Map<string, SelectColumn>(
991
1073
  selectSpec.columns.map((c) => [c.fieldName, c] as const),
992
1074
  );
993
- where = resolveViewFilter(rawFilter, columnsByField, projection.name);
1075
+ where = resolveViewFilter(
1076
+ rawFilter, columnsByField, projection.name, intEnumMapsOf(projection),
1077
+ );
994
1078
  }
995
1079
  }
996
1080
 
@@ -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
+ }