@metaobjectsdev/codegen-ts 0.24.5 → 1.0.0-rc.1

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 (237) hide show
  1. package/README.md +37 -12
  2. package/dist/api-surface.d.ts +29 -0
  3. package/dist/api-surface.d.ts.map +1 -1
  4. package/dist/api-surface.js +47 -0
  5. package/dist/api-surface.js.map +1 -1
  6. package/dist/column-mapper.d.ts +12 -0
  7. package/dist/column-mapper.d.ts.map +1 -1
  8. package/dist/column-mapper.js +1 -0
  9. package/dist/column-mapper.js.map +1 -1
  10. package/dist/constants.d.ts +24 -11
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +54 -19
  13. package/dist/constants.js.map +1 -1
  14. package/dist/db-emitting.d.ts +6 -0
  15. package/dist/db-emitting.d.ts.map +1 -0
  16. package/dist/db-emitting.js +26 -0
  17. package/dist/db-emitting.js.map +1 -0
  18. package/dist/generator-registry.d.ts.map +1 -1
  19. package/dist/generator-registry.js +14 -1
  20. package/dist/generator-registry.js.map +1 -1
  21. package/dist/generator.d.ts +5 -0
  22. package/dist/generator.d.ts.map +1 -1
  23. package/dist/generator.js.map +1 -1
  24. package/dist/generators/agent-docs-file.d.ts +19 -0
  25. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  26. package/dist/generators/agent-docs-file.js +257 -0
  27. package/dist/generators/agent-docs-file.js.map +1 -0
  28. package/dist/generators/agent-requirements-page.d.ts +22 -0
  29. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  30. package/dist/generators/agent-requirements-page.js +175 -0
  31. package/dist/generators/agent-requirements-page.js.map +1 -0
  32. package/dist/generators/agent-schema-input.d.ts +69 -0
  33. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  34. package/dist/generators/agent-schema-input.js +23 -0
  35. package/dist/generators/agent-schema-input.js.map +1 -0
  36. package/dist/generators/agent-schema-page.d.ts +21 -0
  37. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-page.js +241 -0
  39. package/dist/generators/agent-schema-page.js.map +1 -0
  40. package/dist/generators/agent-ui-page.d.ts +22 -0
  41. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  42. package/dist/generators/agent-ui-page.js +253 -0
  43. package/dist/generators/agent-ui-page.js.map +1 -0
  44. package/dist/generators/api-docs-file.d.ts.map +1 -1
  45. package/dist/generators/api-docs-file.js +13 -6
  46. package/dist/generators/api-docs-file.js.map +1 -1
  47. package/dist/generators/api-model.d.ts +6 -0
  48. package/dist/generators/api-model.d.ts.map +1 -1
  49. package/dist/generators/api-model.js +54 -35
  50. package/dist/generators/api-model.js.map +1 -1
  51. package/dist/generators/callable-file.js +1 -1
  52. package/dist/generators/callable-file.js.map +1 -1
  53. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  54. package/dist/generators/docs-data-builder.js +47 -2
  55. package/dist/generators/docs-data-builder.js.map +1 -1
  56. package/dist/generators/docs-data.d.ts +6 -2
  57. package/dist/generators/docs-data.d.ts.map +1 -1
  58. package/dist/generators/index.d.ts +3 -8
  59. package/dist/generators/index.d.ts.map +1 -1
  60. package/dist/generators/index.js +19 -14
  61. package/dist/generators/index.js.map +1 -1
  62. package/dist/generators/names-file.d.ts +17 -0
  63. package/dist/generators/names-file.d.ts.map +1 -0
  64. package/dist/generators/names-file.js +95 -0
  65. package/dist/generators/names-file.js.map +1 -0
  66. package/dist/generators/requirements-markdown.d.ts +18 -1
  67. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  68. package/dist/generators/requirements-markdown.js +11 -9
  69. package/dist/generators/requirements-markdown.js.map +1 -1
  70. package/dist/generators/routes-file-hono.d.ts +17 -2
  71. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  72. package/dist/generators/routes-file-hono.js +9 -11
  73. package/dist/generators/routes-file-hono.js.map +1 -1
  74. package/dist/generators/routes-file.d.ts +17 -2
  75. package/dist/generators/routes-file.d.ts.map +1 -1
  76. package/dist/generators/routes-file.js +10 -8
  77. package/dist/generators/routes-file.js.map +1 -1
  78. package/dist/generators/template-generator.d.ts.map +1 -1
  79. package/dist/generators/template-generator.js +20 -6
  80. package/dist/generators/template-generator.js.map +1 -1
  81. package/dist/index.d.ts +20 -5
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +43 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/metaobjects-config.d.ts +38 -1
  86. package/dist/metaobjects-config.d.ts.map +1 -1
  87. package/dist/metaobjects-config.js +9 -1
  88. package/dist/metaobjects-config.js.map +1 -1
  89. package/dist/names.d.ts +275 -0
  90. package/dist/names.d.ts.map +1 -0
  91. package/dist/names.js +433 -0
  92. package/dist/names.js.map +1 -0
  93. package/dist/overwrite-policy.d.ts.map +1 -1
  94. package/dist/overwrite-policy.js +15 -3
  95. package/dist/overwrite-policy.js.map +1 -1
  96. package/dist/projection/extract-view-spec.d.ts +25 -1
  97. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  98. package/dist/projection/extract-view-spec.js +79 -6
  99. package/dist/projection/extract-view-spec.js.map +1 -1
  100. package/dist/reference-templates.d.ts +1 -1
  101. package/dist/reference-templates.d.ts.map +1 -1
  102. package/dist/reference-templates.js +1 -1
  103. package/dist/reference-templates.js.map +1 -1
  104. package/dist/render-context.d.ts +12 -1
  105. package/dist/render-context.d.ts.map +1 -1
  106. package/dist/render-context.js +1 -0
  107. package/dist/render-context.js.map +1 -1
  108. package/dist/retired-codegen-attrs.d.ts +32 -0
  109. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  110. package/dist/retired-codegen-attrs.js +47 -0
  111. package/dist/retired-codegen-attrs.js.map +1 -0
  112. package/dist/routes-expose.d.ts +63 -0
  113. package/dist/routes-expose.d.ts.map +1 -0
  114. package/dist/routes-expose.js +57 -0
  115. package/dist/routes-expose.js.map +1 -0
  116. package/dist/runner.d.ts +18 -0
  117. package/dist/runner.d.ts.map +1 -1
  118. package/dist/runner.js +109 -7
  119. package/dist/runner.js.map +1 -1
  120. package/dist/templates/callable-file.d.ts +7 -2
  121. package/dist/templates/callable-file.d.ts.map +1 -1
  122. package/dist/templates/callable-file.js +54 -9
  123. package/dist/templates/callable-file.js.map +1 -1
  124. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  125. package/dist/templates/drizzle-schema.js +236 -65
  126. package/dist/templates/drizzle-schema.js.map +1 -1
  127. package/dist/templates/entity-constants.d.ts +15 -6
  128. package/dist/templates/entity-constants.d.ts.map +1 -1
  129. package/dist/templates/entity-constants.js +85 -161
  130. package/dist/templates/entity-constants.js.map +1 -1
  131. package/dist/templates/entity-file.d.ts.map +1 -1
  132. package/dist/templates/entity-file.js +38 -4
  133. package/dist/templates/entity-file.js.map +1 -1
  134. package/dist/templates/entity-meta-file.d.ts +15 -1
  135. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  136. package/dist/templates/entity-meta-file.js +12 -2
  137. package/dist/templates/entity-meta-file.js.map +1 -1
  138. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  139. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  140. package/dist/templates/entity-ui-descriptor.js +238 -0
  141. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  142. package/dist/templates/field-meta.d.ts +46 -9
  143. package/dist/templates/field-meta.d.ts.map +1 -1
  144. package/dist/templates/field-meta.js +95 -21
  145. package/dist/templates/field-meta.js.map +1 -1
  146. package/dist/templates/names-decl.d.ts +38 -0
  147. package/dist/templates/names-decl.d.ts.map +1 -0
  148. package/dist/templates/names-decl.js +104 -0
  149. package/dist/templates/names-decl.js.map +1 -0
  150. package/dist/templates/projection-decl.d.ts +21 -0
  151. package/dist/templates/projection-decl.d.ts.map +1 -1
  152. package/dist/templates/projection-decl.js +53 -29
  153. package/dist/templates/projection-decl.js.map +1 -1
  154. package/dist/templates/queries-file.d.ts.map +1 -1
  155. package/dist/templates/queries-file.js +17 -2
  156. package/dist/templates/queries-file.js.map +1 -1
  157. package/dist/templates/requirement-test.d.ts.map +1 -1
  158. package/dist/templates/requirement-test.js +13 -2
  159. package/dist/templates/requirement-test.js.map +1 -1
  160. package/dist/templates/routes-file-hono.d.ts +2 -1
  161. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  162. package/dist/templates/routes-file-hono.js +11 -2
  163. package/dist/templates/routes-file-hono.js.map +1 -1
  164. package/dist/templates/routes-file.d.ts +2 -1
  165. package/dist/templates/routes-file.d.ts.map +1 -1
  166. package/dist/templates/routes-file.js +54 -21
  167. package/dist/templates/routes-file.js.map +1 -1
  168. package/dist/templates/value-object-file.d.ts.map +1 -1
  169. package/dist/templates/value-object-file.js +15 -1
  170. package/dist/templates/value-object-file.js.map +1 -1
  171. package/dist/templates/view-decl.d.ts +37 -1
  172. package/dist/templates/view-decl.d.ts.map +1 -1
  173. package/dist/templates/view-decl.js +27 -3
  174. package/dist/templates/view-decl.js.map +1 -1
  175. package/dist/templates/zod-validators.d.ts +21 -0
  176. package/dist/templates/zod-validators.d.ts.map +1 -1
  177. package/dist/templates/zod-validators.js +40 -5
  178. package/dist/templates/zod-validators.js.map +1 -1
  179. package/dist/view-context.d.ts +24 -0
  180. package/dist/view-context.d.ts.map +1 -0
  181. package/dist/view-context.js +81 -0
  182. package/dist/view-context.js.map +1 -0
  183. package/package.json +6 -6
  184. package/src/api-surface.ts +53 -0
  185. package/src/column-mapper.ts +13 -0
  186. package/src/constants.ts +91 -16
  187. package/src/db-emitting.ts +31 -0
  188. package/src/generator-registry.ts +14 -4
  189. package/src/generator.ts +5 -0
  190. package/src/generators/agent-docs-file.ts +297 -0
  191. package/src/generators/agent-requirements-page.ts +199 -0
  192. package/src/generators/agent-schema-input.ts +88 -0
  193. package/src/generators/agent-schema-page.ts +297 -0
  194. package/src/generators/agent-ui-page.ts +290 -0
  195. package/src/generators/api-docs-file.ts +13 -6
  196. package/src/generators/api-model.ts +63 -34
  197. package/src/generators/callable-file.ts +1 -1
  198. package/src/generators/docs-data-builder.ts +46 -2
  199. package/src/generators/docs-data.ts +6 -2
  200. package/src/generators/index.ts +25 -14
  201. package/src/generators/names-file.ts +98 -0
  202. package/src/generators/requirements-markdown.ts +36 -2
  203. package/src/generators/routes-file-hono.ts +19 -10
  204. package/src/generators/routes-file.ts +20 -7
  205. package/src/generators/template-generator.ts +20 -6
  206. package/src/index.ts +64 -4
  207. package/src/metaobjects-config.ts +48 -2
  208. package/src/names.ts +629 -0
  209. package/src/overwrite-policy.ts +15 -3
  210. package/src/projection/extract-view-spec.ts +85 -6
  211. package/src/reference/entity.ts +21 -2
  212. package/src/reference/names.ts +125 -0
  213. package/src/reference/queries.ts +19 -3
  214. package/src/reference/routes-hono.ts +19 -9
  215. package/src/reference/routes.ts +25 -9
  216. package/src/reference-templates.ts +1 -1
  217. package/src/render-context.ts +13 -1
  218. package/src/retired-codegen-attrs.ts +52 -0
  219. package/src/routes-expose.ts +86 -0
  220. package/src/runner.ts +122 -11
  221. package/src/templates/callable-file.ts +60 -8
  222. package/src/templates/drizzle-schema.ts +248 -60
  223. package/src/templates/entity-constants.ts +95 -184
  224. package/src/templates/entity-file.ts +46 -4
  225. package/src/templates/entity-meta-file.ts +16 -3
  226. package/src/templates/entity-ui-descriptor.ts +305 -0
  227. package/src/templates/field-meta.ts +100 -19
  228. package/src/templates/names-decl.ts +165 -0
  229. package/src/templates/projection-decl.ts +72 -31
  230. package/src/templates/queries-file.ts +17 -2
  231. package/src/templates/requirement-test.ts +13 -2
  232. package/src/templates/routes-file-hono.ts +14 -2
  233. package/src/templates/routes-file.ts +69 -24
  234. package/src/templates/value-object-file.ts +15 -1
  235. package/src/templates/view-decl.ts +53 -3
  236. package/src/templates/zod-validators.ts +41 -5
  237. package/src/view-context.ts +88 -0
