@metaobjectsdev/codegen-ts 0.24.4 → 0.25.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 (265) 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/client-directive.d.ts +18 -0
  7. package/dist/client-directive.d.ts.map +1 -0
  8. package/dist/client-directive.js +37 -0
  9. package/dist/client-directive.js.map +1 -0
  10. package/dist/column-mapper.d.ts +12 -0
  11. package/dist/column-mapper.d.ts.map +1 -1
  12. package/dist/column-mapper.js +1 -0
  13. package/dist/column-mapper.js.map +1 -1
  14. package/dist/constants.d.ts +24 -11
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +54 -19
  17. package/dist/constants.js.map +1 -1
  18. package/dist/db-emitting.d.ts +6 -0
  19. package/dist/db-emitting.d.ts.map +1 -0
  20. package/dist/db-emitting.js +26 -0
  21. package/dist/db-emitting.js.map +1 -0
  22. package/dist/generator-registry.d.ts.map +1 -1
  23. package/dist/generator-registry.js +7 -1
  24. package/dist/generator-registry.js.map +1 -1
  25. package/dist/generator.d.ts +5 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/agent-docs-file.d.ts +19 -0
  29. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  30. package/dist/generators/agent-docs-file.js +257 -0
  31. package/dist/generators/agent-docs-file.js.map +1 -0
  32. package/dist/generators/agent-requirements-page.d.ts +22 -0
  33. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  34. package/dist/generators/agent-requirements-page.js +175 -0
  35. package/dist/generators/agent-requirements-page.js.map +1 -0
  36. package/dist/generators/agent-schema-input.d.ts +69 -0
  37. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-input.js +23 -0
  39. package/dist/generators/agent-schema-input.js.map +1 -0
  40. package/dist/generators/agent-schema-page.d.ts +21 -0
  41. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  42. package/dist/generators/agent-schema-page.js +241 -0
  43. package/dist/generators/agent-schema-page.js.map +1 -0
  44. package/dist/generators/agent-ui-page.d.ts +22 -0
  45. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  46. package/dist/generators/agent-ui-page.js +253 -0
  47. package/dist/generators/agent-ui-page.js.map +1 -0
  48. package/dist/generators/api-docs-file.d.ts.map +1 -1
  49. package/dist/generators/api-docs-file.js +13 -6
  50. package/dist/generators/api-docs-file.js.map +1 -1
  51. package/dist/generators/api-model.d.ts +6 -0
  52. package/dist/generators/api-model.d.ts.map +1 -1
  53. package/dist/generators/api-model.js +54 -35
  54. package/dist/generators/api-model.js.map +1 -1
  55. package/dist/generators/callable-file.js +1 -1
  56. package/dist/generators/callable-file.js.map +1 -1
  57. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  58. package/dist/generators/docs-data-builder.js +47 -2
  59. package/dist/generators/docs-data-builder.js.map +1 -1
  60. package/dist/generators/docs-data.d.ts +6 -2
  61. package/dist/generators/docs-data.d.ts.map +1 -1
  62. package/dist/generators/index.d.ts +3 -0
  63. package/dist/generators/index.d.ts.map +1 -1
  64. package/dist/generators/index.js +4 -0
  65. package/dist/generators/index.js.map +1 -1
  66. package/dist/generators/names-file.d.ts +17 -0
  67. package/dist/generators/names-file.d.ts.map +1 -0
  68. package/dist/generators/names-file.js +95 -0
  69. package/dist/generators/names-file.js.map +1 -0
  70. package/dist/generators/requirements-markdown.d.ts +18 -1
  71. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  72. package/dist/generators/requirements-markdown.js +11 -9
  73. package/dist/generators/requirements-markdown.js.map +1 -1
  74. package/dist/generators/routes-file-hono.d.ts +17 -2
  75. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  76. package/dist/generators/routes-file-hono.js +15 -12
  77. package/dist/generators/routes-file-hono.js.map +1 -1
  78. package/dist/generators/routes-file.d.ts +17 -2
  79. package/dist/generators/routes-file.d.ts.map +1 -1
  80. package/dist/generators/routes-file.js +10 -8
  81. package/dist/generators/routes-file.js.map +1 -1
  82. package/dist/generators/template-generator.d.ts.map +1 -1
  83. package/dist/generators/template-generator.js +20 -6
  84. package/dist/generators/template-generator.js.map +1 -1
  85. package/dist/index.d.ts +21 -5
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +40 -4
  88. package/dist/index.js.map +1 -1
  89. package/dist/metaobjects-config.d.ts +67 -1
  90. package/dist/metaobjects-config.d.ts.map +1 -1
  91. package/dist/metaobjects-config.js +10 -1
  92. package/dist/metaobjects-config.js.map +1 -1
  93. package/dist/names.d.ts +275 -0
  94. package/dist/names.d.ts.map +1 -0
  95. package/dist/names.js +434 -0
  96. package/dist/names.js.map +1 -0
  97. package/dist/naming.d.ts +17 -0
  98. package/dist/naming.d.ts.map +1 -1
  99. package/dist/naming.js +21 -0
  100. package/dist/naming.js.map +1 -1
  101. package/dist/overwrite-policy.d.ts.map +1 -1
  102. package/dist/overwrite-policy.js +15 -3
  103. package/dist/overwrite-policy.js.map +1 -1
  104. package/dist/payload-codegen.d.ts.map +1 -1
  105. package/dist/payload-codegen.js +4 -3
  106. package/dist/payload-codegen.js.map +1 -1
  107. package/dist/projection/extract-view-spec.d.ts +25 -1
  108. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  109. package/dist/projection/extract-view-spec.js +79 -6
  110. package/dist/projection/extract-view-spec.js.map +1 -1
  111. package/dist/prompt-generator-gate.d.ts +27 -0
  112. package/dist/prompt-generator-gate.d.ts.map +1 -0
  113. package/dist/prompt-generator-gate.js +62 -0
  114. package/dist/prompt-generator-gate.js.map +1 -0
  115. package/dist/reference-templates.d.ts +22 -5
  116. package/dist/reference-templates.d.ts.map +1 -1
  117. package/dist/reference-templates.js +55 -21
  118. package/dist/reference-templates.js.map +1 -1
  119. package/dist/render-context.d.ts +20 -1
  120. package/dist/render-context.d.ts.map +1 -1
  121. package/dist/render-context.js +2 -0
  122. package/dist/render-context.js.map +1 -1
  123. package/dist/retired-codegen-attrs.d.ts +32 -0
  124. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  125. package/dist/retired-codegen-attrs.js +47 -0
  126. package/dist/retired-codegen-attrs.js.map +1 -0
  127. package/dist/routes-expose.d.ts +63 -0
  128. package/dist/routes-expose.d.ts.map +1 -0
  129. package/dist/routes-expose.js +57 -0
  130. package/dist/routes-expose.js.map +1 -0
  131. package/dist/runner.d.ts +18 -0
  132. package/dist/runner.d.ts.map +1 -1
  133. package/dist/runner.js +115 -7
  134. package/dist/runner.js.map +1 -1
  135. package/dist/templates/callable-file.d.ts +7 -2
  136. package/dist/templates/callable-file.d.ts.map +1 -1
  137. package/dist/templates/callable-file.js +54 -9
  138. package/dist/templates/callable-file.js.map +1 -1
  139. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  140. package/dist/templates/drizzle-schema.js +236 -65
  141. package/dist/templates/drizzle-schema.js.map +1 -1
  142. package/dist/templates/entity-constants.d.ts +15 -6
  143. package/dist/templates/entity-constants.d.ts.map +1 -1
  144. package/dist/templates/entity-constants.js +85 -161
  145. package/dist/templates/entity-constants.js.map +1 -1
  146. package/dist/templates/entity-file.d.ts.map +1 -1
  147. package/dist/templates/entity-file.js +38 -4
  148. package/dist/templates/entity-file.js.map +1 -1
  149. package/dist/templates/entity-meta-file.d.ts +15 -1
  150. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  151. package/dist/templates/entity-meta-file.js +12 -2
  152. package/dist/templates/entity-meta-file.js.map +1 -1
  153. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  154. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  155. package/dist/templates/entity-ui-descriptor.js +238 -0
  156. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  157. package/dist/templates/field-meta.d.ts +46 -9
  158. package/dist/templates/field-meta.d.ts.map +1 -1
  159. package/dist/templates/field-meta.js +95 -21
  160. package/dist/templates/field-meta.js.map +1 -1
  161. package/dist/templates/names-decl.d.ts +38 -0
  162. package/dist/templates/names-decl.d.ts.map +1 -0
  163. package/dist/templates/names-decl.js +104 -0
  164. package/dist/templates/names-decl.js.map +1 -0
  165. package/dist/templates/output-parser.d.ts.map +1 -1
  166. package/dist/templates/output-parser.js +54 -8
  167. package/dist/templates/output-parser.js.map +1 -1
  168. package/dist/templates/projection-decl.d.ts +21 -0
  169. package/dist/templates/projection-decl.d.ts.map +1 -1
  170. package/dist/templates/projection-decl.js +53 -29
  171. package/dist/templates/projection-decl.js.map +1 -1
  172. package/dist/templates/queries-file.d.ts.map +1 -1
  173. package/dist/templates/queries-file.js +17 -2
  174. package/dist/templates/queries-file.js.map +1 -1
  175. package/dist/templates/render-helper.d.ts.map +1 -1
  176. package/dist/templates/render-helper.js +3 -1
  177. package/dist/templates/render-helper.js.map +1 -1
  178. package/dist/templates/requirement-test.d.ts.map +1 -1
  179. package/dist/templates/requirement-test.js +48 -21
  180. package/dist/templates/requirement-test.js.map +1 -1
  181. package/dist/templates/routes-file-hono.d.ts +2 -1
  182. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  183. package/dist/templates/routes-file-hono.js +11 -2
  184. package/dist/templates/routes-file-hono.js.map +1 -1
  185. package/dist/templates/routes-file.d.ts +2 -1
  186. package/dist/templates/routes-file.d.ts.map +1 -1
  187. package/dist/templates/routes-file.js +54 -21
  188. package/dist/templates/routes-file.js.map +1 -1
  189. package/dist/templates/value-object-file.d.ts.map +1 -1
  190. package/dist/templates/value-object-file.js +15 -1
  191. package/dist/templates/value-object-file.js.map +1 -1
  192. package/dist/templates/view-decl.d.ts +37 -1
  193. package/dist/templates/view-decl.d.ts.map +1 -1
  194. package/dist/templates/view-decl.js +27 -3
  195. package/dist/templates/view-decl.js.map +1 -1
  196. package/dist/templates/zod-validators.d.ts +21 -0
  197. package/dist/templates/zod-validators.d.ts.map +1 -1
  198. package/dist/templates/zod-validators.js +40 -5
  199. package/dist/templates/zod-validators.js.map +1 -1
  200. package/dist/view-context.d.ts +24 -0
  201. package/dist/view-context.d.ts.map +1 -0
  202. package/dist/view-context.js +81 -0
  203. package/dist/view-context.js.map +1 -0
  204. package/package.json +6 -6
  205. package/src/api-surface.ts +53 -0
  206. package/src/client-directive.ts +36 -0
  207. package/src/column-mapper.ts +13 -0
  208. package/src/constants.ts +91 -16
  209. package/src/db-emitting.ts +31 -0
  210. package/src/generator-registry.ts +7 -0
  211. package/src/generator.ts +5 -0
  212. package/src/generators/agent-docs-file.ts +297 -0
  213. package/src/generators/agent-requirements-page.ts +199 -0
  214. package/src/generators/agent-schema-input.ts +88 -0
  215. package/src/generators/agent-schema-page.ts +297 -0
  216. package/src/generators/agent-ui-page.ts +290 -0
  217. package/src/generators/api-docs-file.ts +13 -6
  218. package/src/generators/api-model.ts +63 -34
  219. package/src/generators/callable-file.ts +1 -1
  220. package/src/generators/docs-data-builder.ts +46 -2
  221. package/src/generators/docs-data.ts +6 -2
  222. package/src/generators/index.ts +10 -0
  223. package/src/generators/names-file.ts +98 -0
  224. package/src/generators/requirements-markdown.ts +36 -2
  225. package/src/generators/routes-file-hono.ts +26 -15
  226. package/src/generators/routes-file.ts +20 -7
  227. package/src/generators/template-generator.ts +20 -6
  228. package/src/index.ts +63 -5
  229. package/src/metaobjects-config.ts +78 -2
  230. package/src/names.ts +630 -0
  231. package/src/naming.ts +23 -0
  232. package/src/overwrite-policy.ts +15 -3
  233. package/src/payload-codegen.ts +4 -3
  234. package/src/projection/extract-view-spec.ts +85 -6
  235. package/src/prompt-generator-gate.ts +73 -0
  236. package/src/reference/barrel.ts +3 -0
  237. package/src/reference/entity.ts +28 -2
  238. package/src/reference/names.ts +125 -0
  239. package/src/reference/queries.ts +14 -2
  240. package/src/reference/routes-hono.ts +113 -0
  241. package/src/reference/routes.ts +30 -9
  242. package/src/reference-templates.ts +61 -21
  243. package/src/render-context.ts +22 -1
  244. package/src/retired-codegen-attrs.ts +52 -0
  245. package/src/routes-expose.ts +86 -0
  246. package/src/runner.ts +129 -11
  247. package/src/templates/callable-file.ts +60 -8
  248. package/src/templates/drizzle-schema.ts +248 -60
  249. package/src/templates/entity-constants.ts +95 -184
  250. package/src/templates/entity-file.ts +46 -4
  251. package/src/templates/entity-meta-file.ts +16 -3
  252. package/src/templates/entity-ui-descriptor.ts +305 -0
  253. package/src/templates/field-meta.ts +100 -19
  254. package/src/templates/names-decl.ts +165 -0
  255. package/src/templates/output-parser.ts +54 -7
  256. package/src/templates/projection-decl.ts +72 -31
  257. package/src/templates/queries-file.ts +17 -2
  258. package/src/templates/render-helper.ts +3 -1
  259. package/src/templates/requirement-test.ts +49 -18
  260. package/src/templates/routes-file-hono.ts +14 -2
  261. package/src/templates/routes-file.ts +69 -24
  262. package/src/templates/value-object-file.ts +15 -1
  263. package/src/templates/view-decl.ts +53 -3
  264. package/src/templates/zod-validators.ts +41 -5
  265. package/src/view-context.ts +88 -0
