@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/names.ts ADDED
@@ -0,0 +1,630 @@
1
+ /**
2
+ * §A2/§A3 — the ONE place a data name is resolved for a generator run.
3
+ *
4
+ * Both the names artifact (namesFile) and the entity generator that consumes it call this,
5
+ * so the constant and the binding it describes cannot be produced by different resolvers or
6
+ * different arguments. That is the whole rule; a name computed twice is a name that can
7
+ * disagree with itself.
8
+ */
9
+ import {
10
+ primaryRdbSource,
11
+ resolveColumnName,
12
+ resolveIndexName,
13
+ resolveTableSchema,
14
+ isMetaObject,
15
+ isMetaSource,
16
+ PHYSICAL_NAME_ATTR_BY_KIND,
17
+ SOURCE_ATTR_SCHEMA,
18
+ SOURCE_ROLE_PRIMARY,
19
+ TYPE_INDEX,
20
+ TYPE_OBJECT,
21
+ TYPE_SOURCE,
22
+ type ColumnNamingStrategy,
23
+ type MetaObject,
24
+ type MetaSource,
25
+ } from "@metaobjectsdev/metadata";
26
+ import { code, imp, type Code } from "ts-poet";
27
+ import { crossEntitySpecifier } from "./import-path.js";
28
+ import type { RenderContext } from "./render-context.js";
29
+
30
+ export interface FieldNames { readonly name: string; readonly column: string; }
31
+
32
+ /**
33
+ * One `source.rdb` child, under the ROLE it plays.
34
+ *
35
+ * The physical name is carried under a key NAMED FOR THE KIND — `table`, `view`,
36
+ * `materializedView`, `proc`, `function` — and that key is not invented here: it is
37
+ * `PHYSICAL_NAME_ATTR_BY_KIND`, the metamodel's own FR-016/ADR-0018 alias map, the same
38
+ * one the canonical serializer rewrites through. So the artifact spells a physical name
39
+ * the way the metadata that declared it does.
40
+ *
41
+ * This is the half of the shape that earns the restructure. A single flat `name` held a
42
+ * table, a view and a stored procedure in the same run, told apart only by a sibling
43
+ * `kind`; under `as const`, `LedgerNames.sources.replica.table` is now a compile error,
44
+ * because that source is a view. The read site answers the question instead of the reader
45
+ * having to.
46
+ */
47
+ export interface SourceNames {
48
+ readonly type: string;
49
+ readonly subType: string;
50
+ /** The `@kind` value, defaulted per ADR-0007 Rule 3 — the discriminator for the alias below. */
51
+ readonly kind: string;
52
+ readonly schema?: string | undefined;
53
+ /** The physical name, under the alias for `kind`. Exactly one of these is present. */
54
+ readonly table?: string | undefined;
55
+ readonly view?: string | undefined;
56
+ readonly materializedView?: string | undefined;
57
+ readonly proc?: string | undefined;
58
+ readonly function?: string | undefined;
59
+ }
60
+
61
+ /**
62
+ * One `identity.*` or `index.*` child.
63
+ *
64
+ * `subType` is load-bearing rather than decorative: it is the ONLY thing distinguishing a
65
+ * unique alternate key from a non-unique lookup index, which is the whole reason ADR-0040
66
+ * put uniqueness in the type rather than in an attribute.
67
+ *
68
+ * `index` — the database name — is present only where a shared resolver produces it:
69
+ * `identity.secondary` and `index.lookup`, via `resolveIndexName`. It is deliberately
70
+ * ABSENT on `identity.primary`, because no such name exists to carry: migrate hardcodes
71
+ * `<table>_pkey` on Postgres, emits an unnamed PK on SQLite, and no port's codegen names a
72
+ * primary key at all. Carrying it would restate a migrate-only, dialect-conditional
73
+ * formula in an artifact whose entire promise is that a name is spelled once — the #293
74
+ * defect, re-created by the mechanism built to prevent it.
75
+ */
76
+ export interface KeyNames {
77
+ readonly type: string;
78
+ readonly subType: string;
79
+ readonly name: string;
80
+ /** The database index name. Present for `identity.secondary` and `index.lookup` only. */
81
+ readonly index?: string | undefined;
82
+ }
83
+
84
+ /** The object whose names artifact this one extends. */
85
+ export interface SuperNames {
86
+ readonly name: string;
87
+ readonly package?: string | undefined;
88
+ }
89
+
90
+ export interface ObjectNames {
91
+ /** The metamodel type — always `object`. */
92
+ readonly type: string;
93
+ /** The metamodel subType — `entity` | `projection` | `value`. */
94
+ readonly subType: string;
95
+ /**
96
+ * The object's OWN name — `"Customer"`, not `"TBL_CUST_MASTER"`.
97
+ *
98
+ * It held the physical name until 0.25.0, which is the one change here that a hand-
99
+ * written consumer can adopt WITHOUT a compile error: `pgTable(CustomerNames.name, …)`
100
+ * still compiles and now binds a table called `Customer`. The release note leads with
101
+ * that; no gate here can see it, because the code that breaks is not generated.
102
+ */
103
+ readonly name: string;
104
+ /**
105
+ * Every `source.rdb` child, keyed by effective `@role` (`primary` | `replica`).
106
+ *
107
+ * Role is the honest axis: the loader requires exactly one primary, and every consumer
108
+ * that binds a second source picks it by role. Keying by role is also what finally gives
109
+ * a WRITE-THROUGH entity's replica view a home — it declares two physical names, the
110
+ * artifact carried one, and both TypeScript and C# emitted the second as a literal.
111
+ *
112
+ * Empty on a FRAGMENT: an abstract base with no source of its own contributes columns
113
+ * and must never acquire a physical name it never declared.
114
+ */
115
+ readonly sources: Readonly<Record<string, SourceNames>>;
116
+ /** The sources DECLARED HERE — what this artifact emits. See `ownFields`. */
117
+ readonly ownSources: Readonly<Record<string, SourceNames>>;
118
+ /**
119
+ * Every field, INHERITED INCLUDED. This is what a consumer looks a column up in, so a
120
+ * lookup for an inherited field must hit — miss and the caller falls back to a literal
121
+ * (see `columnExpr`), which is the whole defect this artifact exists to remove.
122
+ */
123
+ readonly fields: Readonly<Record<string, FieldNames>>;
124
+ /**
125
+ * The fields DECLARED HERE — what the artifact EMITS. Inherited ones are declared by
126
+ * the super's artifact and reached through it, so a subtype states each physical name
127
+ * once instead of restating its parent's.
128
+ *
129
+ * ADR-0039's ONE sanctioned own-accessor use, in the exact form the ADR names: codegen
130
+ * emitting a generated subclass, iterating own members so inherited ones are not
131
+ * re-emitted.
132
+ */
133
+ readonly ownFields: Readonly<Record<string, FieldNames>>;
134
+ /** Every `identity.*` child, inherited included; keyed by metamodel name. */
135
+ readonly identities: Readonly<Record<string, KeyNames>>;
136
+ /** The identities DECLARED HERE. See `ownFields`. */
137
+ readonly ownIdentities: Readonly<Record<string, KeyNames>>;
138
+ /** Every `index.*` child, inherited included; keyed by metamodel name. */
139
+ readonly indexes: Readonly<Record<string, KeyNames>>;
140
+ /** The indexes DECLARED HERE. See `ownFields`. */
141
+ readonly ownIndexes: Readonly<Record<string, KeyNames>>;
142
+ /** The nearest ancestor carrying an artifact of its own, when there is one. */
143
+ readonly superNames?: SuperNames | undefined;
144
+ /**
145
+ * True when the primary source is the SUPER's rather than declared here — a TPH
146
+ * subtype, which shares its base's single table. Structural (the two resolve to the
147
+ * SAME source node), never an equality test on the resolved strings: the physical
148
+ * name, kind and schema then all come from the super's artifact rather than being
149
+ * restated.
150
+ */
151
+ readonly inheritsSource: boolean;
152
+ }
153
+
154
+ /**
155
+ * One source node's names, keyed by the metamodel's own kind→alias map.
156
+ *
157
+ * `readOnly` is deliberately NOT carried, and its removal is the shape's own rule applied
158
+ * to itself: it is not metadata at all but a derivation over `@kind` (`source.isReadOnly()`),
159
+ * and a sweep of all five ports found ZERO consumers, generated or hand-written. An
160
+ * artifact that mirrors the metadata tree carries what was declared; a reader who wants
161
+ * read-only-ness asks `kind`, which is the thing the author actually wrote.
162
+ */
163
+ function sourceNamesOf(source: MetaSource): SourceNames {
164
+ const kind = source.effectiveKind;
165
+ // The metamodel's map, never a local switch: a sixth @kind must not need an edit here to
166
+ // be spelled correctly, and a local copy is a second answer to a question that has one.
167
+ const alias = PHYSICAL_NAME_ATTR_BY_KIND.get(kind);
168
+ const schema = source.attr(SOURCE_ATTR_SCHEMA);
169
+ return {
170
+ type: TYPE_SOURCE,
171
+ subType: source.subType,
172
+ kind,
173
+ ...(typeof schema === "string" && schema !== "" ? { schema } : {}),
174
+ ...(alias === undefined ? {} : { [alias]: source.physicalName }),
175
+ };
176
+ }
177
+
178
+ /** Every `source.rdb` child of `obj`, keyed by effective role. */
179
+ function sourcesOf(sources: readonly MetaSource[], where: string): Record<string, SourceNames> {
180
+ const out: Record<string, SourceNames> = {};
181
+ for (const src of sources) {
182
+ const role = src.role;
183
+ const resolved = sourceNamesOf(src);
184
+ const existing = out[role];
185
+ if (existing === undefined) {
186
+ out[role] = resolved;
187
+ continue;
188
+ }
189
+ // The refusal is about DISAGREEMENT, not about the count. An abstract base and the
190
+ // child that extends it may each declare a `@role: primary` source naming the same
191
+ // relation; that is legal and stays legal, because the records then compare equal.
192
+ //
193
+ // Two sources in one role that resolve DIFFERENTLY is the real problem, and silently
194
+ // keeping one is the `dropped` failure mode this artifact makes impossible: the second
195
+ // name is carried nowhere, read by nobody, and the binding quietly takes the first's.
196
+ //
197
+ // WHAT IS COMPARED, AND HOW IT DIFFERS FROM `primaryRdbSource`. This compares the whole
198
+ // resolved record — kind, schema and the physical name under its alias — for EVERY
199
+ // role. `primaryRdbSource` compares the bare physical name, and only for `primary`.
200
+ // This comment used to claim the two were "deliberately the SAME rule"; they are not,
201
+ // and the gap is reachable: two `@role: primary` sources agreeing on `@table` but
202
+ // disagreeing on `@schema` load with zero errors, are accepted by `primaryRdbSource`,
203
+ // and are refused here — so `meta gen` fails on a model every other door admits.
204
+ //
205
+ // The strict half is the defensible one: a schema is part of the address, so those two
206
+ // sources name DIFFERENT tables and "every consumer binds ONE name" is not satisfied
207
+ // merely because the unqualified names match. Loosening this to match would enshrine
208
+ // that. Tightening `primaryRdbSource` instead is the real fix and is deliberately NOT
209
+ // made here: it is the shared authority, implemented in all five ports, and a model
210
+ // that loads today would begin to throw. `names.test.ts` pins BOTH sides of the
211
+ // divergence so it stays visible until it is decided.
212
+ if (JSON.stringify(existing) !== JSON.stringify(resolved)) {
213
+ throw new Error(
214
+ `${where} declares more than one source.rdb with @role: "${role}", and they do ` +
215
+ `not agree: ${JSON.stringify(existing)} vs ${JSON.stringify(resolved)}. The names ` +
216
+ `artifact keys sources by role, so the second has nowhere to go.`,
217
+ );
218
+ }
219
+ }
220
+ return out;
221
+ }
222
+
223
+ /** Every `source.rdb` child of `obj`. `own` restricts to those declared here. */
224
+ function rdbSourcesOf(obj: MetaObject, own: boolean): MetaSource[] {
225
+ // ADR-0039: children() resolves through `extends`; ownChildren() is the sanctioned
226
+ // own-only twin for "what does THIS artifact declare".
227
+ const kids = own ? obj.ownChildren() : obj.children();
228
+ return kids.filter((c): c is MetaSource => isMetaSource(c));
229
+ }
230
+
231
+ /** Every `identity.*` / `index.*` child, keyed by metamodel name. */
232
+ function keysOf(
233
+ nodes: readonly { readonly name: string; readonly type: string; readonly subType: string }[],
234
+ ): Record<string, KeyNames> {
235
+ const out: Record<string, KeyNames> = {};
236
+ for (const node of nodes) {
237
+ // resolveIndexName owns BOTH the package strip and the empty-name refusal, so the
238
+ // artifact and the DDL cannot disagree about what an index is called — and an
239
+ // `index.lookup` with an empty name (which the loader accepts, unlike an identity)
240
+ // fails here instead of reaching an emitter.
241
+ const hasIndexName = INDEX_NAMED_SUBTYPES.has(`${node.type}.${node.subType}`);
242
+ out[node.name] = {
243
+ type: node.type,
244
+ subType: node.subType,
245
+ name: node.name,
246
+ ...(hasIndexName ? { index: resolveIndexName(node) } : {}),
247
+ };
248
+ }
249
+ return out;
250
+ }
251
+
252
+ /**
253
+ * The nodes whose database index name the artifact carries.
254
+ *
255
+ * A closed set rather than "anything with a name", because the rule is narrow and worth
256
+ * stating: the artifact carries a physical name only where ONE resolver, shared by codegen
257
+ * and migrate, produces it. `identity.primary` and `identity.reference` have names that are
258
+ * addressing handles, not database names — see {@link KeyNames}.
259
+ */
260
+ const INDEX_NAMED_SUBTYPES: ReadonlySet<string> = new Set([
261
+ "identity.secondary",
262
+ "index.lookup",
263
+ ]);
264
+
265
+ /**
266
+ * Whether `obj` DECLARES anything a names artifact carries.
267
+ *
268
+ * One predicate, because the artifact has four collections and the two places that ask
269
+ * this question must agree about all four. They used to ask about fields alone, and the
270
+ * cost was precise: an intermediate abstract declaring only an `identity.secondary` — a
271
+ * key hoisted onto a chain, which is the whole reason such a node exists — answered "no".
272
+ * {@link namesArtifactSuperOf} then walked past it and {@link resolveSuperFragmentNames}
273
+ * emitted nothing for it, so its key appeared in NEITHER the child's own set nor the
274
+ * grandparent's spread. `drizzle-schema.ts` still emitted `uniqueIndex(<E>Names.
275
+ * identities.<key>.index)` against it, and the generated code did not compile.
276
+ *
277
+ * ADR-0039's sanctioned own-accessor use: the question is what this node declares, not
278
+ * what it can see. An inherited key belongs to the ancestor that declared it, and is
279
+ * reached through that ancestor's artifact.
280
+ */
281
+ function declaresNamesContent(obj: MetaObject): boolean {
282
+ return obj.ownFields().length > 0 ||
283
+ obj.ownIdentities().length > 0 ||
284
+ obj.ownLookupIndexes().length > 0;
285
+ }
286
+
287
+ /**
288
+ * The nearest ancestor of `obj` that carries a names artifact of its own, or undefined.
289
+ *
290
+ * Walks past an ancestor with nothing to contribute — an abstract marker with no fields,
291
+ * no keys and no source emits no artifact, so there is nothing to extend and the search
292
+ * continues upward rather than stopping at a name that does not exist.
293
+ */
294
+ export function namesArtifactSuperOf(obj: MetaObject): MetaObject | undefined {
295
+ let cur = obj.superData;
296
+ while (cur !== undefined) {
297
+ // The exported guard, never `as MetaObject` and never a duck-type check on a method
298
+ // name: `superData` is only a MetaData, and CLAUDE.md makes this the required
299
+ // mechanism — two physical copies of the package in one process give a class object
300
+ // and an instance different identities, so `instanceof` returns false for a real node.
301
+ if (isMetaObject(cur) && (declaresNamesContent(cur) || primaryRdbSource(cur) !== undefined)) {
302
+ return cur;
303
+ }
304
+ cur = cur.superData;
305
+ }
306
+ return undefined;
307
+ }
308
+
309
+ export function resolveObjectNames(
310
+ obj: MetaObject,
311
+ strategy?: ColumnNamingStrategy,
312
+ ): ObjectNames | undefined {
313
+ // #248: an object participates in the database iff it declares (or inherits) a primary
314
+ // source. Never gate on the object subtype. ADR-0039: resolving children().
315
+ //
316
+ // primaryRdbSource, not a scan of our own: it is THE primary-source lookup for the whole
317
+ // toolchain, and it carries the divergence refusal that used to live in this function
318
+ // (see below). A second scan here would be a lookup written twice — the same defect one
319
+ // level down from the one this file exists to prevent (a NAME resolved twice).
320
+ const source = primaryRdbSource(obj);
321
+ if (source === undefined) return undefined;
322
+ const ownFieldList = obj.ownFields();
323
+
324
+ const fields: Record<string, FieldNames> = {};
325
+ // ADR-0039: fields() is the RESOLVING accessor — inherited fields must appear, and an
326
+ // inherited @column must resolve, or the constant disagrees with the DDL.
327
+ for (const f of obj.fields()) {
328
+ fields[f.name] = { name: f.name, column: resolveColumnName(f, strategy) };
329
+ }
330
+ // ADR-0039's sanctioned own-accessor use: what this artifact DECLARES. See ObjectNames.
331
+ const ownFields: Record<string, FieldNames> = {};
332
+ for (const f of ownFieldList) {
333
+ ownFields[f.name] = { name: f.name, column: resolveColumnName(f, strategy) };
334
+ }
335
+
336
+ const superObj = namesArtifactSuperOf(obj);
337
+ const superNames: SuperNames | undefined = superObj === undefined
338
+ ? undefined
339
+ : { name: superObj.name, package: superObj.package };
340
+ // Identity of the resolved source NODE, not equality of the resolved strings: a
341
+ // divergence guard is exactly what this codebase forbids here, and the question being
342
+ // asked is structural — did this object declare a source, or is it using its parent's?
343
+ const inheritsSource = source !== undefined && superObj !== undefined &&
344
+ primaryRdbSource(superObj) === source;
345
+
346
+
347
+ // Read the physical name off the primary SOURCE ALREADY IN HAND rather than calling
348
+ // resolveTableName() for it. Both now delegate to primaryRdbSource, so this is no longer
349
+ // about avoiding a second, differently-written lookup — it is that resolveTableName adds
350
+ // a no-source FALLBACK (pluralize(snake(name))) this function must not take: an object
351
+ // with no primary source returns undefined above, and must never acquire a table name it
352
+ // never declared. Mirrors the C# port (CSharpNaming.ResolveObjectNames).
353
+ const name = source.physicalName;
354
+
355
+ // The divergence refusal — an object whose @role: primary sources resolve to more than
356
+ // one physical name — used to live HERE, and that was the defect. Every consumer
357
+ // downstream references this name unconditionally (no per-site equality guard; see
358
+ // drizzle-schema.ts), but this function runs only when the `names` generator is in the
359
+ // run, so with namesFile() unwired nothing refused at all: `meta migrate` emitted DDL
360
+ // against the PARENT's table and ObjectManager read and wrote it, silently, on every
361
+ // run. A refusal that depends on which consumer asked is not a refusal.
362
+ //
363
+ // It now lives in primaryRdbSource (@metaobjectsdev/metadata's naming.ts), called
364
+ // above, so resolveTableName, resolveTableSchema, MetaObject.dbTable and this function
365
+ // all inherit it from one implementation. See that function's doc for the reachability
366
+ // analysis — the shape loads with ZERO errors — and for why the check must be
367
+ // DIRECTION-BLIND rather than comparing against the first primary WRITABLE source.
368
+ // names.test.ts pins both directions through this entry point; naming.test.ts (in
369
+ // @metaobjectsdev/metadata) pins the other three doors.
370
+
371
+ return {
372
+ type: TYPE_OBJECT,
373
+ subType: obj.subType,
374
+ // The object's OWN name. `source.physicalName` (resolved above as `name`) is now
375
+ // reached through `sources.<role>.<alias>`, which is the point of the restructure:
376
+ // one key stopped meaning a table, a view and a procedure depending on the object.
377
+ name: obj.name,
378
+ sources: sourcesOf(rdbSourcesOf(obj, false), obj.name),
379
+ ownSources: sourcesOf(rdbSourcesOf(obj, true), obj.name),
380
+ fields,
381
+ ownFields,
382
+ identities: keysOf(obj.identities()),
383
+ ownIdentities: keysOf(obj.ownIdentities()),
384
+ indexes: keysOf(obj.lookupIndexes()),
385
+ ownIndexes: keysOf(obj.ownLookupIndexes()),
386
+ superNames,
387
+ inheritsSource,
388
+ };
389
+ }
390
+
391
+ /**
392
+ * The names FRAGMENT for an object that a sourced object extends but which declares no
393
+ * source of its own — the `BaseEntity` pattern: shared fields, no table.
394
+ *
395
+ * Separate from {@link resolveObjectNames} on purpose, and the separation is the #248 rule
396
+ * intact rather than weakened. "Has a primary source" still decides whether an object is a
397
+ * database participant, so an `object.value` carrying fields resolves to nothing here as it
398
+ * always has. A fragment is emitted only for an object REACHED from a participant by
399
+ * walking `extends` upward — which is the only context in which its fields are columns at
400
+ * all. It carries no `kind`/`name`/`readOnly`, because it has no physical name and must
401
+ * never acquire one.
402
+ *
403
+ * Returns undefined when the object declares nothing of its own: an abstract marker has
404
+ * nothing to extend, and emitting an empty artifact for it would put a name in the import
405
+ * graph that says nothing. "Nothing" is {@link declaresNamesContent} — fields OR keys, the
406
+ * same question {@link namesArtifactSuperOf} asks, so the walk and the emit cannot disagree
407
+ * about which ancestors exist.
408
+ */
409
+ export function resolveSuperFragmentNames(
410
+ obj: MetaObject,
411
+ strategy?: ColumnNamingStrategy,
412
+ ): ObjectNames | undefined {
413
+ if (!declaresNamesContent(obj)) return undefined;
414
+ const ownFieldList = obj.ownFields();
415
+
416
+ const fields: Record<string, FieldNames> = {};
417
+ for (const f of obj.fields()) {
418
+ fields[f.name] = { name: f.name, column: resolveColumnName(f, strategy) };
419
+ }
420
+ const ownFields: Record<string, FieldNames> = {};
421
+ for (const f of ownFieldList) {
422
+ ownFields[f.name] = { name: f.name, column: resolveColumnName(f, strategy) };
423
+ }
424
+ const superObj = namesArtifactSuperOf(obj);
425
+ return {
426
+ type: TYPE_OBJECT,
427
+ subType: obj.subType,
428
+ name: obj.name,
429
+ // Empty, and emitted as empty rather than omitted: a child spreads `...Super.sources`
430
+ // unconditionally, so the key has to exist. A fragment declares no source and must
431
+ // never acquire a physical name it never wrote.
432
+ sources: {},
433
+ ownSources: {},
434
+ fields,
435
+ ownFields,
436
+ identities: keysOf(obj.identities()),
437
+ ownIdentities: keysOf(obj.ownIdentities()),
438
+ indexes: keysOf(obj.lookupIndexes()),
439
+ ownIndexes: keysOf(obj.ownLookupIndexes()),
440
+ superNames: superObj === undefined
441
+ ? undefined
442
+ : { name: superObj.name, package: superObj.package },
443
+ inheritsSource: false,
444
+ };
445
+ }
446
+
447
+ /**
448
+ * §B2 — the shared builder every `<Object>Names` reference call site used to hand-roll:
449
+ * check whether the artifact exists in this run (`ctx.includeNames`), resolve the
450
+ * constant (`resolveObjectNames`), and build the ts-poet import symbol pointing at
451
+ * `<Object>.names.ts`. Returns undefined whenever the artifact does not exist for this
452
+ * object in this run — a PRESENCE guard ("is the artifact in this run at all"), never the
453
+ * divergence refusal that `primaryRdbSource` owns: this function never compares a
454
+ * resolved value to a literal, it only asks whether the constant exists.
455
+ *
456
+ * `fromPackage` is the package of the FILE BEING EMITTED — the file that will hold the
457
+ * `import { <Object>Names } from …` line — and defaults to `obj.package`, correct for
458
+ * every site that emits `obj`'s OWN module (the entity generator referencing its own
459
+ * names artifact, same object on both ends). A caller emitting a DIFFERENT object's file
460
+ * and reaching across packages for `obj`'s names artifact — an M:N routes file, which
461
+ * lives in the SOURCE entity's package, importing the junction/target's `<X>Names` — MUST
462
+ * pass the emitting file's own package explicitly. Making this an explicit parameter
463
+ * (rather than assuming same-package the way a plain sibling specifier does) is what
464
+ * surfaces that choice: see routes-file.ts's `resolveJunctionColumn`, the one site that
465
+ * got it wrong by assuming same-package.
466
+ */
467
+ export function namesRef(
468
+ obj: MetaObject,
469
+ ctx: RenderContext,
470
+ fromPackage: string | undefined = obj.package,
471
+ ): { readonly resolved: ObjectNames; readonly symbol: Code } | undefined {
472
+ if (!ctx.includeNames) return undefined;
473
+ const resolved = resolveObjectNames(obj, ctx.columnNamingStrategy);
474
+ if (resolved === undefined) return undefined;
475
+ const symbol = code`${imp(
476
+ `${obj.name}Names@${crossEntitySpecifier(
477
+ ctx.selfTarget.outputLayout,
478
+ fromPackage,
479
+ obj.package,
480
+ `${obj.name}.names`,
481
+ ctx.extStyle,
482
+ )}`,
483
+ )}`;
484
+ return { resolved, symbol };
485
+ }
486
+
487
+ /**
488
+ * Adapts a `namesRef()` result to the `{ name, symbol }` shape `renderEntityConstants` /
489
+ * `renderEntityMetaFile` accept. Kept as a separate, narrower shape from `namesRef`'s own
490
+ * `{ resolved, symbol }` rather than folded together: those two functions' parameter
491
+ * predates this helper, and an adopter who already ejected the ADR-0034 reference
492
+ * template calls them with exactly this `{ name, symbol }` shape — widening the required
493
+ * shape to `resolved` would fail to compile in every such copy.
494
+ */
495
+ export function namesConstArg(
496
+ names: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined,
497
+ role: string = SOURCE_ROLE_PRIMARY,
498
+ ): { readonly name: string; readonly symbol: Code } | undefined {
499
+ // `resolved.name` is optional because a FRAGMENT (an abstract base contributing columns,
500
+ // with no source of its own) genuinely has no physical name. No caller reaches here with
501
+ // one — `namesRef` goes through `resolveObjectNames`, which requires a primary source —
502
+ // but returning undefined rather than asserting keeps the fragment case from acquiring a
503
+ // `$table` it never declared, which is the phantom-table failure #248 exists to prevent.
504
+ const src = names?.resolved.sources[role];
505
+ if (names === undefined || src === undefined) return undefined;
506
+ const alias = PHYSICAL_NAME_ATTR_BY_KIND.get(src.kind);
507
+ const physical = alias === undefined ? undefined : src[alias as keyof SourceNames];
508
+ // A fragment (an abstract base contributing columns, no source of its own) genuinely
509
+ // has no physical name. Returning undefined rather than asserting keeps it from
510
+ // acquiring a `$table` it never declared — the phantom-table failure #248 prevents.
511
+ if (typeof physical !== "string") return undefined;
512
+ return { name: physical, symbol: names.symbol };
513
+ }
514
+
515
+ /**
516
+ * A member access that stays valid whatever the key is: `.email` for an identifier,
517
+ * `["2fa-idx"]` otherwise.
518
+ *
519
+ * Needed because two of the artifact's four collections are keyed by an AUTHOR-CHOSEN
520
+ * name. Field keys are field names and are always identifiers; an index name is whatever
521
+ * the author wrote (`uq_cust_email` is an identifier, `2fa-idx` is not), so a dot access
522
+ * built from it does not parse. One helper rather than a per-site guess.
523
+ */
524
+ function member(key: string): string {
525
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? `.${key}` : `[${JSON.stringify(key)}]`;
526
+ }
527
+
528
+ /**
529
+ * The physical-name expression every §A6 `$table` / view-name site builds: the constant
530
+ * when the artifact is present, the literal otherwise. No equality guard — but the two
531
+ * branches earn that two different ways, and both are worth naming:
532
+ *
533
+ * - A TABLE literal (`drizzle-schema.ts`, `entity-constants.ts`) comes from
534
+ * `dbTable`/`resolveTableName`, which delegate to `primaryRdbSource` — the same lookup
535
+ * `resolveObjectNames` uses — so a disagreeing object has already thrown.
536
+ * - A VIEW literal (`projection-decl.ts`, `view-decl.ts`) comes from `viewName()`
537
+ * (`extract-view-spec.ts`), which reads OWN read-only sources only. That cannot diverge:
538
+ * a concrete projection may not inherit a source (`ERR_PROJECTION_INHERITED_SOURCE`) and
539
+ * the loader allows one own primary, so there is nothing for it to disagree with.
540
+ *
541
+ * Either way a reference here is the single spelling, never a lookalike computed twice.
542
+ *
543
+ * `role` selects WHICH source — the parameter that did not exist while the artifact held
544
+ * one name. A write-through entity declares two physical names; passing `replica` is how
545
+ * the read view stops being a literal in TypeScript and C# alike.
546
+ */
547
+ export function physicalNameExpr(
548
+ names: { readonly symbol: Code } | undefined,
549
+ literal: string,
550
+ obj: MetaObject,
551
+ role: string = SOURCE_ROLE_PRIMARY,
552
+ ): Code {
553
+ const alias = sourceAliasOf(obj, role);
554
+ // No alias means either no source in that role or a @kind carrying no physical-name
555
+ // slot. Falling back to the literal keeps a future @kind from emitting `undefined` into
556
+ // a table binding, which would fail at the database rather than at the compiler.
557
+ if (names === undefined || alias === undefined) return code`${JSON.stringify(literal)}`;
558
+ return code`${names.symbol}.sources${member(role)}.${alias}`;
559
+ }
560
+
561
+ /**
562
+ * The physical-name alias key for `obj`'s source in `role` — `table`, `view`, `proc`, …
563
+ *
564
+ * Derived from the object rather than taken as a parameter, so a call site cannot pass an
565
+ * alias that disagrees with the source it is describing. Reads the metamodel's own
566
+ * `PHYSICAL_NAME_ATTR_BY_KIND`; a local switch here would be a second answer to a
567
+ * question the metamodel already answers, which is the defect class this file exists for.
568
+ */
569
+ export function sourceAliasOf(obj: MetaObject, role: string = SOURCE_ROLE_PRIMARY): string | undefined {
570
+ const src = rdbSourcesOf(obj, false).find((c) => c.role === role);
571
+ return src === undefined ? undefined : PHYSICAL_NAME_ATTR_BY_KIND.get(src.effectiveKind);
572
+ }
573
+
574
+ /**
575
+ * A source's `@schema` expression, or undefined when the source declares none.
576
+ *
577
+ * Undefined rather than a literal fallback: an absent `@schema` means "the dialect's
578
+ * default", which a caller expresses by omitting the qualifier entirely — emitting `""`
579
+ * or `"public"` would be this artifact inventing a name the author never wrote.
580
+ */
581
+ export function sourceSchemaExpr(
582
+ names: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined,
583
+ role: string = SOURCE_ROLE_PRIMARY,
584
+ ): Code | undefined {
585
+ const src = names?.resolved.sources[role];
586
+ if (names === undefined || src === undefined || src.schema === undefined) return undefined;
587
+ return code`${names.symbol}.sources${member(role)}.schema`;
588
+ }
589
+
590
+ /**
591
+ * An index's database-name expression — the constant when the artifact carries the node,
592
+ * the literal otherwise.
593
+ *
594
+ * The literal arm goes through `resolveIndexName` rather than reading `node.name`, so the
595
+ * names-off path and the names-on path answer with the same function. That is the whole
596
+ * lesson of `fdb4118f1`: two spellings of one name agree until they do not.
597
+ */
598
+ export function indexNameExpr(
599
+ names: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined,
600
+ node: { readonly name: string; readonly type: string; readonly subType: string },
601
+ ): Code {
602
+ const collection = node.type === TYPE_INDEX ? "indexes" : "identities";
603
+ const entry = names?.resolved[collection][node.name];
604
+ return names !== undefined && entry !== undefined && entry.index !== undefined
605
+ ? code`${names.symbol}.${collection}${member(node.name)}.index`
606
+ : code`${JSON.stringify(resolveIndexName(node))}`;
607
+ }
608
+
609
+ /**
610
+ * A field's physical-column expression: the constant when the given artifact carries the
611
+ * field, the literal otherwise.
612
+ *
613
+ * The literal arm is a PRESENCE guard — "no artifact in this run", the documented ADR-0034
614
+ * opt-out — and NOT a divergence guard. It used to carry a second sanctioned case: a TPH
615
+ * fold emitting a subtype's own columns, which the BASE's artifact never saw. That was
616
+ * never a presence question; the constant existed the whole time, in the subtype's own
617
+ * artifact. Callers now resolve the declaring entity's ref before falling back
618
+ * (`drizzle-schema.ts`), so a miss no longer has a known-good explanation and the fallback
619
+ * is the last resort it was meant to be.
620
+ */
621
+ export function columnExpr(
622
+ names: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined,
623
+ fieldName: string,
624
+ literal: string,
625
+ ): Code {
626
+ const entry = names?.resolved.fields[fieldName];
627
+ return names !== undefined && entry !== undefined
628
+ ? code`${names.symbol}.fields.${fieldName}.column`
629
+ : code`${JSON.stringify(literal)}`;
630
+ }
package/src/naming.ts CHANGED
@@ -191,5 +191,28 @@ export function routesHandlerName(entityName: string): string {
191
191
  return `${entityName.charAt(0).toLowerCase()}${entityName.slice(1)}Routes`;
192
192
  }
