@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
@@ -23,6 +23,7 @@ import { renderEntityConstants } from "./entity-constants.js";
23
23
  import { renderFilterAllowlist, renderSortAllowlist } from "./filter-allowlist.js";
24
24
  import { renderFilterType } from "./filter-type.js";
25
25
  import { GENERATED_HEADER } from "../constants.js";
26
+ import { namesRef, namesConstArg } from "../names.js";
26
27
 
27
28
  export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: RenderContext): string {
28
29
  const enumAliases = renderEnumTypeAliases(obj, ctx);
@@ -34,7 +35,20 @@ export function renderValueObjectFile(obj: MetaObject, apiPrefix = "", ctx?: Ren
34
35
  // FR-017 Tier 3: a TPH subtype also emits its field-metadata constants object
35
36
  // (the `<Sub>` const), so the React form generator can render per-field
36
37
  // labels / rules / inputs the same way it does for ordinary entities.
37
- const tphConstants = tphSubtype ? renderEntityConstants(obj, apiPrefix) : null;
38
+ //
39
+ // §A6/§B2 — a TPH subtype declares no source.rdb of its own; it INHERITS the
40
+ // discriminator base's single shared table (`children()` is resolving — ADR-0039).
41
+ // `resolveObjectNames` therefore returns a DEFINED result for it (confirmed
42
+ // empirically: `name` is the BASE's physical table name, `fields` covers both the
43
+ // base's inherited columns and the subtype's own), and `namesFile()` really does emit
44
+ // a `<Sub>.names.ts` for it — this is not a case where the literal is correct and must
45
+ // stay. Same `namesRef` pair every other §A6 site builds; `ctx` is guaranteed present
46
+ // at every real call site (entity-file.ts and its reference/entity.ts mirror both
47
+ // always pass it) even though the parameter stays optional for a bare unit-test call.
48
+ const tphNames = tphSubtype && ctx !== undefined ? namesRef(obj, ctx) : undefined;
49
+ const tphConstants = tphSubtype
50
+ ? renderEntityConstants(obj, apiPrefix, namesConstArg(tphNames))
51
+ : null;
38
52
  // FR-017 Tier 3: per-subtype filter + sort allowlists, excluding the
39
53
  // discriminator (it's pinned by the per-subtype route path, so a client must
40
54
  // not filter on it). Included fields are the subtype's own + inherited base
@@ -14,6 +14,7 @@ import {
14
14
  import type { ColumnNamingStrategy } from "../metaobjects-config.js";
15
15
  import { mapColumnType } from "../column-mapper.js";
16
16
  import { zodTypeFor } from "./field-meta.js";
17
+ import { columnExpr, type ObjectNames } from "../names.js";
17
18
 
18
19
  export interface ViewDeclOpts {
19
20
  readonly dialect: "postgres" | "sqlite";
@@ -38,6 +39,31 @@ export interface ViewDeclOpts {
38
39
  * UpdateSchema, all of which already treat the PK as non-null.
39
40
  */
40
41
  readonly pkFieldNames: ReadonlySet<string>;
42
+ /**
43
+ * §A6 — the resolved names artifact supplying this declaration's COLUMN constants,
44
+ * plus the ts-poet symbol reference (`imp()`'d import of `<Object>Names`), when the
45
+ * names generator is in the run. Absent ⇒ every column stays a literal.
46
+ *
47
+ * Deliberately says nothing about the view's OWN physical name: the two answers come
48
+ * apart on a write-through entity, whose artifact holds the TABLE's name (its primary
49
+ * source) while this declaration binds the REPLICA view. Conflating them cost the
50
+ * write-through read model its column constants — it had to omit `names` entirely to
51
+ * stop the artifact's view name being emitted, and lost every
52
+ * `.fields.<f>.column` reference with it. The view's name is now the caller's to pass
53
+ * (see the `viewName` parameter), which is the only place that knows.
54
+ */
55
+ readonly names?: { readonly resolved: ObjectNames; readonly symbol: Code } | undefined;
56
+ /**
57
+ * The view's `@schema` — a bare name, or an EXPRESSION for it (a names constant).
58
+ *
59
+ * Passed in rather than derived from `names`, and for the same reason `viewName` is: a
60
+ * write-through entity's REPLICA view is a different source node from the one
61
+ * `<Entity>Names` resolves, so the artifact's `schema` there is the WRITE TABLE's and
62
+ * would be a confidently wrong answer. A caller whose artifact genuinely names this view
63
+ * passes the constant expression; the replica-view caller passes its own literal, which
64
+ * is consistent with that view's NAME already being a known literal for the same reason.
65
+ */
66
+ readonly schema?: string | Code | undefined;
41
67
  }
42
68
 
43
69
  /**
@@ -85,7 +111,10 @@ function viewColumnLine(f: MetaField, opts: ViewDeclOpts): Code {
85
111
  dollarType = code`.$type<Record<string, ${imp(`${vo.name}@${vo.module}`)}>>()`;
86
112
  }
87
113
  }
88
- return code` ${f.name}: ${colSym}(${JSON.stringify(spec.dbName)}${optsArg})${dollarType}${viewModifiers}`;
114
+ // A6 — reference the constant whenever the artifact is in the run AND carries this
115
+ // field. A lookup MISS is normal, not a divergence — see ViewDeclOpts.names.
116
+ const dbNameExpr = columnExpr(opts.names, f.name, spec.dbName);
117
+ return code` ${f.name}: ${colSym}(${dbNameExpr}${optsArg})${dollarType}${viewModifiers}`;
89
118
  }
90
119
 
91
120
  /**
@@ -95,7 +124,13 @@ function viewColumnLine(f: MetaField, opts: ViewDeclOpts): Code {
95
124
  */
96
125
  export function renderExistingViewDecl(
97
126
  fields: readonly MetaField[],
98
- viewName: string,
127
+ /**
128
+ * The view's own physical name: a plain string is emitted as a literal, a `Code` is
129
+ * emitted verbatim (a caller whose names artifact genuinely names THIS view passes
130
+ * `physicalNameExpr(names, viewName)`). See `ViewDeclOpts.names` for why this is not
131
+ * derived from that artifact here.
132
+ */
133
+ viewName: string | Code,
99
134
  viewVar: string,
100
135
  opts: ViewDeclOpts,
101
136
  ): Code {
@@ -103,11 +138,26 @@ export function renderExistingViewDecl(
103
138
  const viewModule = opts.dialect === "postgres" ? "drizzle-orm/pg-core" : "drizzle-orm/sqlite-core";
104
139
  const viewSym = imp(`${viewFn}@${viewModule}`);
105
140
  const viewColumnLines = fields.map((f) => viewColumnLine(f, opts));
141
+ const viewNameExpr = typeof viewName === "string" ? code`${JSON.stringify(viewName)}` : viewName;
142
+
143
+ // @schema — a view lands in a schema exactly as a table does, and migrate qualifies the
144
+ // `CREATE VIEW` it emits. Fixing the table binding and leaving this one would be the more
145
+ // dangerous half-job: the two would then disagree with each OTHER as well as with the
146
+ // database. Drizzle spells it with the same shape as the table case, `pgSchema(s).view(...)`
147
+ // — PgSchema carries `view` alongside `table`. sqlite is excluded for the same reason as
148
+ // there: no schema concept, and migrate refuses a non-default @schema on that dialect.
149
+ const viewSchemaExpr: Code | undefined =
150
+ opts.dialect !== "postgres" || opts.schema === undefined ? undefined
151
+ : typeof opts.schema === "string" ? code`${JSON.stringify(opts.schema)}`
152
+ : opts.schema;
153
+ const viewCall: Code = viewSchemaExpr === undefined
154
+ ? code`${viewSym}`
155
+ : code`${imp(`pgSchema@${viewModule}`)}(${viewSchemaExpr}).view`;
106
156
  return code`
107
157
  // View declaration — Drizzle uses this for typed SELECT queries.
108
158
  // The SQL view is created/managed by migrate-ts; .existing() tells Drizzle
109
159
  // not to attempt DDL for this declaration.
110
- export const ${viewVar} = ${viewSym}(${JSON.stringify(viewName)}, {
160
+ export const ${viewVar} = ${viewCall}(${viewNameExpr}, {
111
161
  ${joinCode(viewColumnLines, { on: ",\n" })}
112
162
  }).existing();
113
163
  `;
@@ -51,19 +51,55 @@ import { isRequired } from "../column-mapper.js";
51
51
  * when the object is not a TPH subtype.
52
52
  */
53
53
  export function tphDiscriminatorPin(obj: MetaObject): { fieldName: string; value: string } | undefined {
54
- // ADR-0039: own — super-resolution walk. A subtype declares its OWN
55
- // @discriminatorValue (must not inherit one), and the base level is found by
56
- // walking superResolved reading each level's OWN @discriminator.
54
+ // ADR-0039: own — a subtype declares its OWN @discriminatorValue (must not inherit
55
+ // one); the base level is found by the super-resolution walk in `tphDiscriminatorLevel`.
57
56
  const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
58
57
  if (typeof value !== "string" || value === "") return undefined;
58
+ const level = tphDiscriminatorLevel(obj);
59
+ if (level === undefined) return undefined;
60
+ return { fieldName: level.fieldName, value };
61
+ }
59
62
 
60
- // Walk the extends chain to find the root carrying @discriminator.
63
+ /**
64
+ * The discriminator BASE an object extends — the nearest ancestor declaring
65
+ * `@discriminator` — or undefined when there is none.
66
+ *
67
+ * The base is NOT the topmost ancestor: a multi-level hierarchy can extend further up
68
+ * past the level that owns the discriminator, and a route path composed against the
69
+ * wrong level names an endpoint nothing mounts. One walk, read by `tphDiscriminatorPin`
70
+ * and by `restPath` (api-surface.ts), so the pin and the address agree on which object
71
+ * is the base.
72
+ */
73
+ export function tphDiscriminatorBase(obj: MetaObject): MetaObject | undefined {
74
+ return tphDiscriminatorLevel(obj)?.base;
75
+ }
76
+
77
+ /**
78
+ * True when this object itself DECLARES `@discriminator` — the level of a TPH hierarchy
79
+ * that owns the discriminator, whether or not any concrete subtype extends it yet.
80
+ *
81
+ * ADR-0039: own — `@discriminator` identifies a hierarchy LEVEL, so it is read own: a
82
+ * subtype inheriting the attr through `extends` must not be mistaken for a base. This is
83
+ * the one predicate the form generator's filter (`hasGeneratedForm`) and the `agent/ui.md`
84
+ * page both ask, so "is this a discriminator base?" has one spelling.
85
+ */
86
+ export function declaresTphDiscriminator(obj: MetaObject): boolean {
87
+ return typeof obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR) === "string";
88
+ }
89
+
90
+ /**
91
+ * The ONE super-resolution walk behind `tphDiscriminatorBase` and `tphDiscriminatorPin`:
92
+ * the nearest ancestor declaring a non-empty `@discriminator`, with the field name it
93
+ * declares. Returning both from one walk is what keeps the pin's field name and the
94
+ * base's identity from ever being read at two different levels.
95
+ */
96
+ function tphDiscriminatorLevel(obj: MetaObject): { base: MetaObject; fieldName: string } | undefined {
61
97
  let cursor = obj.superResolved;
62
98
  while (cursor !== undefined) {
63
99
  // ADR-0039: own — super-resolution walk; read each level's OWN @discriminator.
64
100
  const fieldName = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
65
101
  if (typeof fieldName === "string" && fieldName !== "") {
66
- return { fieldName, value };
102
+ return { base: cursor as MetaObject, fieldName };
67
103
  }
68
104
  cursor = cursor.superResolved;
69
105
  }
@@ -0,0 +1,88 @@
1
+ // Selecting a field's view by the SURFACE that renders it (#356).
2
+ //
3
+ // A field may legally declare more than one `view.*` child and every one of them
4
+ // survives the load, so an emitter reading `field.views()[0]` let DECLARATION
5
+ // ORDER decide its output — and because several emitters read that same list,
6
+ // one declaration drove three unrelated surfaces at once. Declaring a
7
+ // `view.text` so a grid cell rendered as text silently degraded the generated
8
+ // FORM to an `<input>`, and swapping two lines of JSON with no semantic change
9
+ // flipped the form's `<select>` back. That is not a modelling mistake; it is the
10
+ // only possible outcome when one declaration serves three readers.
11
+ //
12
+ // The selector is the view's `name` — a reserved structural key already legal on
13
+ // every node — so this needs no new attribute, no provider and no
14
+ // `metamodelVersion` move (ADR-0037 step 0: the vocabulary existed, only the
15
+ // read was wrong). The same shape as #353's resolution.
16
+ //
17
+ // Two rules, and the second is the load-bearing one:
18
+ //
19
+ // 1. A field declaring ONE view keeps its exact current behaviour: that view
20
+ // applies to every surface, whatever it is named (or unnamed). Existing
21
+ // models are untouched by construction — a `name` is how a view is
22
+ // ADDRESSED by `extends` (ADR-0029 `Customer.priceCents.display`), and
23
+ // re-reading those names as surface names would break addressing.
24
+ //
25
+ // 2. A field declaring SEVERAL views must name one for the surface being
26
+ // rendered. No match is a hard error, not a fallback: falling back to
27
+ // `views()[0]` reinstates the positional read for exactly the multi-view
28
+ // case this exists to fix, and falling back to the inferred default turns a
29
+ // `name` typo ("forms") into a silently degraded control. A field cannot
30
+ // declare two views with the SAME name — the loader merges same-(type,
31
+ // name) siblings into one node — so a match is never ambiguous.
32
+
33
+ import type { MetaField, MetaView } from "@metaobjectsdev/metadata";
34
+ import { CodegenError } from "./errors.js";
35
+
36
+ /**
37
+ * The surface a generated artifact renders, and therefore the `name` its
38
+ * emitter looks for among a field's views.
39
+ *
40
+ * These are the only two surfaces the packaged generators render. An owned
41
+ * generator (FR-040) targeting a third may pass its own name — `viewForContext`
42
+ * takes any string — but it then owns telling its authors what to name.
43
+ */
44
+ export const VIEW_CONTEXT_FORM = "form";
45
+ /** Both grid tiers (TanStack and Angular) render the same surface, so both ask for this. */
46
+ export const VIEW_CONTEXT_GRID = "grid";
47
+
48
+ /** Render `view.dropdown name="form"` / `view.text (no name)` for a diagnostic. */
49
+ function describe(view: MetaView): string {
50
+ return view.name.length > 0
51
+ ? `view.${view.subType} name="${view.name}"`
52
+ : `view.${view.subType} (no name)`;
53
+ }
54
+
55
+ /** `Entity.field`, or just the field name when the field has no parent object. */
56
+ function fieldPath(field: MetaField): string {
57
+ const owner = field.parent?.name;
58
+ return owner !== undefined && owner.length > 0 ? `${owner}.${field.name}` : field.name;
59
+ }
60
+
61
+ /**
62
+ * The view a field declares for `context`, or `undefined` when it declares none
63
+ * at all (the caller's existing "no view" path — an inferred default — applies).
64
+ *
65
+ * Uses the RESOLVING accessor (ADR-0039): a view inherited through `extends` is
66
+ * as much the field's view as one declared on it.
67
+ *
68
+ * @throws CodegenError when the field declares several views and none is named
69
+ * for `context`. The remedy is in the message: name one of them.
70
+ */
71
+ export function viewForContext(field: MetaField, context: string): MetaView | undefined {
72
+ const declared = field.views();
73
+ // One view (or none) is unambiguous: it applies to every surface, exactly as
74
+ // it did before this selector existed.
75
+ if (declared.length <= 1) return declared[0];
76
+
77
+ const match = declared.find((v) => v.name === context);
78
+ if (match !== undefined) return match;
79
+
80
+ throw new CodegenError(
81
+ `Field "${fieldPath(field)}" declares ${declared.length} views ` +
82
+ `(${declared.map(describe).join(", ")}) and none is named "${context}", so codegen ` +
83
+ `cannot tell which one renders the ${context}. Name one of them "${context}" — a ` +
84
+ `view's \`name\` selects the surface it renders ("${VIEW_CONTEXT_FORM}" or ` +
85
+ `"${VIEW_CONTEXT_GRID}"). A field declaring a SINGLE view needs no name: that view ` +
86
+ `applies to every surface.`,
87
+ );
88
+ }