@@ -6,6 +6,8 @@ import {
6
6
  TYPE_RELATIONSHIP,
7
7
  isMetaObject,
8
8
  isReadOnlySource,
9
+ SOURCE_ROLE_PRIMARY,
10
+ SOURCE_ATTR_SCHEMA,
9
11
  ORIGIN_SUBTYPE_PASSTHROUGH,
10
12
  ORIGIN_SUBTYPE_AGGREGATE,
11
13
  ORIGIN_SUBTYPE_COMPUTED,
@@ -49,7 +51,7 @@ import {
49
51
  resolveObjectRef,
50
52
  type AggregateFunction,
51
53
  } from "@metaobjectsdev/metadata";
52
- import { type MetaData, type MetaField, type MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
54
+ import { type MetaData, type MetaField, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
53
55
  import { intValueMapOf } from "../enum-meta.js";
54
56
  import {
55
57
  columnNameFromField,
@@ -331,16 +333,58 @@ function resolveHop(
331
333
  function viewName(projection: MetaObject, ctx: ExtractContext): string {
332
334
  // The read-only source carries the physical view name. FR-016: physicalName
333
335
  // implements the four-step rule (kind-matching alias → legacy @table →
334
- // source.name → entity-name fallback), so the call below correctly resolves
335
- // @view / @materializedView / legacy @table for projection sources.
336
+ // source.name → entity-name fallback), so `viewSource.physicalName` below
337
+ // correctly resolves @view / @materializedView / legacy @table for projection
338
+ // sources.
336
339
  // ADR-0039: own — projection source classification (mirrors C# projection
337
- // OwnSources / IsReadOnlyProjection): the view name comes from the projection's
338
- // OWN read-only source, not one inherited via extends.
339
- const viewSource = projection.ownChildren().find(isReadOnlySource);
340
+ // OwnSources / IsReadOnlyProjection): the view name comes from the object's
341
+ // OWN read-only source, not one inherited via extends. `ERR_PROJECTION_
342
+ // INHERITED_SOURCE` (subtype-rules.ts) already refuses a concrete projection
343
+ // that would otherwise inherit a source, so own-only never drops a legally
344
+ // reachable source here.
345
+ //
346
+ // This function serves TWO shapes — a plain projection, and a write-through
347
+ // ENTITY's replica view (extractViewSpec's `writeThrough` branch, #213/#214;
348
+ // the parameter name is historical) — which need DIFFERENT selection rules,
349
+ // hence "prefer role:primary, else the first read-only source" rather than
350
+ // "require role:primary":
351
+ // - A plain projection: EVERY own source must be read-only-kind
352
+ // (ERR_PROJECTION_SOURCE_WRITABLE), so if it has any sources at all,
353
+ // exactly one is role:"primary" (ERR_SOURCE_NO_PRIMARY /
354
+ // ERR_SOURCE_MULTIPLE_PRIMARY) and that one IS read-only — the primary
355
+ // branch always fires when there is a source.
356
+ // - A write-through entity: its role:"primary" source is the WRITABLE
357
+ // table (never read-only), so the primary branch never fires; its
358
+ // read-only companion (the replica view) is intentionally NOT primary.
359
+ // The fallback (first read-only own source) is what names it.
360
+ //
361
+ // Selecting the role:"primary" read-only source PREFERENTIALLY over
362
+ // declaration order (rather than "first read-only source in file order") is
363
+ // what makes this agree with `resolveTableName()` (the resolver every §A6
364
+ // site defers to via `resolveObjectNames`): a plain projection declaring a
365
+ // role:"replica" source before its role:"primary" one would otherwise bind a
366
+ // different physical view depending on whether the names generator happened
367
+ // to be active in the run.
368
+ const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
369
+ const viewSource =
370
+ readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
340
371
  const explicit = viewSource?.physicalName;
341
372
  // physicalName always returns a string; empty string means the source had
342
373
  // neither alias nor a name and the owning entity name was empty (impossible
343
374
  // for a real projection). Fall through to the helper anyway for safety.
375
+ //
376
+ // This fallback fires only when the object has NO own read-only source at
377
+ // all. It differs from `resolveTableName()`'s own no-source fallback
378
+ // (`v_<snake(name)>` here vs `pluralize(toSnakeCase(name))` there), but that
379
+ // difference is UNOBSERVABLE: whenever a sourceless object reaches this
380
+ // fallback, `resolveObjectNames()` also finds no primary source and returns
381
+ // `undefined` before ever calling `resolveTableName()` — so no §A6 site has
382
+ // a names constant to reference either way, and both the ON and OFF arms
383
+ // land on THIS function's literal on the identical no-source condition. A
384
+ // sourceless projection also fails `isProjection()` itself
385
+ // (projection-detector.ts requires a read-only-kind source), so codegen's
386
+ // dispatch never reaches `renderProjectionDecl`/this function for it in the
387
+ // first place — doubly unreachable.
344
388
  return explicit !== undefined && explicit !== ""
345
389
  ? explicit
346
390
  : viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
@@ -363,6 +407,41 @@ export function projectionViewName(
363
407
  return viewName(projection, { columnNamingStrategy });
364
408
  }
365
409
 
410
+ /**
411
+ * The `@schema` of the SAME source node {@link projectionViewName} names, or undefined.
412
+ *
413
+ * It re-uses that function's selection rule ("prefer role:primary read-only, else the first
414
+ * read-only own source") deliberately, rather than asking the object for its schema: for a
415
+ * write-through ENTITY the object's primary source is the WRITABLE table, so
416
+ * `resolveTableSchema` would hand back the table's schema and qualify the replica view with
417
+ * it. A view and the table it replicates need not live in the same schema, and a name paired
418
+ * with someone else's schema is worse than no schema at all — it is confidently wrong.
419
+ */
420
+ export function projectionViewSchema(projection: MetaObject): string | undefined {
421
+ const schema = projectionViewSource(projection)?.attr(SOURCE_ATTR_SCHEMA);
422
+ return typeof schema === "string" && schema !== "" ? schema : undefined;
423
+ }
424
+
425
+ /**
426
+ * The source node {@link projectionViewName} names — the ONE selection, so the name, the
427
+ * schema and the role cannot be picked by three functions that agree until they do not.
428
+ *
429
+ * Hoisted out of `projectionViewSchema`, which used to be the only place the rule was
430
+ * written twice-removed from `viewName`. The third caller is what forced it: the names
431
+ * artifact keys its sources by ROLE, so a consumer reaching for a write-through entity's
432
+ * replica view has to know which role that view plays, and guessing `"replica"` would be
433
+ * exactly the second derivation this file exists to avoid.
434
+ */
435
+ export function projectionViewSource(projection: MetaObject): MetaSource | undefined {
436
+ const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
437
+ return readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
438
+ }
439
+
440
+ /** The ROLE of that source — the key its entry has in `<Entity>Names.sources`. */
441
+ export function projectionViewRole(projection: MetaObject): string | undefined {
442
+ return projectionViewSource(projection)?.role;
443
+ }
444
+
366
445
  /**
367
446
  * FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
368
447
  * owner part of `<owner>.<child>...` resolved as an object. Mirrors the
@@ -61,11 +61,14 @@ import {
61
61
  isWriteThrough,
62
62
  isAbstract,
63
63
  hasWritableRdbSource,
64
+ isTphSubtype,
64
65
  // engine composer — used for the delegated write-through variant:
65
66
  renderEntityFile,
66
67
  // engine plumbing:
67
68
  formatTs,
68
69
  entityOutputPath,
70
+ namesRef,
71
+ namesConstArg,
69
72
  GENERATED_HEADER,
70
73
  } from "@metaobjectsdev/codegen-ts";
71
74
 
@@ -84,6 +87,10 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
84
87
  }
85
88
  // Projection → read-only view declaration + read schema.
86
89
  if (isProjection(entity)) {
90
+ // §A6/§B2 — same `namesRef` pair renderDrizzleSchema builds, so the projection's view
91
+ // name + per-field dbCol reference the exact constant the names artifact exports
92
+ // (undefined when the artifact is not in this run). `namesRef`'s `{ resolved, symbol }`
93
+ // return is exactly the shape `ProjectionDeclOpts.names` wants.
87
94
  return renderProjectionDecl(entity, ctx.loadedRoot, {
88
95
  columnNamingStrategy: ctx.columnNamingStrategy,
89
96
  dialect: ctx.dialect,
@@ -92,10 +99,18 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
92
99
  allowlists,
93
100
  ctx,
94
101
  includeViewDecl: runtime,
102
+ names: namesRef(entity, ctx),
95
103
  });
96
104
  }
97
105
  // Value-only / contract target → interface + Zod, no Drizzle table.
98
- if (!runtime || !hasWritableRdbSource(entity)) {
106
+ //
107
+ // A TPH subtype (FR-017) routes here too. It INHERITS the discriminator base's writable
108
+ // source.rdb through `extends`, so `hasWritableRdbSource` is true for it under the
109
+ // ADR-0039 resolving read — but the base owns the single shared table. Without the
110
+ // isTphSubtype clause this template fell through to the vanilla path and emitted a
111
+ // SECOND Drizzle table bound to the base's physical name, carrying only the subtype's
112
+ // own columns.
113
+ if (!runtime || !hasWritableRdbSource(entity) || isTphSubtype(entity)) {
99
114
  return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
100
115
  }
101
116
  // #214 — a write-through entity read-view (writable table + a read-only replica view +
@@ -110,12 +125,16 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
110
125
  const enumAliases = renderEnumTypeAliases(entity, ctx);
111
126
  const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
112
127
  const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
128
+ // §A6/§B2 — same `namesRef` pair renderDrizzleSchema (and the projection branch above)
129
+ // build, so the descriptor's $table references the exact constant the names artifact
130
+ // exports (undefined when the artifact is not in this run).
131
+ const constantsNames = namesRef(entity, ctx);
113
132
  const sections: Code[] = [
114
133
  renderDrizzleSchema(entity, ctx),
115
134
  renderInferredTypes(entity, tphBase, ctx),
116
135
  ...(enumAliases !== null ? [enumAliases] : []),
117
136
  renderZodValidators(entity, ctx),
118
- renderEntityConstants(entity, ctx.apiPrefix),
137
+ renderEntityConstants(entity, ctx.apiPrefix, namesConstArg(constantsNames)),
119
138
  ...(allowlists ? [renderFilterAllowlist(entity, undefined, ctx), renderSortAllowlist(entity)] : []),
120
139
  renderFilterType(entity),
121
140
  ...(tphBlock !== null ? [tphBlock] : []),
@@ -0,0 +1,125 @@
1
+ // REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/names.ts) and own it.
2
+ // Then import it LOCALLY in metaobjects.config.ts:
3
+ // import { namesFile } from "./codegen/generators/names.js";
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.
9
+ // targets: the emit step. Replace `renderNamesDecl` to change the artifact's SHAPE
10
+ // (e.g. flat `SUBSCRIBER_TABLE` constants instead of a nested object); keep
11
+ // `resolveObjectNames` so the names still come from the same resolver as the DDL.
12
+ // use-when: you want the physical database names (table/view + column names) for each
13
+ // object available as importable constants, so a hand-written consumer never
14
+ // has to embed a name as a string literal a second time.
15
+ // emits: <target>/<Entity>.names.ts per concrete object with a primary source
16
+ // (under outputLayout: "package", <target>/<pkg>/<Entity>.names.ts — beside
17
+ // the entity module it describes). An object with no primary source (#248)
18
+ // gets no names artifact — EXCEPT an abstract base that a sourced object
19
+ // extends, which gets a fragment (columns, no physical name) so its
20
+ // children extend it instead of restating every inherited column.
21
+ // customize: swap `renderNamesDecl` for your own shape; keep `resolveObjectNames` as the
22
+ // one resolver so the constant and the DDL it describes cannot disagree.
23
+ // composes-with: entity.ts, routes.ts — both reference these constants instead of
24
+ // embedding the names a second time. queries.ts never embeds a physical
25
+ // name of its own: it reads/writes through the Drizzle table object
26
+ // entity.ts builds (columns keyed by FIELD name), so there is nothing in
27
+ // it to wire.
28
+ //
29
+ // The composition here is deliberately a SEPARATE generator, never a boolean on the entity
30
+ // generator — a new artifact is a MINOR under docs/compatibility-policy.md and adds zero
31
+ // bytes to existing files, where a flag would move every $table-carrying golden for the
32
+ // same functionality.
33
+ import {
34
+ crossEntitySpecifier,
35
+ entityOutputPath,
36
+ namesArtifactSuperOf,
37
+ renderNamesDecl,
38
+ resolveObjectNames,
39
+ type EmittedFile,
40
+ type GenContext,
41
+ type Generator,
42
+ } from "@metaobjectsdev/codegen-ts";
43
+ import type { MetaObject } from "@metaobjectsdev/metadata";
44
+
45
+ export function namesFile(): Generator {
46
+ return {
47
+ name: "names",
48
+ // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
49
+ // entity generator can tell whether this artifact will exist. Exactly the mechanism
50
+ // routesFileHono already uses via emitsHonoRoutes/includeHonoRoutes.
51
+ emitsNames: true,
52
+ generate: (ctx: GenContext): EmittedFile[] => {
53
+ const layout = ctx.config.outputLayout ?? "flat";
54
+ const extStyle = ctx.config.extStyle ?? "js";
55
+ // The strategy lives on the RENDER CONTEXT, not on ResolvedGenConfig — `ctx.config`
56
+ // carries outDir/extStyle/dbImport/dialect and nothing about naming.
57
+ const strategy = ctx.renderContext?.columnNamingStrategy;
58
+
59
+ const pathOf = (obj: MetaObject): string =>
60
+ // entityOutputPath, not a bare filename: §A6 makes the entity module IMPORT these
61
+ // constants, so the artifact has to land in the same directory the entity module
62
+ // does. Under outputLayout: "package" a bare name puts it at the target ROOT while
63
+ // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
64
+ // conflicting-duplicate-path failure as soon as two packages declare a
65
+ // same-bare-named entity.
66
+ entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
67
+
68
+ const superSpecifierFor = (obj: MetaObject): string | undefined => {
69
+ const sup = namesArtifactSuperOf(obj);
70
+ return sup === undefined
71
+ ? undefined
72
+ : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
73
+ };
74
+
75
+ const out: EmittedFile[] = [];
76
+ // Pass 1 — every matched object that participates in the database (#248).
77
+ // `emitted` tracks what pass 1 actually WROTE, not what it looked at: a matched
78
+ // abstract base emits nothing here, and seeding it as already-emitted is what would
79
+ // make pass 2 skip the very object it exists to produce.
80
+ const emitted = new Set<string>();
81
+ const participants = ctx.entities.filter(ctx.matches);
82
+ for (const entity of participants) {
83
+ const content = renderNamesDecl(entity, {
84
+ strategy, superSpecifier: superSpecifierFor(entity),
85
+ });
86
+ if (content === "") continue; // no primary source ⇒ no names artifact (#248)
87
+ emitted.add(entity.resolutionKey());
88
+ out.push({ path: pathOf(entity), content });
89
+ }
90
+
91
+ // Pass 2 — the abstract bases those participants EXTEND. Each carries the columns it
92
+ // declares, so a child states them once rather than restating its parent's.
93
+ //
94
+ // Reached by walking UP from a participant, never by scanning for abstracts: that is
95
+ // what keeps #248 intact. A sourceless object nothing persistable extends — an
96
+ // `object.value`, say — is not reached, so it acquires no artifact and no phantom
97
+ // participation. Two children of one base both reach it and emit the same file at
98
+ // the same path with the same bytes; the runner collapses byte-identical duplicates
99
+ // (#266), so it is written once. `emitted` keeps that from even arising, and keeps
100
+ // the walk from re-rendering the same base once per child.
101
+ for (const entity of participants) {
102
+ if (resolveObjectNames(entity, strategy) === undefined) continue;
103
+ for (let sup = namesArtifactSuperOf(entity); sup !== undefined;
104
+ sup = namesArtifactSuperOf(sup)) {
105
+ const key = sup.resolutionKey();
106
+ if (emitted.has(key)) break; // already emitted, and so is everything above it
107
+ emitted.add(key);
108
+ const content = renderNamesDecl(sup, {
109
+ strategy, superSpecifier: superSpecifierFor(sup),
110
+ // "Fragment" means "declares no source". Hardcoding `true` is right for the
111
+ // shape this pass was written for — an abstract base with columns and no table
112
+ // — and wrong for the one it also reaches: `meta gen --entities <Subtype>`
113
+ // walks up to a TPH BASE, which owns the shared table, and a fragment renders
114
+ // no source at all. The engine derives this itself now (`renderNamesDecl`
115
+ // consults the object), so the flag says only "this is an ancestor render".
116
+ fragment: true,
117
+ });
118
+ if (content === "") continue;
119
+ out.push({ path: pathOf(sup), content });
120
+ }
121
+ }
122
+ return out;
123
+ },
124
+ };
125
+ }
@@ -41,6 +41,7 @@ import {
41
41
  renderFindByIdFn,
42
42
  renderListFn,
43
43
  renderCreateFn,
44
+ renderInsertPreservingFn,
44
45
  renderUpdateFn,
45
46
  renderDeleteByIdFn,
46
47
  renderReverseFinderFns,
@@ -50,6 +51,7 @@ import {
50
51
  isWriteThrough,
51
52
  isTphSubtype,
52
53
  hasAnyRdbSource,
54
+ hasAutoSetFields,
53
55
  renderQueriesFile, // engine composer — used for the delegated variants
54
56
  formatTs,
55
57
  entityOutputPath,
@@ -77,20 +79,33 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
77
79
 
78
80
  // `db` is parameter-passed into every finder (ADR-0008). Emit the dialect-correct
79
81
  // Drizzle type alias so signatures typecheck without the consumer constructing one.
82
+ // Keep EVERY type argument as open as Drizzle's own constraint allows — a `Db` this
83
+ // alias cannot name is a helper nobody can call, and uncompilable generated code looks
84
+ // exactly like unused generated code from outside. Both the driver axis (base classes,
85
+ // not `NodePgDatabase` / `<"async">`) and the schema axis (`Record<string, unknown>`,
86
+ // Drizzle's own `TFullSchema extends …` bound, NOT its `Record<string, never>` default)
87
+ // have been re-pinned once already; do not narrow either again. A schema-carrying
88
+ // `drizzle(client, { schema })` must assign, and so must a schema-less `drizzle(client)`.
80
89
  const dbTypeImport =
81
90
  ctx.dialect === "postgres"
82
91
  ? `import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";`
83
92
  : `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
84
93
  const dbTypeAlias =
85
94
  ctx.dialect === "postgres"
86
- ? `type Db = PgDatabase<PgQueryResultHKT, Record<string, never>>;`
87
- : `type Db = BaseSQLiteDatabase<"sync" | "async", unknown>;`;
95
+ ? `type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;`
96
+ : `type Db = BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>;`;
97
+
98
+ // #203 — an @autoSet entity additionally imports its preserving-shape schema and emits
99
+ // the `insertPreserving<Entity>` escape hatch after `create<Entity>`. OWNED: drop both
100
+ // halves together if you never insert rows carrying their own timestamps.
101
+ const autoSet = hasAutoSetFields(obj);
102
+ const preservingImport = autoSet ? `, ${entityName}InsertPreservingSchema` : "";
88
103
 
89
104
  const literalImports = code`
90
105
  ${dbTypeImport}
91
106
  ${dbTypeAlias}
92
107
 
93
- import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}InsertSchema, ${entityName}UpdateSchema } from ${JSON.stringify(entityFileName)};
108
+ import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}InsertSchema${preservingImport}, ${entityName}UpdateSchema } from ${JSON.stringify(entityFileName)};
94
109
  `;
95
110
 
96
111
  const sections: Code[] = [
@@ -98,6 +113,7 @@ import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}I
98
113
  renderFindByIdFn(obj, ctx),
99
114
  renderListFn(obj, ctx),
100
115
  renderCreateFn(obj, ctx),
116
+ ...(autoSet ? [renderInsertPreservingFn(obj, ctx)] : []),
101
117
  renderUpdateFn(obj, ctx),
102
118
  renderDeleteByIdFn(obj, ctx),
103
119
  ];
@@ -19,7 +19,11 @@
19
19
  // object and for TPH subtypes.
20
20
  // customize: this generator is YOURS — edit it freely. For the emitted route
21
21
  // composition, call `renderRoutesFileHono` (exported from the engine) and
22
- // wrap its result, or replace the call entirely.
22
+ // wrap its result, or replace the call entirely. Decide per generator what
23
+ // you consume: wire only the generators whose output you actually import,
24
+ // and narrow this one with its `filter`. There is no `@emit*` metadata
25
+ // attribute — those were never registered vocabulary, so `meta verify`
26
+ // rejects them (ERR_UNKNOWN_ATTR).
23
27
  // composes-with: entity.ts (imports the table/schemas/allowlists), queries.ts.
24
28
 
25
29
  import { type MetaObject } from "@metaobjectsdev/metadata";
@@ -28,15 +32,26 @@ import {
28
32
  type Generator,
29
33
  type GeneratorFactory,
30
34
  renderRoutesFileHono,
35
+ resolveExpose,
36
+ type ExposeOption,
31
37
  isTphSubtype,
32
38
  hasAnyRdbSource,
33
39
  formatTs,
34
40
  entityOutputPath,
35
- CODEGEN_ATTR_EMIT_ROUTES,
36
41
  } from "@metaobjectsdev/codegen-ts";
37
42
 
38
43
  export interface RoutesFileHonoOpts {
39
44
  filter?: (entity: MetaObject) => boolean;
45
+ /**
46
+ * Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
47
+ * absent means all five and emits byte-identical output.
48
+ *
49
+ * routesFileHono({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
50
+ *
51
+ * A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
52
+ * only remove the whole surface, not restrict it to a subset of verbs.
53
+ */
54
+ expose?: ExposeOption;
40
55
  target?: string;
41
56
  }
42
57
 
@@ -48,17 +63,12 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
48
63
  // without being named as held back, or get warned about while still emitting.
49
64
  // (Same shape as tanstack's grid generator, which factors it the same way.)
50
65
  const passesOtherGates = (e: MetaObject): boolean =>
51
- // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
52
- e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false
53
- && hasAnyRdbSource(e)
54
- && userFilter(e);
66
+ hasAnyRdbSource(e) && userFilter(e);
55
67
  const generator: Generator = {
56
68
  name: "routes-file-hono",
57
69
  // Marks this as the Hono routes generator so the runner can aggregate
58
70
  // `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
59
71
  emitsHonoRoutes: true,
60
- // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
61
- //
62
72
  // TPH subtypes are EXCLUDED, matching the Fastify generator. A TPH subtype shares
63
73
  // its base's table, so mounting vanilla CRUD for it produced routes with no
64
74
  // discriminator scoping at all: the list returned EVERY subtype's rows, and
@@ -93,7 +103,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
93
103
  entity.package,
94
104
  `${entity.name}.routes.hono.ts`,
95
105
  ),
96
- content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext)),
106
+ content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
97
107
  };
98
108
  });
