@metaobjectsdev/codegen-ts 0.9.0-rc.1 → 0.10.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 (323) hide show
  1. package/README.md +1 -1
  2. package/dist/column-mapper.d.ts.map +1 -1
  3. package/dist/column-mapper.js +24 -8
  4. package/dist/column-mapper.js.map +1 -1
  5. package/dist/constants.d.ts +8 -0
  6. package/dist/constants.d.ts.map +1 -1
  7. package/dist/constants.js +16 -0
  8. package/dist/constants.js.map +1 -1
  9. package/dist/docs-paths.d.ts +58 -0
  10. package/dist/docs-paths.d.ts.map +1 -0
  11. package/dist/docs-paths.js +89 -0
  12. package/dist/docs-paths.js.map +1 -0
  13. package/dist/enum-import.d.ts +14 -0
  14. package/dist/enum-import.d.ts.map +1 -0
  15. package/dist/enum-import.js +35 -0
  16. package/dist/enum-import.js.map +1 -0
  17. package/dist/enum-shared.d.ts +32 -0
  18. package/dist/enum-shared.d.ts.map +1 -0
  19. package/dist/enum-shared.js +83 -0
  20. package/dist/enum-shared.js.map +1 -0
  21. package/dist/generator-registry.d.ts +22 -0
  22. package/dist/generator-registry.d.ts.map +1 -0
  23. package/dist/generator-registry.js +161 -0
  24. package/dist/generator-registry.js.map +1 -0
  25. package/dist/generator.d.ts +6 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/api-doc-render.d.ts +17 -0
  29. package/dist/generators/api-doc-render.d.ts.map +1 -0
  30. package/dist/generators/api-doc-render.js +431 -0
  31. package/dist/generators/api-doc-render.js.map +1 -0
  32. package/dist/generators/api-docs-file.d.ts +21 -0
  33. package/dist/generators/api-docs-file.d.ts.map +1 -0
  34. package/dist/generators/api-docs-file.js +112 -0
  35. package/dist/generators/api-docs-file.js.map +1 -0
  36. package/dist/generators/api-field-shape.d.ts +39 -0
  37. package/dist/generators/api-field-shape.d.ts.map +1 -0
  38. package/dist/generators/api-field-shape.js +92 -0
  39. package/dist/generators/api-field-shape.js.map +1 -0
  40. package/dist/generators/api-label.d.ts +3 -0
  41. package/dist/generators/api-label.d.ts.map +1 -0
  42. package/dist/generators/api-label.js +8 -0
  43. package/dist/generators/api-label.js.map +1 -0
  44. package/dist/generators/api-model.d.ts +122 -0
  45. package/dist/generators/api-model.d.ts.map +1 -0
  46. package/dist/generators/api-model.js +809 -0
  47. package/dist/generators/api-model.js.map +1 -0
  48. package/dist/generators/docs-data-builder.d.ts +26 -4
  49. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  50. package/dist/generators/docs-data-builder.js +436 -164
  51. package/dist/generators/docs-data-builder.js.map +1 -1
  52. package/dist/generators/docs-data.d.ts +136 -27
  53. package/dist/generators/docs-data.d.ts.map +1 -1
  54. package/dist/generators/docs-data.js +1 -1
  55. package/dist/generators/docs-data.js.map +1 -1
  56. package/dist/generators/docs-file.d.ts +19 -0
  57. package/dist/generators/docs-file.d.ts.map +1 -1
  58. package/dist/generators/docs-file.js +154 -27
  59. package/dist/generators/docs-file.js.map +1 -1
  60. package/dist/generators/entity-file.d.ts.map +1 -1
  61. package/dist/generators/entity-file.js +29 -14
  62. package/dist/generators/entity-file.js.map +1 -1
  63. package/dist/generators/extractor-file.d.ts.map +1 -1
  64. package/dist/generators/extractor-file.js +2 -1
  65. package/dist/generators/extractor-file.js.map +1 -1
  66. package/dist/generators/field-anchor.d.ts +7 -0
  67. package/dist/generators/field-anchor.d.ts.map +1 -0
  68. package/dist/generators/field-anchor.js +23 -0
  69. package/dist/generators/field-anchor.js.map +1 -0
  70. package/dist/generators/index.d.ts +8 -1
  71. package/dist/generators/index.d.ts.map +1 -1
  72. package/dist/generators/index.js +6 -0
  73. package/dist/generators/index.js.map +1 -1
  74. package/dist/generators/mermaid-er.d.ts +14 -0
  75. package/dist/generators/mermaid-er.d.ts.map +1 -1
  76. package/dist/generators/mermaid-er.js +14 -0
  77. package/dist/generators/mermaid-er.js.map +1 -1
  78. package/dist/generators/output-parser-file.d.ts.map +1 -1
  79. package/dist/generators/output-parser-file.js +3 -4
  80. package/dist/generators/output-parser-file.js.map +1 -1
  81. package/dist/generators/output-prompt-file.d.ts.map +1 -1
  82. package/dist/generators/output-prompt-file.js +2 -2
  83. package/dist/generators/output-prompt-file.js.map +1 -1
  84. package/dist/generators/prompt-render-file.d.ts.map +1 -1
  85. package/dist/generators/prompt-render-file.js +3 -4
  86. package/dist/generators/prompt-render-file.js.map +1 -1
  87. package/dist/generators/queries-file.d.ts.map +1 -1
  88. package/dist/generators/queries-file.js +8 -3
  89. package/dist/generators/queries-file.js.map +1 -1
  90. package/dist/generators/render-helper-file.d.ts.map +1 -1
  91. package/dist/generators/render-helper-file.js +2 -2
  92. package/dist/generators/render-helper-file.js.map +1 -1
  93. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  94. package/dist/generators/routes-file-hono.js +5 -1
  95. package/dist/generators/routes-file-hono.js.map +1 -1
  96. package/dist/generators/routes-file.d.ts +3 -0
  97. package/dist/generators/routes-file.d.ts.map +1 -1
  98. package/dist/generators/routes-file.js +6 -1
  99. package/dist/generators/routes-file.js.map +1 -1
  100. package/dist/generators/template-doc-builder.d.ts +19 -0
  101. package/dist/generators/template-doc-builder.d.ts.map +1 -0
  102. package/dist/generators/template-doc-builder.js +220 -0
  103. package/dist/generators/template-doc-builder.js.map +1 -0
  104. package/dist/generators/template-doc-data.d.ts +62 -0
  105. package/dist/generators/template-doc-data.d.ts.map +1 -0
  106. package/dist/generators/template-doc-data.js +16 -0
  107. package/dist/generators/template-doc-data.js.map +1 -0
  108. package/dist/generators/template-payload-tree.d.ts +15 -0
  109. package/dist/generators/template-payload-tree.d.ts.map +1 -0
  110. package/dist/generators/template-payload-tree.js +61 -0
  111. package/dist/generators/template-payload-tree.js.map +1 -0
  112. package/dist/generators/template-source-annotate.d.ts +74 -0
  113. package/dist/generators/template-source-annotate.d.ts.map +1 -0
  114. package/dist/generators/template-source-annotate.js +184 -0
  115. package/dist/generators/template-source-annotate.js.map +1 -0
  116. package/dist/generators/template-source-render.d.ts +24 -0
  117. package/dist/generators/template-source-render.d.ts.map +1 -0
  118. package/dist/generators/template-source-render.js +175 -0
  119. package/dist/generators/template-source-render.js.map +1 -0
  120. package/dist/generators/trace-helper-file.d.ts +9 -0
  121. package/dist/generators/trace-helper-file.d.ts.map +1 -0
  122. package/dist/generators/trace-helper-file.js +196 -0
  123. package/dist/generators/trace-helper-file.js.map +1 -0
  124. package/dist/index.d.ts +29 -4
  125. package/dist/index.d.ts.map +1 -1
  126. package/dist/index.js +28 -2
  127. package/dist/index.js.map +1 -1
  128. package/dist/metaobjects-config.d.ts +75 -2
  129. package/dist/metaobjects-config.d.ts.map +1 -1
  130. package/dist/metaobjects-config.js +43 -0
  131. package/dist/metaobjects-config.js.map +1 -1
  132. package/dist/naming.d.ts +19 -0
  133. package/dist/naming.d.ts.map +1 -1
  134. package/dist/naming.js +41 -0
  135. package/dist/naming.js.map +1 -1
  136. package/dist/payload-codegen.d.ts.map +1 -1
  137. package/dist/payload-codegen.js +12 -4
  138. package/dist/payload-codegen.js.map +1 -1
  139. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  140. package/dist/projection/extract-view-spec.js +51 -25
  141. package/dist/projection/extract-view-spec.js.map +1 -1
  142. package/dist/relation-resolver.d.ts +16 -0
  143. package/dist/relation-resolver.d.ts.map +1 -1
  144. package/dist/relation-resolver.js +82 -1
  145. package/dist/relation-resolver.js.map +1 -1
  146. package/dist/render-context.d.ts +4 -0
  147. package/dist/render-context.d.ts.map +1 -1
  148. package/dist/render-context.js.map +1 -1
  149. package/dist/render-engine/embedded-templates.generated.d.ts +2 -0
  150. package/dist/render-engine/embedded-templates.generated.d.ts.map +1 -0
  151. package/dist/render-engine/embedded-templates.generated.js +15 -0
  152. package/dist/render-engine/embedded-templates.generated.js.map +1 -0
  153. package/dist/render-engine/framework-provider.d.ts.map +1 -1
  154. package/dist/render-engine/framework-provider.js +26 -13
  155. package/dist/render-engine/framework-provider.js.map +1 -1
  156. package/dist/runner.d.ts.map +1 -1
  157. package/dist/runner.js +17 -0
  158. package/dist/runner.js.map +1 -1
  159. package/dist/templates/docs-file.d.ts +2 -6
  160. package/dist/templates/docs-file.d.ts.map +1 -1
  161. package/dist/templates/docs-file.js +2 -5
  162. package/dist/templates/docs-file.js.map +1 -1
  163. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  164. package/dist/templates/drizzle-schema.js +30 -2
  165. package/dist/templates/drizzle-schema.js.map +1 -1
  166. package/dist/templates/entity-constants.d.ts +7 -0
  167. package/dist/templates/entity-constants.d.ts.map +1 -1
  168. package/dist/templates/entity-constants.js +1 -1
  169. package/dist/templates/entity-constants.js.map +1 -1
  170. package/dist/templates/entity-file.d.ts.map +1 -1
  171. package/dist/templates/entity-file.js +16 -5
  172. package/dist/templates/entity-file.js.map +1 -1
  173. package/dist/templates/enums-file.d.ts +11 -0
  174. package/dist/templates/enums-file.d.ts.map +1 -0
  175. package/dist/templates/enums-file.js +44 -0
  176. package/dist/templates/enums-file.js.map +1 -0
  177. package/dist/templates/extract-delegate-emitter.d.ts.map +1 -1
  178. package/dist/templates/extract-delegate-emitter.js +5 -7
  179. package/dist/templates/extract-delegate-emitter.js.map +1 -1
  180. package/dist/templates/extract-schema-emitter.d.ts.map +1 -1
  181. package/dist/templates/extract-schema-emitter.js +5 -1
  182. package/dist/templates/extract-schema-emitter.js.map +1 -1
  183. package/dist/templates/extractor.d.ts.map +1 -1
  184. package/dist/templates/extractor.js +56 -39
  185. package/dist/templates/extractor.js.map +1 -1
  186. package/dist/templates/field-meta.d.ts.map +1 -1
  187. package/dist/templates/field-meta.js +1 -5
  188. package/dist/templates/field-meta.js.map +1 -1
  189. package/dist/templates/filter-allowlist.d.ts +7 -2
  190. package/dist/templates/filter-allowlist.d.ts.map +1 -1
  191. package/dist/templates/filter-allowlist.js +17 -9
  192. package/dist/templates/filter-allowlist.js.map +1 -1
  193. package/dist/templates/filter-type.d.ts +7 -1
  194. package/dist/templates/filter-type.d.ts.map +1 -1
  195. package/dist/templates/filter-type.js +9 -5
  196. package/dist/templates/filter-type.js.map +1 -1
  197. package/dist/templates/find-templates.d.ts +4 -0
  198. package/dist/templates/find-templates.d.ts.map +1 -0
  199. package/dist/templates/find-templates.js +15 -0
  200. package/dist/templates/find-templates.js.map +1 -0
  201. package/dist/templates/fr010-field-mapping.d.ts +2 -0
  202. package/dist/templates/fr010-field-mapping.d.ts.map +1 -1
  203. package/dist/templates/fr010-field-mapping.js +10 -6
  204. package/dist/templates/fr010-field-mapping.js.map +1 -1
  205. package/dist/templates/inferred-types.d.ts +44 -7
  206. package/dist/templates/inferred-types.d.ts.map +1 -1
  207. package/dist/templates/inferred-types.js +107 -16
  208. package/dist/templates/inferred-types.js.map +1 -1
  209. package/dist/templates/mermaid-er.d.ts +35 -2
  210. package/dist/templates/mermaid-er.d.ts.map +1 -1
  211. package/dist/templates/mermaid-er.js +174 -7
  212. package/dist/templates/mermaid-er.js.map +1 -1
  213. package/dist/templates/output-parser.d.ts.map +1 -1
  214. package/dist/templates/output-parser.js +30 -79
  215. package/dist/templates/output-parser.js.map +1 -1
  216. package/dist/templates/output-prompt.d.ts.map +1 -1
  217. package/dist/templates/output-prompt.js +2 -2
  218. package/dist/templates/output-prompt.js.map +1 -1
  219. package/dist/templates/queries-file.d.ts.map +1 -1
  220. package/dist/templates/queries-file.js +112 -4
  221. package/dist/templates/queries-file.js.map +1 -1
  222. package/dist/templates/queries.d.ts +5 -0
  223. package/dist/templates/queries.d.ts.map +1 -1
  224. package/dist/templates/queries.js +7 -7
  225. package/dist/templates/queries.js.map +1 -1
  226. package/dist/templates/recover-schema-emitter.d.ts +8 -0
  227. package/dist/templates/recover-schema-emitter.d.ts.map +1 -0
  228. package/dist/templates/recover-schema-emitter.js +64 -0
  229. package/dist/templates/recover-schema-emitter.js.map +1 -0
  230. package/dist/templates/relations-block.js +10 -0
  231. package/dist/templates/relations-block.js.map +1 -1
  232. package/dist/templates/render-helper.d.ts.map +1 -1
  233. package/dist/templates/render-helper.js +4 -4
  234. package/dist/templates/render-helper.js.map +1 -1
  235. package/dist/templates/routes-file.d.ts.map +1 -1
  236. package/dist/templates/routes-file.js +183 -6
  237. package/dist/templates/routes-file.js.map +1 -1
  238. package/dist/templates/tph-discriminator.d.ts +56 -0
  239. package/dist/templates/tph-discriminator.d.ts.map +1 -0
  240. package/dist/templates/tph-discriminator.js +180 -0
  241. package/dist/templates/tph-discriminator.js.map +1 -0
  242. package/dist/templates/value-object-file.d.ts +2 -1
  243. package/dist/templates/value-object-file.d.ts.map +1 -1
  244. package/dist/templates/value-object-file.js +32 -4
  245. package/dist/templates/value-object-file.js.map +1 -1
  246. package/dist/templates/zod-validators.d.ts +64 -1
  247. package/dist/templates/zod-validators.d.ts.map +1 -1
  248. package/dist/templates/zod-validators.js +181 -8
  249. package/dist/templates/zod-validators.js.map +1 -1
  250. package/package.json +103 -34
  251. package/src/column-mapper.ts +25 -8
  252. package/src/constants.ts +18 -0
  253. package/src/docs-paths.ts +128 -0
  254. package/src/enum-import.ts +43 -0
  255. package/src/enum-shared.ts +95 -0
  256. package/src/generator-registry.ts +204 -0
  257. package/src/generator.ts +6 -0
  258. package/src/generators/api-doc-render.ts +572 -0
  259. package/src/generators/api-docs-file.ts +146 -0
  260. package/src/generators/api-field-shape.ts +114 -0
  261. package/src/generators/api-label.ts +7 -0
  262. package/src/generators/api-model.ts +1067 -0
  263. package/src/generators/docs-data-builder.ts +479 -185
  264. package/src/generators/docs-data.ts +139 -28
  265. package/src/generators/docs-file.ts +205 -39
  266. package/src/generators/entity-file.ts +31 -15
  267. package/src/generators/extractor-file.ts +2 -1
  268. package/src/generators/field-anchor.ts +24 -0
  269. package/src/generators/index.ts +8 -1
  270. package/src/generators/mermaid-er.ts +14 -0
  271. package/src/generators/output-parser-file.ts +3 -4
  272. package/src/generators/output-prompt-file.ts +2 -1
  273. package/src/generators/prompt-render-file.ts +3 -4
  274. package/src/generators/queries-file.ts +9 -3
  275. package/src/generators/render-helper-file.ts +2 -1
  276. package/src/generators/routes-file-hono.ts +5 -1
  277. package/src/generators/routes-file.ts +7 -1
  278. package/src/generators/template-doc-builder.ts +306 -0
  279. package/src/generators/template-doc-data.ts +85 -0
  280. package/src/generators/template-payload-tree.ts +71 -0
  281. package/src/generators/template-source-annotate.ts +290 -0
  282. package/src/generators/template-source-render.ts +203 -0
  283. package/src/generators/trace-helper-file.ts +301 -0
  284. package/src/index.ts +55 -4
  285. package/src/metaobjects-config.ts +117 -2
  286. package/src/naming.ts +48 -0
  287. package/src/payload-codegen.ts +14 -3
  288. package/src/projection/extract-view-spec.ts +49 -30
  289. package/src/relation-resolver.ts +103 -1
  290. package/src/render-context.ts +4 -0
  291. package/src/render-engine/embedded-templates.generated.ts +14 -0
  292. package/src/render-engine/framework-provider.ts +25 -11
  293. package/src/runner.ts +21 -0
  294. package/src/templates/docs-file.ts +2 -9
  295. package/src/templates/drizzle-schema.ts +31 -1
  296. package/src/templates/entity-constants.ts +1 -1
  297. package/src/templates/entity-file.ts +16 -5
  298. package/src/templates/enums-file.ts +50 -0
  299. package/src/templates/extract-delegate-emitter.ts +5 -6
  300. package/src/templates/extractor.ts +68 -38
  301. package/src/templates/field-meta.ts +0 -6
  302. package/src/templates/filter-allowlist.ts +17 -10
  303. package/src/templates/filter-type.ts +8 -6
  304. package/src/templates/find-templates.ts +15 -0
  305. package/src/templates/fr010-field-mapping.ts +10 -8
  306. package/src/templates/inferred-types.ts +108 -18
  307. package/src/templates/mermaid-er.ts +176 -8
  308. package/src/templates/output-parser.ts +30 -79
  309. package/src/templates/output-prompt.ts +2 -1
  310. package/src/templates/queries-file.ts +132 -3
  311. package/src/templates/queries.ts +15 -7
  312. package/src/templates/relations-block.ts +17 -0
  313. package/src/templates/render-helper.ts +4 -3
  314. package/src/templates/routes-file.ts +233 -6
  315. package/src/templates/tph-discriminator.ts +232 -0
  316. package/src/templates/value-object-file.ts +38 -4
  317. package/src/templates/zod-validators.ts +204 -7
  318. package/templates/api/agent-api.md.mustache +30 -0
  319. package/templates/api/entity-api.md.mustache +69 -0
  320. package/templates/api/index.md.mustache +21 -0
  321. package/templates/docs/entity-page.md.mustache +33 -21
  322. package/templates/docs/template-page.md.mustache +56 -0
  323. package/src/templates/extract-schema-emitter.ts +0 -111