package/src/constants.ts CHANGED
@@ -1,28 +1,103 @@
1
1
  // Local constants for codegen-ts.
2
2
 
3
- /** The marker that says "codegen owns this file" — drives the overwrite policy. */
3
+ /**
4
+ * The header line every emitter puts at the top of a generated file.
5
+ *
6
+ * Informational ON THIS PORT. It used to be documented here as the thing that "drives
7
+ * the overwrite policy", and it does not: `overwrite-policy.ts` decides from the
8
+ * `.gen-state` snapshot body and the committed hash manifest and never reads this
9
+ * string (grep it — no non-template module imports it). Deleting the header from a
10
+ * generated file does not take ownership of it here; it just changes the content, so
11
+ * the hash stops matching and the file is refused like any other edit.
12
+ *
13
+ * The JVM ports are the opposite: there the marker IS the decision
14
+ * (`GeneratedFileWriter`), and deleting it is exactly how you take ownership. Both
15
+ * behaviours are stated in `docs/features/own-your-codegen.md` — do not collapse them
16
+ * into one sentence, because the gesture that protects a file differs per port.
17
+ */
4
18
  export const GENERATED_HEADER = "@generated by @metaobjectsdev/codegen-ts";
5
19
 
6
- /** Suffix for sibling user extension files (codegen never touches these). */
7
- export const EXTRA_SUFFIX = ".extra";
20
+ // `EXTRA_SUFFIX = ".extra"` was removed here. It was a public export of this package
21
+ // wired to nothing: no generator, orphan sweep or write path ever compared a path
22
+ // against it, so its presence implied tool support for a name that is pure convention.
23
+ // A `<Entity>.extra.ts` sibling is safe because codegen only writes the paths it
24
+ // records in `.gen-state/.hashes.json` — not because of its name — and the generated
25
+ // barrel is built from the model, so it does not re-export it. Say that in prose;
26
+ // do not re-add a constant that suggests the engine knows the name.
8
27
 