99
109
  if (opts?.target) {
@@ -12,14 +12,20 @@
12
12
  // framework; see also the routes-hono template, whose deps-injected shape
13
13
  // ports more easily to hosts that hand you a request.
14
14
  // use-when: you want generated Fastify REST routes per entity. Drop it and hand-write routes
15
- // if you need bespoke endpoints — or keep it and add handlers via <Entity>.extra.ts.
15
+ // if you need bespoke endpoints — or keep it and register extra handlers YOURSELF
16
+ // alongside the generated `register<Entity>Routes(app)` call. Nothing here
17
+ // discovers a sibling module: a `<Entity>.extra.ts` next to the output is a naming
18
+ // convention, not a plugin point, so its handlers only mount if your server calls them.
16
19
  // emits: <target>/<Entity>.routes.ts — full CRUD for write-through entities, read-only
17
20
  // (GET list + GET :id) for projections, polymorphic + per-subtype for TPH bases.
18
21
  // Skipped for any sourceless object (incl. every object.value, source-less by
19
22
  // value purity) and for TPH subtypes — no source.rdb means no table/allowlist
20
23
  // for a routes file to import (#248 R2).
21
- // customize: this generator (filter, output path, per-entity @emitRoutes opt-out, target) is
22
- // YOURS edit it freely. The route *composition* itself is richer than the others
24
+ // customize: this generator (filter, output path, target) is YOURS edit it freely.
25
+ // Decide per generator what you consume: wire only the generators whose
26
+ // output you actually import, and narrow this one with its `filter`. There
27
+ // is no `@emit*` metadata attribute — those were never registered
28
+ // vocabulary, so `meta verify` rejects them (ERR_UNKNOWN_ATTR). The route *composition* itself is richer than the others
23
29
  // (M:N junction traversal, TPH per-subtype route sets), so it stays in the engine via
24
30
  // `renderRoutesFile`. To own the composition too, copy `renderRoutesFile`'s body out
25
31
  // of the package source — it dispatches projection → mountReadOnlyCrudRoutes,
@@ -34,15 +40,26 @@ import {
34
40
  type Generator,
35
41
  type GeneratorFactory,
36
42
  renderRoutesFile,
43
+ resolveExpose,
44
+ type ExposeOption,
37
45
  isTphSubtype,
38
46
  hasAnyRdbSource,
39
47
  formatTs,
40
48
  entityOutputPath,
41
- CODEGEN_ATTR_EMIT_ROUTES,
42
49
  } from "@metaobjectsdev/codegen-ts";
43
50
 
44
51
  export interface RoutesFileOpts {
45
52
  filter?: (entity: MetaObject) => boolean;
53
+ /**
54
+ * Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
55
+ * absent means all five and emits byte-identical output.
56
+ *
57
+ * routesFile({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
58
+ *
59
+ * A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
60
+ * only remove the whole surface, not restrict it to a subset of verbs.
61
+ */
62
+ expose?: ExposeOption;
46
63
  target?: string;
47
64
  }
48
65
 
@@ -50,21 +67,20 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
50
67
  const userFilter = opts?.filter ?? (() => true);
51
68
  const generator: Generator = {
52
69
  name: "routes-file",
53
- // per-entity opt-out via `@emitRoutes: false`; TPH subtypes get no standalone routes
54
- // file (their routes live in the discriminator base's); AND-composed with your filter.
70
+ // TPH subtypes get no standalone routes file (their routes live in the discriminator
71
+ // base's); AND-composed with your filter.
55
72
  // #248 R2: an object with no declared/inherited source.rdb (of ANY kind) isn't
56
73
  // backed by any store — routes against it would import Drizzle table/allowlist
57
74
  // exports the entity file never emits. Gated by hasAnyRdbSource.
58
75
  filter: (e: MetaObject) =>
59
- // ADR-0039: resolving a concrete entity may inherit its @emit* opt-out flag via extends.
60
- e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
76
+ hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
61
77
  generate: perEntity(async (entity, ctx) => {
62
78
  if (!ctx.renderContext) {
63
79
  throw new Error("routes-file: renderContext is required (provided by runGen)");
64
80
  }
65
81
  return {
66
82
  path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.routes.ts`),
67
- content: await formatTs(renderRoutesFile(entity, ctx.renderContext)),
83
+ content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
68
84
  };
69
85
  }),
70
86
  };
@@ -13,7 +13,7 @@ import { dirname, join } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
14
 
15
15
  /** Basenames (no extension) of the copyable reference generators shipped in `src/reference/`. */
16
- export const REFERENCE_GENERATOR_NAMES = ["entity", "queries", "routes", "routes-hono", "barrel"] as const;
16
+ export const REFERENCE_GENERATOR_NAMES = ["entity", "queries", "routes", "routes-hono", "barrel", "names"] as const;
17
17
  export type ReferenceGeneratorName = (typeof REFERENCE_GENERATOR_NAMES)[number];
18
18
 
19
19
  /** A directory is a reference root iff it holds the first template the reader was told to expect. */
@@ -65,6 +65,16 @@ export interface RenderContext {
65
65
  emitAbstractShapes: boolean;
66
66
  /** Output layout mode: "flat" (default) — all files in outDir; "package" — sub-paths from entity metadata package. */
67
67
  outputLayout: OutputLayout;
68
+ /**
69
+ * §A6 — whether the run emits the `<Entity>Names` artifact (the runner aggregates the
70
+ * suite's `emitsNames` markers; `ctx.config.includeNames` carries the same fact for
71
+ * generators). A template may reference those constants ONLY when this is true: the
72
+ * names generator is opt-in under ADR-0034 scaffold-and-own, so an unconditional
73
+ * import would break every project that has not enabled it. Defaults to FALSE, which
74
+ * is what keeps output byte-identical for such a project — and for every bare-context
75
+ * caller (unit tests, generators invoked outside runGen).
76
+ */
77
+ includeNames: boolean;
68
78
  /**
69
79
  * Resolve an entity name to its Drizzle collection (table) variable name,
70
80
  * applying the project's pluralization config + per-entity overrides. Every
@@ -108,7 +118,7 @@ export interface RenderContext {
108
118
  }
109
119
 
110
120
  /** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). `collectionName` is built from `pluralizeCollections` + `collectionNameOverrides` (both default to always-pluralize). */
111
- export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "clientDirective" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "valueObjectNames" | "valueObjectEmittedName" | "resolveValueObjectName" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
121
+ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "clientDirective" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "includeNames" | "packageOf" | "valueObjectNames" | "valueObjectEmittedName" | "resolveValueObjectName" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
112
122
  extStyle?: ExtStyle;
113
123
  omImport?: string;
114
124
  columnNamingStrategy?: ColumnNamingStrategy;
@@ -117,6 +127,7 @@ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "
117
127
  apiPrefix?: string;
118
128
  emitAbstractShapes?: boolean;
119
129
  outputLayout?: OutputLayout;
130
+ includeNames?: boolean;
120
131
  packageOf?: Map<string, string | undefined>;
121
132
  /** ADR-0044/#228 value-object emitted-name map (resolutionKey → emitted name).
122
133
  * Defaults to an empty Map — bare names, byte-identical to pre-#228 output.
@@ -196,6 +207,7 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
196
207
  apiPrefix: opts.apiPrefix ?? "",
197
208
  emitAbstractShapes: opts.emitAbstractShapes ?? true,
198
209
  outputLayout,
210
+ includeNames: opts.includeNames ?? false,
199
211
  packageOf: opts.packageOf ?? new Map(),
200
212
  valueObjectNames,
201
213
  valueObjectEmittedName: (obj: MetaData) => valueObjectNames.get(obj.resolutionKey()) ?? obj.name,
@@ -0,0 +1,52 @@
1
+ import type { MetaObject } from "@metaobjectsdev/metadata";
2
+ import { RETIRED_CODEGEN_ATTRS } from "./constants.js";
3
+
4
+ /**
5
+ * `@emitRoutes` / `@emitTanstack` / `@emitForm` / `@emitGrid` / `@emitAngular` were read
6
+ * off metadata by generator filters and were never registered metamodel vocabulary. Under
7
+ * the strict loader — which is what `meta verify` runs — every one of them is
8
+ * `ERR_UNKNOWN_ATTR`, while `meta gen` loads non-strict and honoured them. That is the
9
+ * defect: an adopter who authored the documented opt-out got working suppression AND a red
10
+ * `meta verify`, with nothing connecting the two.
11
+ *
12
+ * The reads are gone (see constants.ts for why registering them was refused rather than
13
+ * the other way round). But a project can be sitting on a WORKING `@emitRoutes: false`
14
+ * today, and deleting the read alone would start writing that suppressed file with no
15
+ * explanation — a silent behaviour change on upgrade, which is the one outcome not open
16
+ * to us. So the run says it.
17
+ *
18
+ * This follows the `layout.dataGrid` precedent (#287, data-grid-gate.ts) and the prompt
19
+ * generator gate beside it: tell the adopter at `meta gen` time rather than in a doc line
20
+ * that gets missed the same way the original one was. Warning only — the exit code is
21
+ * untouched, and `--dry-run` reports it too, since generators run before the write phase
22
+ * branches.
23
+ *
24
+ * It lives in the RUNNER, not in a generator, for two reasons. It is a model-level
25
+ * observation, so it must fire exactly ONCE per run rather than once per generator that
26
+ * happens to be wired; and the attribute is equally stale whether or not the generator it
27
+ * used to suppress is wired at all, so a generator-local check would go quiet in exactly
28
+ * the project that dropped the generator and kept the attribute.
29
+ *
30
+ * Self-extinguishing: removing the attribute from the metadata silences it forever. That
31
+ * is also the fix, since the same edit is what makes `meta verify` pass.
32
+ */
33
+ export function warnRetiredCodegenAttrs(
34
+ entities: readonly MetaObject[],
35
+ warn: (msg: string) => void,
36
+ ): void {
37
+ for (const { name, replacement } of RETIRED_CODEGEN_ATTRS) {
38
+ // ADR-0039: resolving — an INHERITED flag suppressed emission exactly as an own one
39
+ // did, so an own-only read here would leave the inheriting adopter unwarned while
40
+ // their output silently changed. `hasAttr` rather than a value comparison: an
41
+ // `@emitForm: true` was a no-op that still fails `meta verify`, so it is just as
42
+ // stale as the `false` that did something.
43
+ const carriers = entities.filter((e) => e.hasAttr(name));
44
+ if (carriers.length === 0) continue;
45
+ warn(
46
+ `@${name} on ${carriers.map((e) => e.name).join(", ")} is no longer read by any ` +
47
+ `generator — it was never registered metamodel vocabulary, so \`meta verify\` ` +
48
+ `rejects it outright with ERR_UNKNOWN_ATTR. Decide per generator what you ` +
49
+ `consume: ${replacement}. Remove the attribute to silence this.`,
50
+ );
51
+ }
52
+ }