@@ -10,7 +10,7 @@
10
10
  // downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
11
11
 
12
12
  import { code, joinCode, imp, type Code } from "ts-poet";
13
- import { MetaObject, MetaField } from "@metaobjectsdev/metadata";
13
+ import { MetaObject, MetaField, stripPackage } from "@metaobjectsdev/metadata";
14
14
  import {
15
15
  FIELD_SUBTYPE_STRING, FIELD_SUBTYPE_INT, FIELD_SUBTYPE_LONG, FIELD_SUBTYPE_CURRENCY,
16
16
  FIELD_SUBTYPE_BOOLEAN, FIELD_SUBTYPE_DOUBLE, FIELD_SUBTYPE_FLOAT,
@@ -24,9 +24,82 @@ import {
24
24
  AUTO_SET_ON_CREATE, AUTO_SET_ON_UPDATE,
25
25
  VALIDATOR_ATTR_MAX, VALIDATOR_ATTR_MIN, VALIDATOR_ATTR_PATTERN,
26
26
  GENERATION_INCREMENT, GENERATION_UUID,
27
+ OBJECT_ATTR_DISCRIMINATOR, OBJECT_ATTR_DISCRIMINATOR_VALUE,
27
28
  } from "@metaobjectsdev/metadata";
28
29
  import { enumValues, zodEnumExpr } from "../enum-meta.js";
29
30
  import { renderDocsFor } from "./jsdoc.js";
31
+ import { sharedEnumForField } from "../enum-shared.js";
32
+ import { sharedEnumImportSpecifier } from "../enum-import.js";
33
+ import { sharedEnumZodConstName } from "./enums-file.js";
34
+ import type { RenderContext } from "../render-context.js";
35
+
36
+ /**
37
+ * FR-017 Tier 1 — when this object is a TPH subtype (@discriminatorValue set
38
+ * and an ancestor carries @discriminator), return the discriminator-field-name
39
+ * → pinned-literal-value pair. Subtypes emit `<field>: z.literal("<value>")`
40
+ * instead of the inherited field's normal type expression. Returns undefined
41
+ * when the object is not a TPH subtype.
42
+ */
43
+ export function tphDiscriminatorPin(obj: MetaObject): { fieldName: string; value: string } | undefined {
44
+ const value = obj.ownAttr(OBJECT_ATTR_DISCRIMINATOR_VALUE);
45
+ if (typeof value !== "string" || value === "") return undefined;
46
+
47
+ // Walk the extends chain to find the root carrying @discriminator.
48
+ let cursor = obj.superResolved;
49
+ while (cursor !== undefined) {
50
+ const fieldName = cursor.ownAttr(OBJECT_ATTR_DISCRIMINATOR);
51
+ if (typeof fieldName === "string" && fieldName !== "") {
52
+ return { fieldName, value };
53
+ }
54
+ cursor = cursor.superResolved;
55
+ }
56
+ return undefined;
57
+ }
58
+
59
+ /** True when this object is a TPH subtype — it declares @discriminatorValue
60
+ * and an ancestor carries @discriminator. */
61
+ export function isTphSubtype(obj: MetaObject): boolean {
62
+ return tphDiscriminatorPin(obj) !== undefined;
63
+ }
64
+
65
+ /**
66
+ * FR-017 Tier 2 — the per-subtype FULL read schema `<Sub>Schema`. Unlike the
67
+ * insert schema, this includes every effective field (PK included) so a raw DB
68
+ * row parses through it. The discriminator field is pinned to its literal value
69
+ * (`type: z.literal("Bridge")`) so the schema rejects a row of another subtype.
70
+ *
71
+ * This is the schema parse<Base>(row) dispatches to (see tph-discriminator.ts).
72
+ * Non-required columns are `.nullable()`-tolerant: a nullable TPH column read
73
+ * back from the DB arrives as `null`, not `undefined`, so the read schema must
74
+ * accept null (the insert schema, by contrast, makes them `.optional()`).
75
+ */
76
+ export function renderTphSubtypeReadSchema(obj: MetaObject): Code {
77
+ const z = imp("z@zod");
78
+ const tphPin = tphDiscriminatorPin(obj);
79
+
80
+ const fieldLines: Code[] = [];
81
+ for (const child of obj.fields()) {
82
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
83
+ fieldLines.push(code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`);
84
+ continue;
85
+ }
86
+ const expr = zodFieldExpr(child);
87
+ // zodFieldExpr already appends `.optional()` for non-required fields; add
88
+ // `.nullable()` on top so a NULL column value (the TPH default for any
89
+ // subtype-only column) parses cleanly.
90
+ fieldLines.push(
91
+ fieldWillBeOptional(child) ? code` ${child.name}: ${expr}.nullable()` : code` ${child.name}: ${expr}`,
92
+ );
93
+ }
94
+
95
+ const docs = renderDocsFor(obj);
96
+ const docsPrefix = docs ? `${docs}\n` : "";
97
+ return code`
98
+ ${docsPrefix}export const ${obj.name}Schema = ${z}.object({
99
+ ${joinCode(fieldLines, { on: ",\n" })}
100
+ });
101
+ `;
102
+ }
30
103
 
31
104
  /** Auto-generated PK field names that should be omitted from InsertSchema. */
32
105
  function autoGenPkFieldNames(obj: MetaObject): Set<string> {
@@ -55,6 +128,7 @@ function autoGenPkFieldNames(obj: MetaObject): Set<string> {
55
128
  export function renderInsertSchemaOnly(obj: MetaObject): Code {
56
129
  const z = imp("z@zod");
57
130
  const autoGenPkFields = autoGenPkFieldNames(obj);
131
+ const tphPin = tphDiscriminatorPin(obj);
58
132
 
59
133
  const insertFieldLines: Code[] = [];
60
134
  for (const child of obj.fields()) {
@@ -64,6 +138,14 @@ export function renderInsertSchemaOnly(obj: MetaObject): Code {
64
138
  // create-shape schema entirely.
65
139
  if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
66
140
 
141
+ // FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
142
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
143
+ insertFieldLines.push(
144
+ code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
145
+ );
146
+ continue;
147
+ }
148
+
67
149
  const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
68
150
 
69
151
  if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
@@ -86,9 +168,90 @@ ${joinCode(insertFieldLines, { on: ",\n" })}
86
168
  `;
87
169
  }
88
170
 
89
- export function renderZodValidators(obj: MetaObject): Code {
171
+ /** One documented field in an Insert/Update schema's accepted shape. */
172
+ export interface SchemaFieldShape {
173
+ /** The field name (the schema property key). */
174
+ name: string;
175
+ /** Whether the property is optional in the schema (`.optional()` / omitted-OK). */
176
+ optional: boolean;
177
+ /** For the @discriminator field on a TPH subtype's InsertSchema: the pinned
178
+ * literal value (`z.literal("Bridge")`). Undefined otherwise. */
179
+ pinnedLiteral?: string;
180
+ /** True for @autoSet timestamp fields the schema fills server-side
181
+ * (`z.string().optional().transform(...)`). */
182
+ autoSet?: boolean;
183
+ }
184
+
185
+ /**
186
+ * The field SET (name + optionality) the `<Name>InsertSchema` accepts — derived
187
+ * by the SAME iteration + skip rules `renderInsertSchemaOnly` /
188
+ * `renderZodValidators` use to EMIT that schema, so a documented create-payload
189
+ * shape can never drift from the real schema:
190
+ * • auto-generated PK fields are omitted (caller doesn't provide them);
191
+ * • @readOnly fields are omitted (DB / replication owns the write path);
192
+ * • a TPH subtype's @discriminator field is a pinned `z.literal(value)`;
193
+ * • @autoSet fields are present but optional (server fills them);
194
+ * • every other field's optionality is `fieldWillBeOptional` (not required, or
195
+ * carries a @default).
196
+ */
197
+ export function insertSchemaFields(obj: MetaObject): SchemaFieldShape[] {
198
+ const autoGenPkFields = autoGenPkFieldNames(obj);
199
+ const tphPin = tphDiscriminatorPin(obj);
200
+ const out: SchemaFieldShape[] = [];
201
+ for (const child of obj.fields()) {
202
+ if (autoGenPkFields.has(child.name)) continue;
203
+ if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
204
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
205
+ out.push({ name: child.name, optional: false, pinnedLiteral: tphPin.value });
206
+ continue;
207
+ }
208
+ const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
209
+ if (autoSet === AUTO_SET_ON_CREATE || autoSet === AUTO_SET_ON_UPDATE) {
210
+ out.push({ name: child.name, optional: true, autoSet: true });
211
+ } else {
212
+ out.push({ name: child.name, optional: fieldWillBeOptional(child) });
213
+ }
214
+ }
215
+ return out;
216
+ }
217
+
218
+ /**
219
+ * The field SET the `<Name>UpdateSchema` accepts — same iteration + skip rules
220
+ * as `insertSchemaFields`, but mirroring the UpdateSchema branch of
221
+ * `renderZodValidators`:
222
+ * • a TPH subtype's @discriminator field is OMITTED (clients can't change subtype);
223
+ * • @autoSet onCreate fields are OMITTED (creation timestamps are immutable);
224
+ * • @autoSet onUpdate fields are present + optional (server fills them);
225
+ * • every other field is optional (PATCH semantics).
226
+ */
227
+ export function updateSchemaFields(obj: MetaObject): SchemaFieldShape[] {
228
+ const autoGenPkFields = autoGenPkFieldNames(obj);
229
+ const tphPin = tphDiscriminatorPin(obj);
230
+ const out: SchemaFieldShape[] = [];
231
+ for (const child of obj.fields()) {
232
+ if (autoGenPkFields.has(child.name)) continue;
233
+ if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
234
+ // TPH subtype discriminator: omitted from the update schema entirely.
235
+ if (tphPin !== undefined && child.name === tphPin.fieldName) continue;
236
+ const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
237
+ if (autoSet === AUTO_SET_ON_CREATE) {
238
+ // Omitted: creation timestamps cannot change after creation.
239
+ continue;
240
+ }
241
+ if (autoSet === AUTO_SET_ON_UPDATE) {
242
+ out.push({ name: child.name, optional: true, autoSet: true });
243
+ continue;
244
+ }
245
+ // All non-autoSet fields are optional in the update schema (PATCH semantics).
246
+ out.push({ name: child.name, optional: true });
247
+ }
248
+ return out;
249
+ }
250
+
251
+ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code {
90
252
  const z = imp("z@zod");
91
253
  const autoGenPkFields = autoGenPkFieldNames(obj);
254
+ const tphPin = tphDiscriminatorPin(obj);
92
255
 
93
256
  const insertFieldLines: Code[] = [];
94
257
  const updateFieldLines: Code[] = [];
@@ -100,6 +263,17 @@ export function renderZodValidators(obj: MetaObject): Code {
100
263
  // contract at the boundary with a 400 response).
101
264
  if (child.ownAttr(FIELD_ATTR_READ_ONLY) === true) continue;
102
265
 
266
+ // FR-017 Tier 1: TPH subtype pins its discriminator field to z.literal(...).
267
+ // The discriminator is implicit on subtype rows (controlled by URL / insert
268
+ // path) — the app never writes it via the body and never updates it.
269
+ // Insert: pinned literal. Update: omitted entirely (clients can't change subtype).
270
+ if (tphPin !== undefined && child.name === tphPin.fieldName) {
271
+ insertFieldLines.push(
272
+ code` ${child.name}: z.literal(${JSON.stringify(tphPin.value)})`,
273
+ );
274
+ continue;
275
+ }
276
+
103
277
  const autoSet = child.ownAttr(FIELD_ATTR_AUTO_SET);
104
278
 
105
279
  // Insert schema: @autoSet fields use transform (always override client input).
@@ -108,7 +282,7 @@ export function renderZodValidators(obj: MetaObject): Code {
108
282
  code` ${child.name}: z.string().optional().transform(() => new Date().toISOString())`,
109
283
  );
110
284
  } else {
111
- insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child)}`);
285
+ insertFieldLines.push(code` ${child.name}: ${zodFieldExpr(child, obj, ctx)}`);
112
286
  }
113
287
 
114
288
  // Update schema: @autoSet onCreate → omit entirely; onUpdate → transform
@@ -122,7 +296,7 @@ export function renderZodValidators(obj: MetaObject): Code {
122
296
  // All non-autoSet fields are optional in the update schema (PATCH semantics).
123
297
  // zodFieldExpr already appends .optional() when the field is non-required
124
298
  // OR has a default; only append once more when it didn't.
125
- const baseExpr = zodFieldExpr(child);
299
+ const baseExpr = zodFieldExpr(child, obj, ctx);
126
300
  updateFieldLines.push(
127
301
  fieldWillBeOptional(child) ? code` ${child.name}: ${baseExpr}` : code` ${child.name}: ${baseExpr}.optional()`,
128
302
  );
@@ -146,7 +320,7 @@ ${joinCode(updateFieldLines, { on: ",\n" })}
146
320
  `;
147
321
  }
148
322
 
149
- function zodFieldExpr(field: MetaField): Code {
323
+ function zodFieldExpr(field: MetaField, owner?: MetaObject, ctx?: RenderContext): Code {
150
324
  // FIELD_SUBTYPE_OBJECT: emit z.array(<Ref>InsertSchema) / <Ref>InsertSchema
151
325
  // via an imp() so ts-poet hoists the cross-module import. Without this the
152
326
  // field used to collapse to z.string() / z.array(z.string()) and downstream
@@ -154,7 +328,10 @@ function zodFieldExpr(field: MetaField): Code {
154
328
  if (field.subType === FIELD_SUBTYPE_OBJECT) {
155
329
  const ref = field.ownAttr(FIELD_ATTR_OBJECT_REF);
156
330
  if (typeof ref === "string" && ref.length > 0) {
157
- const refImp = imp(`${ref}InsertSchema@./${ref}.js`);
331
+ // @objectRef may be authored fully-qualified or bare — the referenced
332
+ // <Ref>InsertSchema + its sibling module use the BARE short name.
333
+ const refBase = stripPackage(ref);
334
+ const refImp = imp(`${refBase}InsertSchema@./${refBase}.js`);
158
335
  let base: Code = code`${refImp}`;
159
336
  if (field.isArray) base = code`z.array(${base})`;
160
337
  return appendValidatorChain(base, field);
@@ -187,7 +364,27 @@ function zodFieldExpr(field: MetaField): Code {
187
364
  break;
188
365
  case FIELD_SUBTYPE_ENUM: {
189
366
  const values = enumValues(field);
190
- baseStr = values !== undefined ? zodEnumExpr(values) : "z.string()";
367
+ if (values === undefined) {
368
+ baseStr = "z.string()";
369
+ break;
370
+ }
371
+ // FR-019: a field extending a MATERIALIZED root-level abstract enum uses the
372
+ // shared `<E>Enum` Zod const (imported from ./enums) instead of inlining
373
+ // z.enum([...]). A @provided enum keeps inline z.enum([...]) — validation
374
+ // stays metaobjects-owned (the @values SSOT); only the TS type is external.
375
+ // Inline enums (and bare-ctx unit-test calls) keep inlining as before.
376
+ if (ctx !== undefined) {
377
+ const shared = sharedEnumForField(field);
378
+ if (shared !== undefined && !shared.provided) {
379
+ const constName = sharedEnumZodConstName(shared.name);
380
+ const spec = sharedEnumImportSpecifier(ctx, owner?.package);
381
+ const sharedConst = imp(`${constName}@${spec}`);
382
+ let base: Code = code`${sharedConst}`;
383
+ if (field.isArray) base = code`z.array(${base})`;
384
+ return appendValidatorChain(base, field);
385
+ }
386
+ }
387
+ baseStr = zodEnumExpr(values);
191
388
  break;
192
389
  }
193
390
  case FIELD_SUBTYPE_STRING:
@@ -0,0 +1,30 @@
1
+ {{{generatedMarker}}}
2
+
3
+ # {{title}}
4
+
5
+ Generated API reference for {{project}}; call these exactly as written. {{importNote}}
6
+ {{#hasSetup}}
7
+
8
+ ## Setup
9
+ {{#setup}}
10
+ - `{{handle}}` — {{{note}}} `{{{snippetInline}}}`
11
+ {{/setup}}
12
+ {{/hasSetup}}
13
+ {{#units}}
14
+
15
+ ## {{node}}
16
+ {{#groups}}
17
+
18
+ `{{importHeader}}`
19
+ {{#symbols}}
20
+ - `{{signature}}` — {{usage}}{{#throwsMarker}} {{throwsMarker}}{{/throwsMarker}}
21
+ {{/symbols}}
22
+ {{/groups}}
23
+ {{#example}}
24
+
25
+ Example:
26
+ ```ts
27
+ {{{example}}}
28
+ ```
29
+ {{/example}}
30
+ {{/units}}
@@ -0,0 +1,69 @@
1
+ {{{generatedMarker}}}
2
+
3
+ # {{node}} API
4
+ {{#modelPageHref}}
5
+
6
+ **Model / metadata:** [{{node}}]({{modelPageHref}})
7
+ {{/modelPageHref}}
8
+
9
+ > Import paths are relative to your generated-output directory.
10
+ {{#hasSetup}}
11
+
12
+ ## Setup
13
+
14
+ Obtain the runtime handles the calls below need:
15
+ {{#setup}}
16
+
17
+ - `{{handle}}` — {{{note}}}
18
+
19
+ ```ts
20
+ {{{snippet}}}
21
+ ```
22
+ {{/setup}}
23
+ {{/hasSetup}}
24
+ {{#unitExample}}
25
+
26
+ ## Example
27
+
28
+ ```ts
29
+ {{{unitExample}}}
30
+ ```
31
+ {{/unitExample}}
32
+ {{#sections}}
33
+
34
+ ## {{heading}}
35
+ {{#symbols}}
36
+
37
+ ### `{{signature}}`
38
+
39
+ {{usage}}
40
+
41
+ ```ts
42
+ {{importLine}}
43
+ ```
44
+ {{#hasFields}}
45
+
46
+ {{fieldsCaption}}:
47
+
48
+ | Field | Type | Required | Notes |
49
+ |---|---|---|---|
50
+ {{#fieldRows}}
51
+ | `{{field}}` | `{{{type}}}` | {{required}} | {{notes}} |
52
+ {{/fieldRows}}
53
+ {{/hasFields}}
54
+ {{#mountNote}}
55
+
56
+ Mount: {{{mountNote}}}
57
+ {{/mountNote}}
58
+ {{#throws}}
59
+
60
+ Throws: {{throws}}
61
+ {{/throws}}
62
+ {{#example}}
63
+
64
+ ```ts
65
+ {{{example}}}
66
+ ```
67
+ {{/example}}
68
+ {{/symbols}}
69
+ {{/sections}}
@@ -0,0 +1,21 @@
1
+ {{{generatedMarker}}}
2
+
3
+ # {{title}}
4
+
5
+ {{intro}}
6
+ {{#hasEntities}}
7
+
8
+ ## Entities
9
+
10
+ {{#entities}}
11
+ - [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})
12
+ {{/entities}}
13
+ {{/hasEntities}}
14
+ {{#hasTemplates}}
15
+
16
+ ## Templates
17
+
18
+ {{#templates}}
19
+ - [{{node}}]({{href}}) — {{summary}} ({{symbolCount}} symbol{{^one}}s{{/one}})
20
+ {{/templates}}
21
+ {{/hasTemplates}}
@@ -1,21 +1,20 @@
1
1
  {{{generatedMarker}}}
2
2
 
3
3
  # {{entity.name}}
4
+ {{#summaryLead}}
5
+
6
+ {{{.}}}
7
+ {{/summaryLead}}
4
8
  {{#descriptionQuote}}
5
9
 
6
10
  {{{.}}}
7
11
  {{/descriptionQuote}}
12
+ {{#apiRefs.0}}
8
13
 
9
- {{{preambleHeader}}}
10
- {{#hasStorage}}
11
-
12
- ## Storage
14
+ **API reference:** {{/apiRefs.0}}{{#apiRefs}}[{{label}}]({{href}}){{^last}} · {{/last}}{{/apiRefs}}{{#apiRefs.0}}
15
+ {{/apiRefs.0}}
13
16
 
14
- {{{storage.tableHeader}}}
15
- {{#storage.rows}}
16
- {{{rowLine}}}
17
- {{/storage.rows}}
18
- {{/hasStorage}}
17
+ {{{preambleHeader}}}
19
18
  {{#hasIdentities}}
20
19
 
21
20
  ## Identity
@@ -24,6 +23,31 @@
24
23
  - {{{bullet}}}
25
24
  {{/identities}}
26
25
  {{/hasIdentities}}
26
+ {{#hasNeighborhoodEr}}
27
+
28
+ ## In context
29
+
30
+ {{{neighborhoodErBlock}}}
31
+ {{/hasNeighborhoodEr}}
32
+ {{#fields.hasFields}}
33
+
34
+ ## Fields
35
+
36
+ | Field | Type | Required | Column | Rules |
37
+ |---|---|---|---|---|
38
+ {{#fields.rows}}
39
+ | {{{fieldCell}}} | {{{typeCell}}} | {{requiredCell}} | {{{storageCell}}} | {{{rulesCell}}} |
40
+ {{/fields.rows}}
41
+ {{/fields.hasFields}}
42
+ {{#fieldDetails.hasDetails}}
43
+
44
+ ## Field details
45
+
46
+ {{#fieldDetails.rows}}
47
+ {{{block}}}
48
+
49
+ {{/fieldDetails.rows}}
50
+ {{/fieldDetails.hasDetails}}
27
51
  {{#hasRelationships}}
28
52
 
29
53
  ## Relationships
@@ -32,12 +56,6 @@
32
56
  - {{{bullet}}}
33
57
  {{/relationships}}
34
58
  {{/hasRelationships}}
35
-
36
- ## Validation
37
-
38
- - `{{validation.insertSchema}}` (Zod) — for creating new {{validation.lower}}s.
39
- - `{{validation.updateSchema}}` (Zod) — for partial updates.
40
- - See `{{validation.entityFile}}` for the exported schemas.
41
59
  {{#hasUsedBy}}
42
60
 
43
61
  ## Used by
@@ -46,9 +64,3 @@
46
64
  - {{{bullet}}}
47
65
  {{/usedBy}}
48
66
  {{/hasUsedBy}}
49
-
50
- ## Generated code
51
-
52
- {{#generated}}
53
- - `{{filename}}` — {{{description}}}
54
- {{/generated}}
@@ -0,0 +1,56 @@
1
+ {{{generatedMarker}}}
2
+
3
+ # {{name}}
4
+ {{#descriptionQuote}}
5
+
6
+ {{{.}}}
7
+ {{/descriptionQuote}}
8
+
9
+ **Kind:** {{kind}}
10
+
11
+ ## Output
12
+ {{^isEmail}}
13
+
14
+ - Format: `{{format}}`
15
+ {{/isEmail}}
16
+ {{#isEmail}}
17
+
18
+ Multipart email — rendered as the following parts:
19
+
20
+ | Part | Source | Format | Escaping |
21
+ |---|---|---|---|
22
+ {{#parts}}
23
+ | {{label}} | `{{ref}}` | `{{format}}` | {{#escaped}}escaped{{/escaped}}{{^escaped}}raw{{/escaped}} |
24
+ {{/parts}}
25
+ {{/isEmail}}
26
+
27
+ ## Input
28
+
29
+ - Payload: [`{{payload.name}}`]({{payload.link}})
30
+ {{#hasRequiredTags}}
31
+ - Required fields:{{#requiredTags}} `{{.}}`{{/requiredTags}}
32
+ {{/hasRequiredTags}}
33
+
34
+ ## Render contract
35
+
36
+ - Every field referenced by the template is validated against the payload at generation time; an unknown field fails generation.
37
+ {{#maxChars}}
38
+ - Maximum length: {{.}} characters (rendering longer output fails).
39
+ {{/maxChars}}
40
+ {{#hasRequiredTags}}
41
+ - Required tags must be present:{{#requiredTags}} `{{.}}`{{/requiredTags}}
42
+ {{/hasRequiredTags}}
43
+
44
+ ## Source
45
+
46
+ {{#sourceRefs}}
47
+ - `{{.}}`
48
+ {{/sourceRefs}}
49
+ {{#templateSourceSection}}
50
+
51
+ {{{.}}}
52
+ {{/templateSourceSection}}
53
+
54
+ ## Capability
55
+
56
+ {{capability}}
@@ -1,111 +0,0 @@
1
- // server/typescript/packages/codegen-ts/src/templates/extract-schema-emitter.ts
2
- //
3
- // Turns a payload value-object into TS source fragments for the FR-010 extract codegen:
4
- // • schemaLiteral — a `extractSchema(Format.JSON, "root", [ … ])` baked descriptor
5
- // built from FieldSpec factories (scalar / enumField).
6
- // • mirrorInterface — an all-nullable mirror interface `<Payload>Extracted` (each
7
- // component `T | null`); extract returns this nullable twin rather
8
- // than the strict payload (same reasoning as the Java/C#/Kotlin ports).
9
- // • mirrorInitializer — `{ prop: asString(d, "prop"), … }` building the mirror from the
10
- // forgiving outcome map `d`.
11
- //
12
- // Mirrors the C# ExtractSchemaEmitter (adapted to TS syntax + the `| null` nullable mirror).
13
- // Bounded scope: scalar / enum / scalar-array. Nested object + array-of-enum deferred.
14
-
15
- import {
16
- type MetaData,
17
- FIELD_SUBTYPE_ENUM,
18
- FIELD_SUBTYPE_OBJECT,
19
- FIELD_ATTR_ENUM_ALIAS,
20
- } from "@metaobjectsdev/metadata";
21
- import {
22
- fields,
23
- isRequired,
24
- isArray,
25
- scalarKind,
26
- mirrorType,
27
- extractMapCall,
28
- enumValues,
29
- coerceDefault,
30
- defaultValue,
31
- resolveNormalize,
32
- jsonStringLiteral,
33
- stringArrayLiteral,
34
- propertiesMapLiteral,
35
- } from "./fr010-field-mapping.js";
36
- import { NORMALIZE_DEFAULT } from "@metaobjectsdev/metadata";
37
-
38
- /** Emit `extractSchema(Format.X, "rootName", [ … ])`. */
39
- export function schemaLiteral(vo: MetaData, format: string, rootName: string): string {
40
- const formatEnum = format.toLowerCase() === "xml" ? "Format.XML" : "Format.JSON";
41
- const specs = fields(vo).map((f) => fieldSpecLiteral(f, vo));
42
- return `extractSchema(${formatEnum}, ${jsonStringLiteral(rootName)}, [${specs.join(", ")}])`;
43
- }
44
-
45
- function fieldSpecLiteral(field: MetaData, owner: MetaData): string {
46
- const name = jsonStringLiteral(field.name);
47
- const required = isRequired(field);
48
-
49
- if (field.subType === FIELD_SUBTYPE_ENUM) {
50
- const valuesLit = stringArrayLiteral(enumValues(field));
51
- const aliasLit = propertiesMapLiteral(field.ownAttr(FIELD_ATTR_ENUM_ALIAS));
52
- // FR-011: extended enumField signature is (name, required, values, aliases,
53
- // coerceDefault?, normalize="strip", defaultValue?). Resolve the three new args
54
- // (field → object → "strip" for normalize) and emit only what's needed: keep the
55
- // back-compat 4-arg form when nothing is set, else emit the positional tail up to
56
- // the last meaningful arg.
57
- const cd = coerceDefault(field);
58
- const dv = defaultValue(field);
59
- const normalize = resolveNormalize(field, owner);
60
- // enumField() sets array:false; enum-array is a bounded deferral (parity with Java/C#).
61
- if (cd == null && dv == null && normalize === NORMALIZE_DEFAULT) {
62
- return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit})`;
63
- }
64
- const cdLit = cd == null ? "null" : jsonStringLiteral(cd);
65
- const normLit = jsonStringLiteral(normalize);
66
- if (dv == null) {
67
- return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit}, ${cdLit}, ${normLit})`;
68
- }
69
- return `enumField(${name}, ${required}, ${valuesLit}, ${aliasLit}, ${cdLit}, ${normLit}, ${jsonStringLiteral(dv)})`;
70
- }
71
-
72
- if (field.subType === FIELD_SUBTYPE_OBJECT) {
73
- // FR-010: nested extract deferred — treat as an opaque required/optional string slot.
74
- return `scalar(${name}, FieldKind.STRING, ${required}) /* FR-010: nested extract deferred */`;
75
- }
76
-
77
- const kind = scalarKind(field.subType) ?? "STRING";
78
- // Scalar-array: the scalar() factory only builds singular specs (array:false), so emit a
79
- // FieldSpec object literal with array:true. Tier-2 win over the Roslyn proof: the emitted
80
- // extract() actually populates the array at runtime (ExtractMap.asStringList).
81
- if (isArray(field)) {
82
- return (
83
- `{ name: ${name}, kind: FieldKind.${kind}, required: ${required}, array: true, ` +
84
- `enumValues: null, enumAlias: null, min: null, max: null, nested: null }`
85
- );
86
- }
87
- return `scalar(${name}, FieldKind.${kind}, ${required})`;
88
- }
89
-
90
- /** Emit the all-nullable mirror interface declaration. */
91
- export function mirrorInterface(vo: MetaData, interfaceName: string): string {
92
- const base = interfaceName.endsWith("Extracted")
93
- ? interfaceName.slice(0, -"Extracted".length)
94
- : interfaceName;
95
- const lines: string[] = [];
96
- lines.push(
97
- `/** Best-effort extracted twin of \`${base}\` — every field nullable (null where lost/malformed). */`,
98
- );
99
- lines.push(`export interface ${interfaceName} {`);
100
- for (const f of fields(vo)) {
101
- lines.push(` ${f.name}: ${mirrorType(f)};`);
102
- }
103
- lines.push("}");
104
- return lines.join("\n");
105
- }
106
-
107
- /** Emit `{ prop: asString(d, "prop"), … }` building the mirror from the forgiving map `d`. */
108
- export function mirrorInitializer(vo: MetaData): string {
109
- const assigns = fields(vo).map((f) => `${f.name}: ${extractMapCall(f)}`);
110
- return `{ ${assigns.join(", ")} }`;
111
- }