9
28
  /** Default outDir used by tests + as a sane default for generate(). */
10
29
  export const DEFAULT_OUT_DIR = "./src/db/entities";
11
30
 
12
31
  // ---------------------------------------------------------------------------
13
- // Codegen-control attributes.
32
+ // Retired codegen-control attributes.
14
33
  //
15
- // These are per-entity opt-in/opt-out flags read by generators (NOT metamodel
16
- // vocabulary they tune codegen, not the model). Named here so the literals
17
- // aren't scattered as magic strings across the generator packages (compile-time
18
- // typo safety), matching the metadata package's constants discipline.
34
+ // `@emitRoutes` / `@emitTanstack` / `@emitForm` / `@emitGrid` / `@emitAngular`
35
+ // were read off metadata by generator filters and were NEVER registered
36
+ // metamodel vocabulary. The strict loader rejects every one of them with
37
+ // ERR_UNKNOWN_ATTR and `meta verify` loads strict — while `meta gen` loads
38
+ // non-strict and honoured them. So an adopter who authored the documented
39
+ // per-entity opt-out got the behaviour AND a red `meta verify`: half-working,
40
+ // which is worse than either half, and exactly the class ADR-0023 §2 names.
41
+ //
42
+ // Registering them was refused. That moves `metamodelVersion` and forces a
43
+ // four-registry publish for a TypeScript-only generator kill switch that four
44
+ // of five ports would never read. The replacement already exists and is
45
+ // verified end to end: decide per generator what you consume — wire only the
46
+ // generators whose output you actually import, and narrow one with its
47
+ // `filter` option. The one clause that was opt-IN rather than opt-out (a TPH
48
+ // subtype's own per-subtype grid) cannot be a `filter`, since a filter is
49
+ // ANDed with the built-in gates and can only NARROW; it is the
50
+ // `tphSubtypeGrids` option on tanstackGrid()/tanstackGridHook() instead.
51
+ //
52
+ // No generator reads any of these names now. They survive HERE and nowhere
53
+ // else, so `meta gen` can recognise a stale one still sitting in an adopter's
54
+ // metadata and say what replaced it (see retired-codegen-attrs.ts). Keeping
55
+ // the name and its replacement in ONE record is the point: a warning whose
56
+ // text lives apart from the list it is warning about drifts from it.
19
57
  // ---------------------------------------------------------------------------