193
193
 
194
+
195
+ /**
196
+ * The PascalCase base every generated symbol for a `template.*` node is built from —
197
+ * `render<Base>`, `parse<Base>`, `safeParse<Base>`, `<Base>Schema`, `<Base>Data`,
198
+ * `<Base>ValidationError`, `extractLenient<Base>WithLoader`.
199
+ *
200
+ * Kept here, and used by ALL of the template emitters, because they disagreed. Three
201
+ * generators named the same node three ways: `promptRender()`'s handle applied a private
202
+ * `pascal()` (`renderTriageTicket`), while `outputParser()` and the render-helper
203
+ * concatenated the raw name (`parsetriageTicket`, `type triageTicketData`). One `meta gen`
204
+ * run, one template, two conventions — and the skill's own examples use the lower-camel
205
+ * spelling that triggers it, so following the documentation is what produced it.
206
+ *
207
+ * An UpperCamel template name is unaffected (`pascal("TriageTicket") === "TriageTicket"`),
208
+ * so this only moves symbols for lower-camel names, which are exactly the ones that were
209
+ * spelled inconsistently.
210
+ */
211
+ export function templateSymbolBase(templateName: string): string {
212
+ return templateName.length > 0
213
+ ? templateName[0]!.toUpperCase() + templateName.slice(1)
214
+ : templateName;
215
+ }
216
+
194
217
  // Re-exported here for callers that import from codegen-ts's naming module.
195
218
  export { toKebabCase };