20
58
 
21
- /** `@emitTanstack: false` skip the TanStack hooks + grid generators for an entity. */
22
- export const CODEGEN_ATTR_EMIT_TANSTACK = "emitTanstack";
23
- /** `@emitGrid: true` opt a TPH subtype IN to its own per-subtype grid (default: the polymorphic base grid is the single source). */
24
- export const CODEGEN_ATTR_EMIT_GRID = "emitGrid";
25
- /** `@emitForm: false` skip the React form generator for an entity. */
26
- export const CODEGEN_ATTR_EMIT_FORM = "emitForm";
27
- /** `@emitRoutes: false` — skip the Fastify routes generator for an entity. */
28
- export const CODEGEN_ATTR_EMIT_ROUTES = "emitRoutes";
59
+ /** One retired codegen-control attribute, paired with what an adopter does instead. */
60
+ export interface RetiredCodegenAttr {
61
+ /** The bare attribute name, as it appears in metadata (no `@` sigil). */
62
+ readonly name: string;
63
+ /** What to do instead, phrased as an instruction and naming a real API. */
64
+ readonly replacement: string;
65
+ }
66
+
67
+ /** Every retired codegen-control attribute. Read ONLY by the `meta gen` warning. */
68
+ export const RETIRED_CODEGEN_ATTRS: readonly RetiredCodegenAttr[] = [
69
+ {
70
+ name: "emitRoutes",
71
+ replacement:
72
+ 'drop routesFile() / routesFileHono() from `generators` in metaobjects.config.ts if ' +
73
+ 'you do not consume REST routes, or narrow it — routesFile({ filter: (e) => e.name !== "Ledger" })',
74
+ },
75
+ {
76
+ name: "emitTanstack",
77
+ replacement:
78
+ 'drop tanstackQuery() / tanstackGrid() / tanstackGridHook() from `generators` in ' +
79
+ 'metaobjects.config.ts if you do not consume their output, or narrow one — ' +
80
+ 'tanstackQuery({ filter: (e) => e.name !== "InternalAudit" })',
81
+ },
82
+ {
83
+ name: "emitForm",
84
+ replacement:
85
+ 'drop formFile() from `generators` in metaobjects.config.ts if you do not consume ' +
86
+ 'generated forms, or narrow it — formFile({ filter: (e) => e.name !== "InternalAudit" })',
87
+ },
88
+ {
89
+ name: "emitGrid",
90
+ replacement:
91
+ 'pass the `tphSubtypeGrids` option — tanstackGrid({ tphSubtypeGrids: (e) => e.name === ' +
92
+ '"CopayAuth" }) — and pass the SAME predicate to tanstackGridHook(), or the emitted ' +
93
+ '<Sub>.grid.ts has no <Sub>.columns.tsx to import. This one is an opt-IN, so a `filter` ' +
94
+ 'cannot express it: a filter is ANDed with the built-in gates and can only narrow',
95
+ },
96
+ {
97
+ name: "emitAngular",
98
+ replacement:
99
+ 'drop angularServiceFile() / angularFormFile() / angularGridFile() from `generators` in ' +
100
+ 'metaobjects.config.ts if you do not consume Angular output, or narrow one — ' +
101
+ 'angularServiceFile({ filter: (e) => e.name !== "InternalAudit" })',
102
+ },
103
+ ] as const;
@@ -0,0 +1,31 @@
1
+ // Which objects make a project a DATABASE project — and therefore require a `dialect`.
2
+ //
3
+ // THIS IS THE ONE ANSWER, and it is hoisted here because two commands need it and a second
4
+ // copy would be a second answer. `runGen` throws when a model emits database code and the
5
+ // config declares no dialect (see `missingDialectMessage`), and it throws BEFORE
6
+ // `normalizeConfig` fills its defaults on purpose: `DEFAULT_DIALECT` is INERT, existing
7
+ // only so a value-object-only project need not name one. A DB project that forgot it gets
8
+ // a named error rather than silently-defaulted output — "a Postgres project quietly
9
+ // emitting sqlite", as the runner's own comment puts it.
10
+ //
11
+ // `meta docs` needs the same answer for `agent/schema.md`. Reading `?? DEFAULT_DIALECT`
12
+ // there instead would document a sqlite schema for a project `meta gen` refuses to build
13
+ // — asserting an answer the toolchain never gave.
14
+
15
+ import type { MetaObject } from "@metaobjectsdev/metadata";
16
+ import { hasAnyRdbSource } from "./source-detect.js";
17
+
18
+ /** The concrete objects a run would emit database code for. Empty ⇒ no dialect needed. */
19
+ export function dbEmittingObjects(entities: readonly MetaObject[]): MetaObject[] {
20
+ return entities.filter((e) => !e.isAbstract && hasAnyRdbSource(e));
21
+ }
22
+
23
+ /** The one wording for "this model needs a dialect and the config has none". */
24
+ export function missingDialectMessage(dbEmitting: readonly MetaObject[]): string {
25
+ const names = dbEmitting.map((e) => e.name).join(", ");
26
+ return (
27
+ `codegen config is missing dialect — required because this model ` +
28
+ `generates database code for: ${names}. Set dialect in ` +
29
+ `metaobjects.config.ts. (A model of only value objects and/or sourceless projections may omit it.)`
30
+ );
31
+ }
@@ -25,6 +25,7 @@ import {
25
25
  routesFile,
26
26
  routesFileHono,
27
27
  barrel,
28
+ namesFile,
28
29
  mermaidErDiagram,
29
30
  promptRender,
30
31
  outputParser,
@@ -111,6 +112,12 @@ export const generatorRegistry: Record<string, GeneratorRegistryEntry> = {
111
112
  factory: () => barrel(),
112
113
  options: "target?",
113
114
  },
115
+ names: {
116
+ name: "names",
117
+ description: "Per-entity physical database name constants (table/view, schema, columns).",
118
+ tier: "native",
119
+ factory: () => namesFile(),
120
+ },
114
121
  "prompt-render": {
115
122
  name: "prompt-render",
116
123
  description: "Per-template prompt-render helper over the render engine.",
package/src/generator.ts CHANGED
@@ -63,6 +63,11 @@ export interface Generator {
63
63
  * that Hono routes are actually being emitted and document them — rather than
64
64
  * silently omitting the Hono CRUD registrars whenever the variant is wired. */
65
65
  emitsHonoRoutes?: boolean;
66
+ /** §A6 — marks the generator that emits the <Entity>Names artifact. The runner
67
+ * aggregates this across the suite into ResolvedGenConfig.includeNames, which the
68
+ * entity generator reads to decide whether it may reference those constants.
69
+ * Same mechanism as emitsHonoRoutes/includeHonoRoutes. */
70
+ emitsNames?: boolean;
66
71
  }
67
72
 
68
73
  export type GeneratorFactory<TOpts = void> = TOpts extends void
@@ -0,0 +1,297 @@
1
+ // agentDocsFile() — the `agent` docs surface.
2
+ //
3
+ // Three pages under `agent/`, each one an agent reads BEFORE touching a tier:
4
+ //
5
+ // • `agent/schema.md` — before touching persistence
6
+ // • `agent/ui.md` — before touching a form or a grid
7
+ // • `agent/requirements.md` — before adding a capability
8
+ //
9
+ // (`api/AGENT-API.md`, the fourth file the always-on pointer names, is the api surface's
10
+ // and is emitted by `apiDocsFile()`. It is not duplicated here.)
11
+ //
12
+ // EVERY PAGE IS DERIVED FROM AN EXISTING BUILDER, and that is the design constraint
13
+ // rather than an implementation detail. A documentation surface an agent is told to
14
+ // TRUST has to be true, and the only way to keep three more pages true is to give them
15
+ // no derivation of their own:
16
+ //
17
+ // schema ← the expected-schema snapshot `meta migrate` diffs and emits from
18
+ // (injected; see agent-schema-input.ts for why codegen-ts refuses to
19
+ // compute it) + `resolveObjectNames`, the field→column resolver the
20
+ // names artifact and the DDL already share
21
+ // ui ← `buildEntityUiDescriptor`, the same derivation emitted as the
22
+ // `<Entity>` const that `useEntityForm` reads at runtime
23
+ // requirements ← `walkRequirements` + `requirementRows`, the same walk the ledger
24
+ // surface and the generated test stubs are built on
25
+ //
26
+ // CONFIG-GATED, like the api surface. Physical names, the dialect and view dispatch all
27
+ // depend on the gen config, so `meta docs` only selects this surface when it has one.
28
+ // The NEUTRAL model surface stays neutral (ADR-0020); this is a different surface with a
29
+ // different contract, not a relaxation of that one.
30
+ //
31
+ // AN EMPTY PAGE IS NO FILE. Each renderer returns "" when its tier has nothing to
32
+ // describe — no physical schema, no UI, no ledger — and an empty render emits nothing.
33
+ // That is what makes the surface safe to leave on: a headless project with no ledger sees
34
+ // no `agent/` directory rather than three pages of headings.
35
+ //
36
+ // These files are READ, never imported. There is no three-way merge and no hand-edit
37
+ // preservation to think about: regenerate and the page is current.
38
+
39
+ import {
40
+ CARDINALITY_MANY,
41
+ CARDINALITY_ONE,
42
+ FIELD_SUBTYPE_ENUM,
43
+ ORIGIN_AGGREGATE_ATTR_AGG,
44
+ ORIGIN_AGGREGATE_ATTR_OF,
45
+ ORIGIN_AGGREGATE_ATTR_VIA,
46
+ ORIGIN_ATTR_ORDER_BY,
47
+ ORIGIN_PASSTHROUGH_ATTR_FROM,
48
+ ORIGIN_PASSTHROUGH_ATTR_VIA,
49
+ ORIGIN_SUBTYPE_AGGREGATE,
50
+ ORIGIN_SUBTYPE_COMPUTED,
51
+ ORIGIN_SUBTYPE_FIRST,
52
+ ORIGIN_SUBTYPE_PASSTHROUGH,
53
+ RELATIONSHIP_ATTR_CARDINALITY,
54
+ RELATIONSHIP_ATTR_OBJECT_REF,
55
+ RELATIONSHIP_ATTR_ON_DELETE,
56
+ RELATIONSHIP_ATTR_THROUGH,
57
+ PHYSICAL_NAME_ATTR_BY_KIND,
58
+ SOURCE_ROLE_PRIMARY,
59
+ TYPE_ORIGIN,
60
+ resolveColumnName,
61
+ } from "@metaobjectsdev/metadata";
62
+ import type { ColumnNamingStrategy, MetaField, MetaObject } from "@metaobjectsdev/metadata";
63
+ import type { EmittedFile, Generator, GeneratorFactory } from "../generator.js";
64
+ import { resolveObjectNames } from "../names.js";
65
+ import { isAbstract } from "../instance-artifacts.js";
66
+ import { enumValues, intValueMapOf } from "../enum-meta.js";
67
+ import { renderAgentSchemaPage } from "./agent-schema-page.js";
68
+ import { renderAgentUiPage } from "./agent-ui-page.js";
69
+ import { renderAgentRequirementsPage } from "./agent-requirements-page.js";
70
+ import type { AgentSchemaInput } from "./agent-schema-input.js";
71
+
72
+ /** All three pages live here, under the docs root. */
73
+ const DEFAULT_AGENT_DIR = "agent";
74
+
75
+ export interface AgentDocsFileOpts {
76
+ /** Output prefix for the agent pages. Default `agent`. */
77
+ subDir?: string;
78
+ /** Optional named output target (registry key). */
79
+ target?: string;
80
+ /**
81
+ * The physical schema, with its resolvers injected by whoever owns them. ABSENT is a
82
+ * supported state — `meta docs` runs without a dialect, and a project with no physical
83
+ * schema gets no schema page rather than a page of unknowns.
84
+ */
85
+ schema?: AgentSchemaInput;
86
+ /** The project's column naming strategy, for the field→column mapping. */
87
+ columnNamingStrategy?: ColumnNamingStrategy;
88
+ }
89
+
90
+ /** One `origin.*` child rendered as a lineage phrase. `origin.*` NEVER inherits, so the
91
+ * own-accessor read here is the correct one rather than an ADR-0039 slip. */
92
+ function lineageOf(field: MetaField): string | undefined {
93
+ const origin = field.ownChildren().find((c) => c.type === TYPE_ORIGIN);
94
+ if (origin === undefined) return undefined;
95
+ const str = (name: string): string | undefined => {
96
+ const v = origin.attr(name);
97
+ return typeof v === "string" && v !== "" ? v : undefined;
98
+ };
99
+ switch (origin.subType) {
100
+ case ORIGIN_SUBTYPE_PASSTHROUGH: {
101
+ const from = str(ORIGIN_PASSTHROUGH_ATTR_FROM);
102
+ const via = str(ORIGIN_PASSTHROUGH_ATTR_VIA);
103
+ return `passthrough from \`${from ?? "?"}\`${via === undefined ? "" : ` via \`${via}\``}`;
104
+ }
105
+ case ORIGIN_SUBTYPE_AGGREGATE: {
106
+ const agg = str(ORIGIN_AGGREGATE_ATTR_AGG) ?? "?";
107
+ const of = str(ORIGIN_AGGREGATE_ATTR_OF);
108
+ const via = str(ORIGIN_AGGREGATE_ATTR_VIA);
109
+ return `\`${agg}\`${of === undefined ? "" : ` of \`${of}\``}${via === undefined ? "" : ` via \`${via}\``}`;
110
+ }
111
+ case ORIGIN_SUBTYPE_FIRST: {
112
+ const of = str(ORIGIN_AGGREGATE_ATTR_OF);
113
+ const via = str(ORIGIN_AGGREGATE_ATTR_VIA);
114
+ const order = str(ORIGIN_ATTR_ORDER_BY);
115
+ return `first${of === undefined ? "" : ` \`${of}\``}${via === undefined ? "" : ` via \`${via}\``}` +
116
+ `${order === undefined ? "" : ` ordered by \`${order}\``}`;
117
+ }
118
+ case ORIGIN_SUBTYPE_COMPUTED:
119
+ // The @expr tree is structured; naming it is enough to route a reader to the
120
+ // declaration, and rendering a tree into a cell would be a second SQL lowering.
121
+ return "computed from a declared `@expr`";
122
+ default:
123
+ return `\`origin.${origin.subType}\``;
124
+ }
125
+ }
126
+
127
+ /**
128
+ * The cardinality phrase, from the DECLARING object's side.
129
+ *
130
+ * `@cardinality: one` does NOT mean one-to-one. It means this object holds the foreign
131
+ * key — many rows of it point at one target row — which is what `relation-resolver.ts`
132
+ * builds a belongs-to `one()` relation from. Rendering it as "one-to-one" told a reader
133
+ * the target row is claimed by at most one row here, which is the direction that makes
134
+ * someone write a lookup expecting a single result.
135
+ *
136
+ * `@cardinality` is OPTIONAL vocabulary (min 0), and ABSENT is its own answer: no
137
+ * navigation is generated at all. Folding that into "one-to-one" invented a shape the
138
+ * model never declared.
139
+ */
140
+ function cardinalityPhrase(cardinality: unknown, through: unknown): string {
141
+ if (cardinality === CARDINALITY_MANY) {
142
+ return typeof through === "string" && through !== "" ? "many-to-many" : "one-to-many";
143
+ }
144
+ if (cardinality === CARDINALITY_ONE) return "many-to-one";
145
+ return "cardinality not declared";
146
+ }
147
+
148
+ /** `- \`Order.lines\` — one-to-many → \`OrderLine\`` */
149
+ function relationshipLines(objects: readonly MetaObject[]): string[] {
150
+ const out: string[] = [];
151
+ for (const obj of objects) {
152
+ for (const rel of obj.relationships()) {
153
+ const cardinality = rel.attr(RELATIONSHIP_ATTR_CARDINALITY);
154
+ const target = rel.attr(RELATIONSHIP_ATTR_OBJECT_REF);
155
+ const through = rel.attr(RELATIONSHIP_ATTR_THROUGH);
156
+ const onDelete = rel.attr(RELATIONSHIP_ATTR_ON_DELETE);
157
+ // The FQN, not the short name: this line is an ADDRESS a reader searches for.
158
+ const parts = [`\`${obj.resolutionKey()}.${rel.name}\``, `\`${rel.subType}\``];
159
+ parts.push(
160
+ `${cardinalityPhrase(cardinality, through)} → \`${String(target ?? "?")}\``,
161
+ );
162
+ if (typeof through === "string" && through !== "") parts.push(`through \`${through}\``);
163
+ if (typeof onDelete === "string" && onDelete !== "") parts.push(`on delete \`${onDelete}\``);
164
+ out.push(`- ${parts.join(" · ")}`);
165
+ }
166
+ }
167
+ return out;
168
+ }
169
+
170
+ /**
171
+ * `| \`Order.status\` | OPEN, CLOSED | string-backed |`
172
+ *
173
+ * `objects` IS ONLY THE TABLE-BACKED SUBSET OF THE MODEL — the caller scopes it to the
174
+ * objects the snapshot actually holds as tables. This section sits on the SCHEMA page, so
175
+ * every row it prints is read as a statement about a column. Walking every loaded object
176
+ * printed rows for abstract bases, for `object.value`s that have no column anywhere, and
177
+ * for projections whose "column" is a view expression — three kinds of thing the physical
178
+ * schema does not contain.
179
+ *
180
+ * It also says nothing about a `CHECK`. Whether the database constrains the members is
181
+ * `table.checks`, which is already on this page from the snapshot, and an `@isArray`
182
+ * enum deliberately gets none (`migrate-ts` skips it) — so asserting one here was a
183
+ * SECOND derivation of a fact the page already carries correctly, in the direction that
184
+ * promises the database will refuse a value it will accept.
185
+ */
186
+ function enumLines(objects: readonly MetaObject[]): string[] {
187
+ const rows: string[] = [];
188
+ for (const obj of objects) {
189
+ for (const field of obj.fields()) {
190
+ if (field.subType !== FIELD_SUBTYPE_ENUM) continue;
191
+ const members = enumValues(field);
192
+ if (members === undefined || members.length === 0) continue;
193
+ const intMap = intValueMapOf(field);
194
+ const backing =
195
+ intMap === undefined
196
+ ? "string-backed"
197
+ : `int-backed (${members.map((m) => `${m}=${intMap[m]}`).join(", ")})`;
198
+ rows.push(
199
+ `| \`${obj.resolutionKey()}.${field.name}\` | ${members.map((m) => `\`${m}\``).join(", ")} | ${backing} |`,
200
+ );
201
+ }
202
+ }
203
+ if (rows.length === 0) return [];
204
+ return ["| Field | Members | Storage |", "|---|---|---|", ...rows];
205
+ }
206
+
207
+ export const agentDocsFile = function agentDocsFile(opts?: AgentDocsFileOpts): Generator {
208
+ const dir = (opts?.subDir ?? DEFAULT_AGENT_DIR).replace(/\/$/, "");
209
+
210
+ const generator: Generator = {
211
+ name: "agent-docs",
212
+ generate(ctx) {
213
+ const objects = ctx.loadedRoot.objects();
214
+ const files: EmittedFile[] = [];
215
+
216
+ // ---- schema.md
217
+ if (opts?.schema !== undefined) {
218
+ const schema = opts.schema;
219
+ // The qualified names the snapshot actually holds. An object whose physical name
220
+ // is not one of these contributes nothing to the physical schema (an abstract
221
+ // base, a sourceless value, an `@unmanaged` object) and must not appear on a page
222
+ // describing it — `buildExpectedSchema`'s Pass 1 owns those skip rules and this
223
+ // reads its answer rather than re-deriving them.
224
+ const tableNames = new Set(schema.tables.map((t) => schema.qualify(t)));
225
+ const viewNames = new Set(schema.views.map((v) => schema.qualify(v)));
226
+ // The objects backed by a TABLE in the snapshot — the enum section's scope, since
227
+ // a row there is read as a statement about a column.
228
+ const tableBacked: MetaObject[] = [];
229
+ // column → declaring field, per qualified table name. `resolveObjectNames` is the
230
+ // ONE field→column resolver — the same one the names artifact and the DDL use —
231
+ // so this mapping cannot disagree with the column it labels.
232
+ const declaredBy = new Map<string, Map<string, { field: string; type: string }>>();
233
+ const viewLineage = new Map<string, string[]>();
234
+ for (const obj of objects) {
235
+ const names = resolveObjectNames(obj, opts.columnNamingStrategy);
236
+ // The PRIMARY source's physical name and schema — `names.name` is the object's
237
+ // metamodel name since 0.25.0, which would match nothing in the snapshot.
238
+ const primary = names?.sources[SOURCE_ROLE_PRIMARY];
239
+ const physical = primary === undefined
240
+ ? undefined
241
+ : primary[(PHYSICAL_NAME_ATTR_BY_KIND.get(primary.kind) ?? "") as keyof typeof primary];
242
+ if (typeof physical !== "string") continue;
243
+ const key = schema.qualify({ name: physical, schema: primary?.schema });
244
+ const isTable = tableNames.has(key);
245
+ if (!isTable && !viewNames.has(key)) continue;
246
+ // ABSTRACT BASES ARE EXCLUDED, and being in the snapshot does not exempt them:
247
+ // an abstract base that declares the `source.rdb` its concrete child inherits
248
+ // resolves to that child's table, so the qualified-name test admits it and the
249
+ // page printed the base's enum a second time under a different owner.
250
+ // `buildExpectedSchema`'s Pass 1 skips abstracts; this reads the same rule.
251
+ if (isTable && !isAbstract(obj)) tableBacked.push(obj);
252
+ let map = declaredBy.get(key);
253
+ if (map === undefined) {
254
+ map = new Map();
255
+ declaredBy.set(key, map);
256
+ }
257
+ const lineage: string[] = [];
258
+ for (const field of obj.fields()) {
259
+ const column = resolveColumnName(field, opts.columnNamingStrategy);
260
+ // A TPH base and its subtypes share one table: first writer wins, so the
261
+ // base's own column keeps its label rather than being relabelled by whichever
262
+ // subtype was walked last.
263
+ if (!map.has(column)) {
264
+ map.set(column, { field: field.name, type: `${field.type}.${field.subType}` });
265
+ }
266
+ const line = lineageOf(field);
267
+ if (line !== undefined) lineage.push(`| \`${column}\` | ${line} |`);
268
+ }
269
+ if (lineage.length > 0) viewLineage.set(key, lineage);
270
+ }
271
+ const content = renderAgentSchemaPage(schema, {
272
+ declaredBy,
273
+ viewLineage,
274
+ relationships: relationshipLines(objects),
275
+ enums: enumLines(tableBacked),
276
+ });
277
+ if (content !== "") files.push({ path: `${dir}/schema.md`, content });
278
+ }
279
+
280
+ // ---- ui.md
281
+ // The apiPrefix is the project's, from the render context the runner built — the
282
+ // same value `routes-file.ts` emits as the SERVER mount prefix. Without it the page
283
+ // names an address nothing serves. (The client descriptor no longer carries it: a
284
+ // browser's base URL is supplied at runtime by the provider's `baseUrl`.)
285
+ const ui = renderAgentUiPage(ctx.loadedRoot, ctx.renderContext?.apiPrefix ?? "");
286
+ if (ui !== "") files.push({ path: `${dir}/ui.md`, content: ui });
287
+
288
+ // ---- requirements.md
289
+ const requirements = renderAgentRequirementsPage(ctx.loadedRoot);
290
+ if (requirements !== "") files.push({ path: `${dir}/requirements.md`, content: requirements });
291
+
292
+ return files;
293
+ },
294
+ };
295
+ if (opts?.target) generator.target = opts.target;
296
+ return generator;
297
+ } as GeneratorFactory<AgentDocsFileOpts>;