@metaobjectsdev/codegen-ts 0.9.0 → 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
@@ -0,0 +1,1067 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/api-model.ts
2
+ //
3
+ // ApiModel — an intermediate representation (IR) of the PUBLIC API surface an
4
+ // adopter's codegen produces from their metadata. The whole point is to be
5
+ // ACCURATE BY CONSTRUCTION: every symbol NAME here is derived by REUSING the
6
+ // real generators' own naming/signature logic (the same helpers the generators
7
+ // call when they emit code), never invented. The Task-4 accuracy gate runs the
8
+ // real generators and asserts each ApiModel symbol name actually appears in the
9
+ // generated output — so this builder must agree with them by construction.
10
+ //
11
+ // What it documents, per node:
12
+ // • ENTITY (object.entity, queryable):
13
+ // - model : the entity type/const (entity-file emits `<Name>`)
14
+ // - data-access : findById / list / create / update / deleteById helpers
15
+ // (templates/queries.ts — exact spellings via naming.ts)
16
+ // - rest : the 5 CRUD endpoints the routes generator mounts at the
17
+ // entity's $path (read-only set for projections)
18
+ // - validation : <Name>InsertSchema / <Name>UpdateSchema (zod-validators)
19
+ // • template.output:
20
+ // - extractor : extract<Name> / extractLenient<Name> (templates/extractor.ts)
21
+ // — ONLY when @format is json/xml (extractor generator gate)
22
+ // - render : render<Name> (templates/render-helper.ts) — document →
23
+ // string, email → EmailDocument (@kind gate)
24
+ // • ENTITY (additional, T5 — relationships / callable / Hono):
25
+ // - relation : the `<var>Relations` drizzle relations() export the
26
+ // entity file composes (relations-block.ts), one per entity
27
+ // that has relations; the per-navigation accessors (1:N
28
+ // one() / M:N many(junction)) ride in its field shape, named
29
+ // + cardinality-tagged + target-tagged. ONLY when the
30
+ // relation-resolver derives a relations() block for it.
31
+ // - callable : call<Entity> (templates/callable-file.ts) — ONLY when the
32
+ // entity is backed by a stored-proc / table-function source
33
+ // (isCallableEntity); the typed proc wrapper.
34
+ // - rest-hono : the Hono CRUD registrar register<Entity>Routes
35
+ // (templates/routes-file-hono.ts) — the OPT-IN Hono variant
36
+ // of the Fastify REST surface. Documented ONLY when the
37
+ // adopter wires routesFileHono() (ctx.includeHonoRoutes), and
38
+ // gated by the SAME @emitRoutes:false filter.
39
+ // • template.prompt (T5):
40
+ // - prompt : render<Name> (payload, provider): string — the prompt
41
+ // render handle promptRender() emits into a single
42
+ // aggregated `prompts.ts` (payload-codegen generateRenderHandle).
43
+ // ONLY for TOP-LEVEL template.prompt nodes (matching
44
+ // prompt-render-file.ts's `ctx.loadedRoot.ownChildren()`).
45
+ //
46
+ // DEFERRALS (tracked follow-ups — NOT documented by this builder yet, stated here
47
+ // so the gap is known + intentional):
48
+ // • TanStack / React generator surface — formFile / tanstackQuery / grid + hooks
49
+ // are framework ADD-ONS (a separate front-end-codegen effort); their emitted
50
+ // symbols are out of scope for the back-end public-API IR this builder models.
51
+ // • TPH BASE per-subtype write helpers (create<Sub> / update<Sub>ById /
52
+ // delete<Sub>ById scoped to the shared table) + the subtype REST subpaths —
53
+ // the prior deliberate deferral (see the TPH skip note below). Under-documented
54
+ // (allowed), never invented.
55
+ //
56
+ // SKIP rules honored (matching the real generators' filters):
57
+ // • object.value records have no primary identity → the queries generator skips
58
+ // them entirely (queries-file.ts `skipNonQueryable` = subType !== "value" &&
59
+ // !isTphSubtype), and they get no CRUD/routes/validation. So value objects
60
+ // contribute ONLY a model symbol here.
61
+ // • TPH subtypes (a @discriminatorValue under a @discriminator base) are ALSO
62
+ // skipped by the queries + routes generators (isTphSubtype, from
63
+ // templates/zod-validators.ts) — their query/route/validation surface lives
64
+ // in the discriminator BASE's polymorphic file, NOT their own. So a TPH
65
+ // subtype likewise contributes ONLY a model symbol here.
66
+ // The discriminator BASE itself stays queryable, but its data-access surface
67
+ // is REDUCED: the queries generator emits only the polymorphic reads
68
+ // find<Base>ById + list<Base>s on the base — create/update/delete are emitted
69
+ // PER CONCRETE SUBTYPE (create<Sub> …), since a base row can't be inserted
70
+ // without choosing a subtype. So the builder documents only those two reads
71
+ // (plus the base's validation schemas + base-path REST, which ARE emitted);
72
+ // documenting create<Base>/update<Base>/delete<Base>ById would be
73
+ // over-documentation (the Task-4 accuracy gate catches exactly that).
74
+ // DEFERRAL: the TPH BASE's per-subtype polymorphic write helpers (create<Sub>
75
+ // / update<Sub>ById / delete<Sub>ById scoped to the shared table) and the
76
+ // subtype REST subpaths are NOT YET documented by this builder — that fuller
77
+ // TPH modeling is a tracked follow-up (under-documentation, allowed).
78
+ // • @emitRoutes:false entities → the routes generator filters them out
79
+ // (routes-file.ts: ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false), so they get
80
+ // NO REST symbols here. The queries + validator generators do NOT honor
81
+ // @emitRoutes, so data-access + validation symbols still apply.
82
+
83
+ import {
84
+ type MetaRoot,
85
+ type MetaObject,
86
+ type MetaData,
87
+ OBJECT_SUBTYPE_VALUE,
88
+ TYPE_TEMPLATE,
89
+ TEMPLATE_SUBTYPE_OUTPUT,
90
+ TEMPLATE_SUBTYPE_PROMPT,
91
+ TEMPLATE_ATTR_PAYLOAD_REF,
92
+ TEMPLATE_ATTR_FORMAT,
93
+ TEMPLATE_ATTR_KIND,
94
+ TEMPLATE_KIND_EMAIL,
95
+ TEMPLATE_KIND_DEFAULT,
96
+ TYPE_SOURCE,
97
+ SOURCE_ATTR_PARAMETER_REF,
98
+ refMatchesObject,
99
+ } from "@metaobjectsdev/metadata";
100
+ import {
101
+ findByIdFnName,
102
+ listFnName,
103
+ createFnName,
104
+ updateFnName,
105
+ deleteByIdFnName,
106
+ routesHandlerName,
107
+ variableNameFromEntity,
108
+ } from "../naming.js";
109
+ import { getPkInfo } from "../templates/queries.js";
110
+ import { isTphSubtype } from "../templates/zod-validators.js";
111
+ import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
112
+ import { isCallableEntity } from "../templates/callable-file.js";
113
+ import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
114
+ import { resourcePath } from "../templates/entity-constants.js";
115
+ import { isProjection } from "../projection/projection-detector.js";
116
+ import { buildPkMap } from "../pk-resolver.js";
117
+ import { buildRelationMap, type RelationEntry, type RelationMap } from "../relation-resolver.js";
118
+ import { effectivePackage } from "../docs-paths.js";
119
+ import { entityOutputPath, type OutputLayout } from "../import-path.js";
120
+ import type { RenderContext } from "../render-context.js";
121
+ import type { PkInfo } from "../pk-resolver.js";
122
+ import {
123
+ modelFieldShapes,
124
+ createFieldShapes,
125
+ updateFieldShapes,
126
+ payloadFieldShapes,
127
+ type FieldShape,
128
+ } from "./api-field-shape.js";
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Public IR shape.
132
+ // ---------------------------------------------------------------------------
133
+
134
+ export type ApiSymbolKind =
135
+ | "model"
136
+ | "data-access"
137
+ | "rest"
138
+ | "validation"
139
+ | "extractor"
140
+ | "render"
141
+ // T5 additions:
142
+ | "relation" // the drizzle relations() export + per-nav accessors (1:N / M:N)
143
+ | "callable" // call<Entity> stored-proc / table-function wrapper
144
+ | "rest-hono" // the opt-in Hono CRUD registrar variant
145
+ | "prompt"; // render<Name> prompt-render handle for a template.prompt
146
+
147
+ export interface ApiSymbol {
148
+ /** The exact symbol the real generator emits (function/type/schema name, or
149
+ * a "METHOD /path" for a REST endpoint). Never invented. */
150
+ name: string;
151
+ kind: ApiSymbolKind;
152
+ /**
153
+ * The module specifier an adopter imports this symbol from — the generated
154
+ * file's path WITHOUT the `.ts` extension, exactly as the EMITTING generator
155
+ * writes it (so it can't drift): e.g. `Product.queries`, `Product`,
156
+ * `ProductSummary.extractor`, `ProductSummary.render`. Package layout folds
157
+ * entity-derived modules under the package path (`acme/shop/Product.queries`)
158
+ * iff the emitting generator does (it keys off the entity's OWN package).
159
+ *
160
+ * REST symbols are NOT importable functions — their importPath is the entity's
161
+ * routes MODULE; `registrar` carries the camelCase `<entity>Routes` handler an
162
+ * adopter mounts (`await <registrar>(fastify)`) to wire the endpoints.
163
+ */
164
+ importPath: string;
165
+ /** REST-only: the route-registrar function exported from `importPath` that an
166
+ * adopter mounts to wire the endpoints (`<entity>Routes`). Undefined for
167
+ * importable-symbol kinds (their `name` IS the import). */
168
+ registrar?: string;
169
+ /** A human-readable one-line signature (composed; the param/return SHAPE
170
+ * mirrors the generated code). For REST symbols this is "METHOD /path". */
171
+ signature: string;
172
+ /** Parameter descriptions, when meaningful. */
173
+ params?: string[];
174
+ /** Return-type description (e.g. "Product | null", "string", "EmailDocument"). */
175
+ returns?: string;
176
+ /** When/why the symbol throws, if it does. */
177
+ throws?: string;
178
+ /** One-line "what you use this for" prose. */
179
+ usage: string;
180
+ /** Optional usage example snippet. */
181
+ example?: string;
182
+ /**
183
+ * The field SHAPE this symbol's payload carries — name + TS type + optionality
184
+ * per field — so both renderers (human field table + agent inline shape) can
185
+ * show WHAT fields to pass, not just the type NAME. Accurate by construction:
186
+ * derived by REUSING the real generators' field walks (api-field-shape.ts), so
187
+ * the api-docs accuracy gate can assert the documented field set == the emitted
188
+ * one. Attached to:
189
+ * • model → the entity's inferred fields (model line / GET response);
190
+ * • create payload → the InsertSchema field set (create<Name> / POST body);
191
+ * • update payload → the UpdateSchema field set (update<Name> / PATCH body);
192
+ * • extractor payload → the @payloadRef VO interface (extract<Name>'s return).
193
+ * Undefined for symbols with no documented payload shape (e.g. deleteById, list).
194
+ */
195
+ fields?: FieldShape[];
196
+ }
197
+
198
+ export interface ApiUnitDoc {
199
+ /** The metadata node name (entity or template). */
200
+ node: string;
201
+ /** The node's EFFECTIVE package (own package OR the file-default captured at
202
+ * parse time), used to place the unit's doc page + compute collision-safe
203
+ * links to it in package layout. Undefined for a package-less node. */
204
+ package?: string | undefined;
205
+ nodeKind: "entity" | "template";
206
+ symbols: ApiSymbol[];
207
+ /**
208
+ * ONE worked, runnable example per unit — a concrete call site an agent (or a
209
+ * human) can copy. ACCURATE BY CONSTRUCTION: it is composed from the SAME
210
+ * symbol NAMES + importPaths this builder already documents (never invented)
211
+ * and the SAME field SHAPES (T2) attached to the unit's payload symbols, with
212
+ * example VALUES derived from each field's TS type (string→"…", number→1,
213
+ * enum→a real member, …) — not entity-hardcoded. The body lines (without
214
+ * imports) are what the agent form shows; the human page wraps the full block
215
+ * (imports + body) in a fenced ```ts.
216
+ *
217
+ * For an ENTITY: a create→find→update→delete flow over the entity's own
218
+ * CRUD helpers. For a TEMPLATE: an extract (parse LLM text) and/or render
219
+ * (produce the document) call. Undefined when a unit has no runnable surface
220
+ * (e.g. a bare value-object model with no queries/template).
221
+ */
222
+ example?: UnitExample;
223
+ }
224
+
225
+ /** A worked example for a unit: the imports it needs (one `import { … } from "…"`
226
+ * per module, in first-appearance order) + the body statements. Split so the
227
+ * human page can render a full fenced block and the agent form can show a tight
228
+ * body. */
229
+ export interface UnitExample {
230
+ /** `import { a, b } from "Mod"` lines, deduped by module, reusing the symbols'
231
+ * own importPaths (never re-derived). */
232
+ imports: string[];
233
+ /** The worked-flow statements (no imports), e.g.
234
+ * `const created = await createProduct(db, { name: "…" });`. */
235
+ body: string[];
236
+ }
237
+
238
+ export interface ApiModel {
239
+ units: ApiUnitDoc[];
240
+ }
241
+
242
+ /** Minimal context the builder needs. Accepts a full RenderContext OR just the
243
+ * loaded root (pkMap is derived when absent). Keeping it structural means the
244
+ * builder runs both inside a gen run and from a thin docs entrypoint. */
245
+ export interface ApiModelContext {
246
+ loadedRoot: MetaRoot;
247
+ pkMap?: Map<string, PkInfo>;
248
+ /** The relation map (relation-resolver) the entity file's relations() block is
249
+ * derived from. Derived from `loadedRoot` when absent — keeps the builder
250
+ * callable from a thin docs entrypoint. */
251
+ relationMap?: RelationMap;
252
+ /** The output layout the codegen run uses. The per-symbol `importPath` mirrors
253
+ * the emitting generator's own path computation under this layout (flat →
254
+ * `Product.queries`; package → folded under the entity's package path iff the
255
+ * generator folds). Defaults to "flat" (today's byte-identical placement). */
256
+ outputLayout?: OutputLayout;
257
+ /** Whether to ALSO document the OPT-IN Hono CRUD variant (routesFileHono).
258
+ * Hono is not in the default generator suite — it is an alternative wired by
259
+ * the adopter — so its symbols are documented ONLY when the adopter opts in
260
+ * (mirrors "match the generator's filters": don't over-document a surface the
261
+ * run didn't configure). The Fastify REST surface is always documented (it is
262
+ * the default-suite routes generator). Defaults to false. */
263
+ includeHonoRoutes?: boolean;
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Builder.
268
+ // ---------------------------------------------------------------------------
269
+
270
+ export function buildApiModel(root: MetaRoot, ctx: ApiModelContext): ApiModel {
271
+ const pkMap = ctx.pkMap ?? buildPkMap(root);
272
+ // getPkInfo wants a RenderContext; it only reads `.pkMap`, so a structural
273
+ // shim is sufficient (and avoids forcing callers to build a full context).
274
+ const pkCtx = { pkMap } as RenderContext;
275
+ const layout = ctx.outputLayout ?? "flat";
276
+ const relationMap = ctx.relationMap ?? buildRelationMap(root);
277
+ const includeHono = ctx.includeHonoRoutes ?? false;
278
+
279
+ const units: ApiUnitDoc[] = [];
280
+
281
+ for (const obj of root.objects()) {
282
+ units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono));
283
+ }
284
+
285
+ for (const tmpl of templateOutputs(root)) {
286
+ units.push(buildTemplateUnit(tmpl, root, layout));
287
+ }
288
+
289
+ for (const tmpl of templatePrompts(root)) {
290
+ units.push(buildPromptUnit(tmpl, root));
291
+ }
292
+
293
+ return { units };
294
+ }
295
+
296
+ // ---------------------------------------------------------------------------
297
+ // importPath derivation — the SINGLE place a documented symbol's import module
298
+ // is computed. It mirrors the EMITTING generator's own path logic exactly so a
299
+ // documented import can never drift from where the code actually lands:
300
+ //
301
+ // • entity / queries / routes files use
302
+ // entityOutputPath(layout, entity.package, "<Name>.<suffix>.ts")
303
+ // (queries-file.ts / entity-file.ts / routes-file.ts) — note they key off
304
+ // the entity's OWN bare `.package` (often undefined for objects, FR5d), so
305
+ // in package layout they only fold when the object actually carries a
306
+ // package. We pass the SAME `obj.package` here, not effectivePackage.
307
+ // • extractor / render-helper files emit a FLAT `<Name>.extractor.ts` /
308
+ // `<Name>.render.ts` regardless of layout (extractor-file.ts /
309
+ // render-helper-file.ts: `${t.name}.<suffix>.ts`, no package folding).
310
+ //
311
+ // The importPath is the emitted path WITHOUT the trailing `.ts`.
312
+ // ---------------------------------------------------------------------------
313
+
314
+ /** Extension-less module specifier for an entity-derived file
315
+ * (`<Name>` / `<Name>.queries` / `<Name>.routes`), folded by the SAME
316
+ * entityOutputPath logic the emitting generator uses. */
317
+ function entityModulePath(layout: OutputLayout, obj: MetaObject, basename: string): string {
318
+ return stripTs(entityOutputPath(layout, obj.package, `${basename}.ts`));
319
+ }
320
+
321
+ /** Extension-less module specifier for a template-derived file
322
+ * (`<Name>.extractor` / `<Name>.render`) — always flat (the generators do not
323
+ * fold these by package). */
324
+ function templateModulePath(basename: string): string {
325
+ return basename;
326
+ }
327
+
328
+ function stripTs(path: string): string {
329
+ return path.endsWith(".ts") ? path.slice(0, -3) : path;
330
+ }
331
+
332
+ // ---------------------------------------------------------------------------
333
+ // Entities.
334
+ // ---------------------------------------------------------------------------
335
+
336
+ /** Mirror of the queries generator's filter (queries-file.ts `skipNonQueryable`
337
+ * = `subType !== OBJECT_SUBTYPE_VALUE && !isTphSubtype`). A queryable entity is
338
+ * any non-value, non-TPH-subtype object:
339
+ * • Value objects have no primary identity → the queries/routes/validation
340
+ * generators emit no CRUD for them.
341
+ * • TPH subtypes (@discriminatorValue under a @discriminator base) emit no
342
+ * standalone queries/routes file — their surface lives in the discriminator
343
+ * BASE's polymorphic file (routes-file.ts:27 + queries-file.ts:21-22).
344
+ * Either way the object contributes only a model symbol here. (The TPH base's
345
+ * per-subtype polymorphic helpers + subpaths are a documented deferral — see
346
+ * the module header.) */
347
+ function isQueryable(obj: MetaObject): boolean {
348
+ return obj.subType !== OBJECT_SUBTYPE_VALUE && !isTphSubtype(obj);
349
+ }
350
+
351
+ /** Whether the routes generator emits REST routes for this entity. It filters
352
+ * out @emitRoutes:false (routes-file.ts:27), unlike the queries + validator
353
+ * generators which always emit. So REST symbols are gated separately from the
354
+ * other queryable kinds. */
355
+ function emitsRoutes(obj: MetaObject): boolean {
356
+ return obj.ownAttr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
357
+ }
358
+
359
+ function buildEntityUnit(
360
+ obj: MetaObject,
361
+ ctx: RenderContext,
362
+ root: MetaRoot,
363
+ layout: OutputLayout,
364
+ relationMap: RelationMap,
365
+ includeHono: boolean,
366
+ ): ApiUnitDoc {
367
+ const name = obj.name;
368
+ const symbols: ApiSymbol[] = [];
369
+
370
+ // The entity MODEL + its zod schemas are emitted into `<Name>.ts` (entity-file
371
+ // composes drizzle-schema + inferred-types + zod-validators), so model AND
372
+ // validation share the entity module's importPath.
373
+ const entityMod = entityModulePath(layout, obj, name);
374
+
375
+ // --- model: the entity type/const the entity-file generator emits (bare name). ---
376
+ symbols.push({
377
+ name,
378
+ kind: "model",
379
+ importPath: entityMod,
380
+ signature: `interface ${name}`,
381
+ returns: name,
382
+ usage: `The typed shape of a ${name} row, generated from its metadata.`,
383
+ fields: modelFieldShapes(obj),
384
+ });
385
+
386
+ if (isQueryable(obj)) {
387
+ symbols.push(...dataAccessSymbols(obj, ctx, root, layout));
388
+ symbols.push(...validationSymbols(obj, entityMod));
389
+ // REST is additionally gated: @emitRoutes:false suppresses routes only.
390
+ if (emitsRoutes(obj)) {
391
+ symbols.push(...restSymbols(obj, layout));
392
+ // The OPT-IN Hono variant mounts the SAME CRUD verbs under the SAME
393
+ // @emitRoutes filter — documented only when the adopter wired it.
394
+ if (includeHono) symbols.push(...restHonoSymbols(obj, layout));
395
+ }
396
+ }
397
+
398
+ // --- relation: the drizzle relations() export, when the resolver derives a
399
+ // relations() block for this entity (1:N belongs-to + inverse many, M:N
400
+ // @through). Independent of isQueryable — a relations() block is emitted by
401
+ // the entity file regardless. ---
402
+ const relationSym = relationSymbol(obj, entityMod, relationMap);
403
+ if (relationSym !== undefined) symbols.push(relationSym);
404
+
405
+ // --- callable: call<Entity>, only when the entity is backed by a stored-proc /
406
+ // table-function source (matching the callable generator's isCallableEntity
407
+ // filter). ---
408
+ const callableSym = callableSymbol(obj, root, layout);
409
+ if (callableSym !== undefined) symbols.push(callableSym);
410
+
411
+ const unit: ApiUnitDoc = {
412
+ node: name,
413
+ package: effectivePackage(obj),
414
+ nodeKind: "entity",
415
+ symbols,
416
+ };
417
+ // The worked example reads the row back by its REAL primary key (e.g.
418
+ // `created.code`), not a hard-coded `id` — reuse the same getPkInfo the
419
+ // data-access symbols were named from so the example is accurate-by-
420
+ // construction for any PK field name.
421
+ const pkName = getPkInfo(obj, ctx).fieldName;
422
+ const example = entityExample(name, pkName, symbols);
423
+ if (example !== undefined) unit.example = example;
424
+ return unit;
425
+ }
426
+
427
+ /** The CRUD helpers templates/queries.ts emits, named via the SHARED naming
428
+ * helpers the template itself uses (so the names cannot drift). The PK field +
429
+ * TS type come from the real getPkInfo.
430
+ *
431
+ * TPH discriminator BASE divergence: when `obj` is a discriminator base, the
432
+ * queries generator does NOT emit standalone create<Base>/update<Base>/
433
+ * delete<Base>ById — a base row can't be inserted without choosing a concrete
434
+ * subtype, so write helpers are emitted PER CONCRETE SUBTYPE (create<Sub> …),
435
+ * not on the base. The base file emits only the polymorphic reads find<Base>ById
436
+ * + list<Base>s. Documenting create<Base>/update<Base>/delete<Base>ById would be
437
+ * OVER-documentation (the api-docs accuracy gate catches exactly this). The
438
+ * per-subtype write helpers themselves are a tracked deferral (module header),
439
+ * so we under-document (allowed) rather than invent names. */
440
+ function dataAccessSymbols(
441
+ obj: MetaObject,
442
+ ctx: RenderContext,
443
+ root: MetaRoot,
444
+ layout: OutputLayout,
445
+ ): ApiSymbol[] {
446
+ const name = obj.name;
447
+ const { fieldName: pk, tsType: pkType } = getPkInfo(obj, ctx);
448
+
449
+ // All CRUD helpers are emitted into `<Name>.queries.ts` (queries-file.ts).
450
+ const mod = entityModulePath(layout, obj, `${name}.queries`);
451
+
452
+ const find = findByIdFnName(name);
453
+ const list = listFnName(name);
454
+ const create = createFnName(name);
455
+ const update = updateFnName(name);
456
+ const del = deleteByIdFnName(name);
457
+
458
+ const reads: ApiSymbol[] = [
459
+ {
460
+ name: find,
461
+ kind: "data-access",
462
+ importPath: mod,
463
+ signature: `${find}(db: Db, ${pk}: ${pkType}): Promise<${name} | null>`,
464
+ params: [`db: Db`, `${pk}: ${pkType}`],
465
+ returns: `Promise<${name} | null>`,
466
+ usage: `Fetch a single ${name} by its primary key; null when not found.`,
467
+ },
468
+ {
469
+ name: list,
470
+ kind: "data-access",
471
+ importPath: mod,
472
+ signature: `${list}(db: Db, opts?: { limit?: number; offset?: number }): Promise<${name}[]>`,
473
+ params: [`db: Db`, `opts?: { limit?: number; offset?: number }`],
474
+ returns: `Promise<${name}[]>`,
475
+ usage: `List ${name} rows with optional limit/offset paging.`,
476
+ },
477
+ ];
478
+
479
+ // A TPH discriminator base emits ONLY the polymorphic reads — the write
480
+ // helpers are per concrete subtype (create<Sub> …), not on the base.
481
+ if (isTphDiscriminatorBase(obj, root)) {
482
+ return reads;
483
+ }
484
+
485
+ // Create/update payload shapes — the EXACT InsertSchema/UpdateSchema field sets
486
+ // (api-field-shape reuses the zod emitter's own walk), so `data: unknown`'s
487
+ // real shape is documented + gate-verified against the emitted schema.
488
+ const createShape = createFieldShapes(obj);
489
+ const updateShape = updateFieldShapes(obj);
490
+
491
+ return [
492
+ ...reads,
493
+ {
494
+ name: create,
495
+ kind: "data-access",
496
+ importPath: mod,
497
+ signature: `${create}(db: Db, data: unknown): Promise<${name}>`,
498
+ params: [`db: Db`, `data: unknown`],
499
+ returns: `Promise<${name}>`,
500
+ throws: `ZodError when data fails ${name}InsertSchema validation.`,
501
+ usage: `Validate (via ${name}InsertSchema) and insert a new ${name}.`,
502
+ fields: createShape,
503
+ },
504
+ {
505
+ name: update,
506
+ kind: "data-access",
507
+ importPath: mod,
508
+ signature: `${update}(db: Db, ${pk}: ${pkType}, data: unknown): Promise<${name} | null>`,
509
+ params: [`db: Db`, `${pk}: ${pkType}`, `data: unknown`],
510
+ returns: `Promise<${name} | null>`,
511
+ throws: `ZodError when data fails the partial ${name}InsertSchema validation.`,
512
+ usage: `Partially update an existing ${name} by primary key; null when not found.`,
513
+ fields: updateShape,
514
+ },
515
+ {
516
+ name: del,
517
+ kind: "data-access",
518
+ importPath: mod,
519
+ signature: `${del}(db: Db, ${pk}: ${pkType}): Promise<boolean>`,
520
+ params: [`db: Db`, `${pk}: ${pkType}`],
521
+ returns: `Promise<boolean>`,
522
+ usage: `Delete a ${name} by primary key; true when a row was removed.`,
523
+ },
524
+ ];
525
+ }
526
+
527
+ /** The two zod schemas the validator generator emits per entity. The route +
528
+ * queries generators import these exact names (<Name>InsertSchema /
529
+ * <Name>UpdateSchema), so the spelling is verified against their usage. */
530
+ function validationSymbols(obj: MetaObject, entityMod: string): ApiSymbol[] {
531
+ const name = obj.name;
532
+ // The zod schemas are composed INTO the entity file (entity-file.ts calls
533
+ // renderZodValidators), so they import from the same `<Name>` module. The
534
+ // documented field shapes ARE those schemas' accepted shapes.
535
+ return [
536
+ {
537
+ name: `${name}InsertSchema`,
538
+ kind: "validation",
539
+ importPath: entityMod,
540
+ signature: `${name}InsertSchema: ZodType`,
541
+ returns: `ZodType`,
542
+ usage: `Zod schema validating the body of a create<${name}> / POST request (auto-generated PKs excluded).`,
543
+ fields: createFieldShapes(obj),
544
+ },
545
+ {
546
+ name: `${name}UpdateSchema`,
547
+ kind: "validation",
548
+ importPath: entityMod,
549
+ signature: `${name}UpdateSchema: ZodType`,
550
+ returns: `ZodType`,
551
+ usage: `Zod schema validating the body of an update / PATCH request (all fields optional).`,
552
+ fields: updateFieldShapes(obj),
553
+ },
554
+ ];
555
+ }
556
+
557
+ /**
558
+ * The REST endpoints the routes generator mounts for an entity. The routes
559
+ * generator does NOT emit one function per verb — it emits a single
560
+ * `<name>Routes(fastify)` handler that mounts the standard CRUD verb set at the
561
+ * entity's $path via mountCrudRoutes (or the read-only subset via
562
+ * mountReadOnlyCrudRoutes for a projection). We reuse resourcePath() — the same
563
+ * function entity-constants.ts uses to compute $path — so the documented paths
564
+ * match the generated routes exactly. The verb→path mapping mirrors the runtime
565
+ * mountCrudRoutes contract referenced in routes-file.ts's comments.
566
+ */
567
+ function restSymbols(obj: MetaObject, layout: OutputLayout): ApiSymbol[] {
568
+ const name = obj.name;
569
+ const path = resourcePath(obj);
570
+ const readOnly = isProjection(obj);
571
+
572
+ // REST endpoints are not importable functions — to WIRE them an adopter
573
+ // imports the entity's route registrar (`<entity>Routes`) from the routes
574
+ // module the routes generator emits (`<Name>.routes.ts`) and mounts it:
575
+ // import { <registrar> } from "<routesMod>"; await <registrar>(fastify);
576
+ // Every endpoint of one entity shares that single registrar import.
577
+ const routesMod = entityModulePath(layout, obj, `${name}.routes`);
578
+ const registrar = routesHandlerName(name);
579
+
580
+ // The REST bodies/responses ARE the same gate-verified shapes: a GET returns
581
+ // the model shape, POST takes the create shape, PATCH takes the update shape.
582
+ const modelShape = modelFieldShapes(obj);
583
+ const createShape = createFieldShapes(obj);
584
+ const updateShape = updateFieldShapes(obj);
585
+
586
+ const ep = restEndpointFactory("rest", routesMod, registrar);
587
+
588
+ const symbols: ApiSymbol[] = [
589
+ ep("GET", path, `List ${name} (supports filter/sort/paging query params).`, modelShape),
590
+ ep("GET", `${path}/:id`, `Fetch a single ${name} by id (404 when not found).`, modelShape),
591
+ ];
592
+
593
+ if (!readOnly) {
594
+ symbols.push(
595
+ ep("POST", path, `Create a ${name} (body validated by ${name}InsertSchema).`, createShape),
596
+ ep("PATCH", `${path}/:id`, `Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape),
597
+ ep("DELETE", `${path}/:id`, `Delete a ${name} by id.`),
598
+ );
599
+ }
600
+
601
+ return symbols;
602
+ }
603
+
604
+ /** Build a REST endpoint symbol factory bound to one route surface (kind +
605
+ * route module + registrar). The Fastify and Hono REST builders share this so a
606
+ * `METHOD /path` endpoint is shaped one way; only the surface-level kind/module/
607
+ * registrar and the per-endpoint description differ between them. */
608
+ function restEndpointFactory(
609
+ kind: ApiSymbolKind,
610
+ routesMod: string,
611
+ registrar: string,
612
+ ): (method: string, p: string, desc: string, fields?: FieldShape[]) => ApiSymbol {
613
+ return (method, p, desc, fields) => {
614
+ const sym: ApiSymbol = {
615
+ name: `${method} ${p}`,
616
+ kind,
617
+ importPath: routesMod,
618
+ registrar,
619
+ signature: `${method} ${p}`,
620
+ usage: desc,
621
+ };
622
+ if (fields !== undefined) sym.fields = fields;
623
+ return sym;
624
+ };
625
+ }
626
+
627
+ // ---------------------------------------------------------------------------
628
+ // T5: relations / callable / Hono (entity-level), then prompt (template.prompt).
629
+ // ---------------------------------------------------------------------------
630
+
631
+ /**
632
+ * The drizzle relations() export the entity file composes for an entity that has
633
+ * relations. The relations-block generator emits
634
+ * `export const <var>Relations = relations(<var>, ({ one, many }) => ({ … }))`
635
+ * where `<var> = variableNameFromEntity(name)` (so `Post` → `postRelations`) and
636
+ * the body is one accessor per RelationEntry the resolver derived — `author:
637
+ * one(User, …)` for a 1:N belongs-to, `tags: many(PostTag)` for an M:N @through,
638
+ * and the inverse `posts: many(…)` registered on the target. We document the
639
+ * EXPORT (the importable symbol) and ride each navigation in the field shape
640
+ * (name → cardinality-tagged target), all derived from the SAME RelationMap the
641
+ * generator emits from — never invented. Returns undefined when the resolver
642
+ * derived no relations() block for this entity (the entity file emits none).
643
+ */
644
+ function relationSymbol(
645
+ obj: MetaObject,
646
+ entityMod: string,
647
+ relationMap: RelationMap,
648
+ ): ApiSymbol | undefined {
649
+ const entries = relationMap.get(obj.name);
650
+ if (entries === undefined || entries.length === 0) return undefined;
651
+
652
+ const varName = variableNameFromEntity(obj.name);
653
+ const relationsExport = `${varName}Relations`;
654
+
655
+ // One field-shape row per navigation: name is the relation accessor key the
656
+ // block emits; type carries the cardinality + the entity you traverse to; note
657
+ // explains how to query it via the relational API.
658
+ const navFields: FieldShape[] = entries.map((e) => relationNavField(e));
659
+
660
+ return {
661
+ name: relationsExport,
662
+ kind: "relation",
663
+ importPath: entityMod,
664
+ signature: `const ${relationsExport}: Relations<"${tableName(varName)}", …>`,
665
+ returns: relationsExport,
666
+ usage:
667
+ `Drizzle relations() for ${obj.name} — register it with your schema, then ` +
668
+ `traverse via the relational query API (db.query.${varName}.findMany({ with: { … } })).`,
669
+ fields: navFields,
670
+ };
671
+ }
672
+
673
+ /** Drizzle's relations() first arg is the table var; we only need a stable label
674
+ * here for the signature, so reuse the entity's table var name. */
675
+ function tableName(varName: string): string {
676
+ return varName;
677
+ }
678
+
679
+ /** A field-shape row describing ONE relation navigation: accessor name + a
680
+ * cardinality-tagged target "type" + a how-to-traverse note. Mirrors the
681
+ * RelationEntry the resolver produced (1:N one() / M:N many(junction) / inverse
682
+ * many()), never restated. */
683
+ function relationNavField(e: RelationEntry): FieldShape {
684
+ if (e.cardinality === "one") {
685
+ return {
686
+ name: e.name,
687
+ type: `${e.targetEntity} (1:1 / N:1)`,
688
+ optional: true,
689
+ note: `belongs-to → ${e.targetEntity}${e.fkField ? ` via ${e.fkField}` : ""}`,
690
+ };
691
+ }
692
+ // many — either a M:N through a junction or a 1:N inverse.
693
+ if (e.junctionEntity !== undefined) {
694
+ return {
695
+ name: e.name,
696
+ type: `${e.targetEntity}[] (M:N via ${e.junctionEntity})`,
697
+ optional: true,
698
+ note: `many-to-many → ${e.targetEntity} through ${e.junctionEntity}`,
699
+ };
700
+ }
701
+ return {
702
+ name: e.name,
703
+ type: `${e.targetEntity}[] (1:N)`,
704
+ optional: true,
705
+ note: `has-many → ${e.targetEntity}`,
706
+ };
707
+ }
708
+
709
+ /**
710
+ * The callable wrapper `call<Entity>` the callable generator emits for an entity
711
+ * backed by a stored-proc / table-function source (isCallableEntity — the SAME
712
+ * filter the generator factory uses). The wrapper takes `(db, args: <argsVO>)`
713
+ * (or just `(db)` for a zero-arg proc) and returns `Promise<<Entity>[]>`, emitted
714
+ * into a FLAT-OR-PACKAGE-FOLDED `<Entity>.callable.ts` (entityOutputPath, same as
715
+ * the generator). Returns undefined for a non-callable entity (no file emitted).
716
+ */
717
+ function callableSymbol(
718
+ obj: MetaObject,
719
+ root: MetaRoot,
720
+ layout: OutputLayout,
721
+ ): ApiSymbol | undefined {
722
+ if (!isCallableEntity(obj)) return undefined;
723
+
724
+ const name = obj.name;
725
+ const fn = `call${name}`;
726
+ const mod = entityModulePath(layout, obj, `${name}.callable`);
727
+
728
+ // Resolve the @parameterRef args value-object name (same resolution the
729
+ // callable template uses) to type the `args` param — undefined ⇒ zero-arg proc.
730
+ const argsRef = callableArgsRef(obj, root);
731
+ const signature = argsRef
732
+ ? `${fn}(db: NodePgDatabase, args: ${argsRef}): Promise<${name}[]>`
733
+ : `${fn}(db: NodePgDatabase): Promise<${name}[]>`;
734
+ const params = argsRef
735
+ ? [`db: NodePgDatabase`, `args: ${argsRef}`]
736
+ : [`db: NodePgDatabase`];
737
+
738
+ return {
739
+ name: fn,
740
+ kind: "callable",
741
+ importPath: mod,
742
+ signature,
743
+ params,
744
+ returns: `${name}[]`,
745
+ usage: `Call the ${name} stored procedure / table function and parse each row into a typed ${name}.`,
746
+ };
747
+ }
748
+
749
+ /** The @parameterRef value-object name for a callable entity's source, or
750
+ * undefined for a zero-arg proc. Mirrors the callable template's resolution
751
+ * (the source child's SOURCE_ATTR_PARAMETER_REF). */
752
+ function callableArgsRef(obj: MetaObject, root: MetaRoot): string | undefined {
753
+ for (const child of obj.ownChildren()) {
754
+ if (child.type !== TYPE_SOURCE) continue;
755
+ const ref = child.ownAttr(SOURCE_ATTR_PARAMETER_REF);
756
+ if (typeof ref === "string" && ref !== "") {
757
+ // Only count it when it resolves to a value object (the template's guard).
758
+ const vo = root
759
+ .ownChildren()
760
+ .find((c) => c.subType === OBJECT_SUBTYPE_VALUE && refMatchesObject(c, ref));
761
+ if (vo !== undefined) return ref;
762
+ }
763
+ }
764
+ return undefined;
765
+ }
766
+
767
+ /**
768
+ * The OPT-IN Hono CRUD registrar `register<Entity>Routes(app, deps)` the
769
+ * routesFileHono generator emits into `<Entity>.routes.hono.ts`. Parallels the
770
+ * Fastify restSymbols (same verb set, same resourcePath, read-only for
771
+ * projections) but carries the Hono registrar name + import module. Documented
772
+ * only when the adopter opts into the Hono variant (includeHonoRoutes).
773
+ */
774
+ function restHonoSymbols(obj: MetaObject, layout: OutputLayout): ApiSymbol[] {
775
+ const name = obj.name;
776
+ const path = resourcePath(obj);
777
+ const readOnly = isProjection(obj);
778
+
779
+ const honoMod = entityModulePath(layout, obj, `${name}.routes.hono`);
780
+ const registrar = `register${name}Routes`;
781
+
782
+ const modelShape = modelFieldShapes(obj);
783
+ const createShape = createFieldShapes(obj);
784
+ const updateShape = updateFieldShapes(obj);
785
+
786
+ const ep = restEndpointFactory("rest-hono", honoMod, registrar);
787
+
788
+ const symbols: ApiSymbol[] = [
789
+ ep("GET", path, `[Hono] List ${name} (filter/sort/paging query params).`, modelShape),
790
+ ep("GET", `${path}/:id`, `[Hono] Fetch a single ${name} by id (404 when not found).`, modelShape),
791
+ ];
792
+
793
+ if (!readOnly) {
794
+ symbols.push(
795
+ ep("POST", path, `[Hono] Create a ${name} (body validated by ${name}InsertSchema).`, createShape),
796
+ ep("PATCH", `${path}/:id`, `[Hono] Partially update a ${name} by id (body validated by ${name}UpdateSchema).`, updateShape),
797
+ ep("DELETE", `${path}/:id`, `[Hono] Delete a ${name} by id.`),
798
+ );
799
+ }
800
+
801
+ return symbols;
802
+ }
803
+
804
+ // ---------------------------------------------------------------------------
805
+ // template.output nodes.
806
+ // ---------------------------------------------------------------------------
807
+
808
+ function templateOutputs(root: MetaRoot): MetaData[] {
809
+ return root
810
+ .ownChildren()
811
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
812
+ }
813
+
814
+ function buildTemplateUnit(tmpl: MetaData, root: MetaRoot, _layout: OutputLayout): ApiUnitDoc {
815
+ const name = tmpl.name;
816
+ const symbols: ApiSymbol[] = [];
817
+ // extractor + render-helper generators emit FLAT `<Name>.extractor.ts` /
818
+ // `<Name>.render.ts` (no package folding), so importPath ignores layout.
819
+ const extractorMod = templateModulePath(`${name}.extractor`);
820
+ const renderMod = templateModulePath(`${name}.render`);
821
+
822
+ const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
823
+ const payload = typeof payloadRef === "string" ? payloadRef : undefined;
824
+ const format = ((tmpl.ownAttr(TEMPLATE_ATTR_FORMAT) as string | undefined) ?? "text").toLowerCase();
825
+ const kind = ((tmpl.ownAttr(TEMPLATE_ATTR_KIND) as string | undefined) ?? TEMPLATE_KIND_DEFAULT).toLowerCase();
826
+
827
+ // --- extractor: only json/xml output-parsers expose the extract API (matches
828
+ // extractor-file.ts's `if (format !== "json" && format !== "xml") continue`). ---
829
+ if (payload && (format === "json" || format === "xml")) {
830
+ const extract = `extract${name}`;
831
+ const extractLenient = `extractLenient${name}`;
832
+ // The extractor's strict return IS the @payloadRef value-object's interface —
833
+ // document its field shape (same VO field walk the payload interface emitter
834
+ // uses) so an agent sees what `extract<Name>` yields, not just the type name.
835
+ const payloadShape = payloadFieldShapes(root, payload);
836
+ const extractSym: ApiSymbol = {
837
+ name: extract,
838
+ kind: "extractor",
839
+ importPath: extractorMod,
840
+ signature: `${extract}(root: MetaRoot, text: string): ${payload}`,
841
+ params: [`root: MetaRoot`, `text: string`],
842
+ returns: payload,
843
+ throws: `Error when a @required field is lost (the strict opt-in gate).`,
844
+ usage: `Parse dirty LLM ${format} text into a strict, fully-typed ${payload} graph.`,
845
+ };
846
+ if (payloadShape !== undefined) extractSym.fields = payloadShape;
847
+ symbols.push(
848
+ extractSym,
849
+ {
850
+ name: extractLenient,
851
+ kind: "extractor",
852
+ importPath: extractorMod,
853
+ signature: `${extractLenient}(root: MetaRoot, text: string): ExtractionResult<${name}Extracted>`,
854
+ params: [`root: MetaRoot`, `text: string`],
855
+ returns: `ExtractionResult<${name}Extracted>`,
856
+ usage: `Never-throwing extract; inspect report for lost/defaulted fields.`,
857
+ },
858
+ );
859
+ }
860
+
861
+ // --- render: render<Name>; document → string, email → EmailDocument
862
+ // (matches render-helper.ts's @kind branch). Render is emitted for any
863
+ // @format (the helper wraps render() regardless), so it is NOT format-gated. ---
864
+ if (payload) {
865
+ const render = `render${name}`;
866
+ const isEmail = kind === TEMPLATE_KIND_EMAIL;
867
+ const returns = isEmail ? "EmailDocument" : "string";
868
+ symbols.push({
869
+ name: render,
870
+ kind: "render",
871
+ importPath: renderMod,
872
+ signature: `${render}(payload: ${payload}, provider: Provider): ${returns}`,
873
+ params: [`payload: ${payload}`, `provider: Provider`],
874
+ returns,
875
+ usage: isEmail
876
+ ? `Render the ${name} email (subject + bodies) from a typed ${payload} payload.`
877
+ : `Render the ${name} document from a typed ${payload} payload.`,
878
+ });
879
+ }
880
+
881
+ const unit: ApiUnitDoc = {
882
+ node: name,
883
+ package: effectivePackage(tmpl),
884
+ nodeKind: "template",
885
+ symbols,
886
+ };
887
+ const example = templateExample(name, symbols);
888
+ if (example !== undefined) unit.example = example;
889
+ return unit;
890
+ }
891
+
892
+ // ---------------------------------------------------------------------------
893
+ // template.prompt nodes — the prompt-render handle.
894
+ // ---------------------------------------------------------------------------
895
+
896
+ /** TOP-LEVEL template.prompt nodes — matching the promptRender generator's own
897
+ * collection (`ctx.loadedRoot.ownChildren()` filtered to TYPE_TEMPLATE +
898
+ * TEMPLATE_SUBTYPE_PROMPT). A prompt nested INSIDE an entity is not collected by
899
+ * the generator, so the builder must not document it either (no over-doc). */
900
+ function templatePrompts(root: MetaRoot): MetaData[] {
901
+ return root
902
+ .ownChildren()
903
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
904
+ }
905
+
906
+ /**
907
+ * The render handle promptRender() emits per template.prompt — generateRenderHandle
908
+ * (payload-codegen.ts) produces
909
+ * `export function render<Name>(payload: <payloadRef>, provider: Provider): string`
910
+ * and promptRender aggregates every handle into a SINGLE file (default outFile
911
+ * "prompts.ts"), so the import module is the bare `prompts` (no package folding;
912
+ * the generator writes the outFile verbatim). The payload field shape is the
913
+ * @payloadRef VO interface (same walk the payload-interface emitter uses), so an
914
+ * agent sees what to pass.
915
+ */
916
+ function buildPromptUnit(tmpl: MetaData, root: MetaRoot): ApiUnitDoc {
917
+ const name = tmpl.name;
918
+ const symbols: ApiSymbol[] = [];
919
+ // promptRender writes the aggregated handles to `outFile` (default "prompts.ts").
920
+ const promptsMod = templateModulePath("prompts");
921
+
922
+ const payloadRef = tmpl.ownAttr(TEMPLATE_ATTR_PAYLOAD_REF);
923
+ const payload = typeof payloadRef === "string" ? payloadRef : undefined;
924
+
925
+ if (payload) {
926
+ const render = `render${name}`;
927
+ const sym: ApiSymbol = {
928
+ name: render,
929
+ kind: "prompt",
930
+ importPath: promptsMod,
931
+ signature: `${render}(payload: ${payload}, provider: Provider): string`,
932
+ params: [`payload: ${payload}`, `provider: Provider`],
933
+ returns: "string",
934
+ usage: `Render the ${name} prompt text from a typed ${payload} payload (ready to send to an LLM).`,
935
+ };
936
+ const payloadShape = payloadFieldShapes(root, payload);
937
+ if (payloadShape !== undefined) sym.fields = payloadShape;
938
+ symbols.push(sym);
939
+ }
940
+
941
+ return {
942
+ node: name,
943
+ package: effectivePackage(tmpl),
944
+ nodeKind: "template",
945
+ symbols,
946
+ };
947
+ }
948
+
949
+ // ---------------------------------------------------------------------------
950
+ // Worked examples — composed from the symbols already documented above (their
951
+ // real names + importPaths) and the field SHAPES (T2) attached to the payload
952
+ // symbols, with VALUES derived from each field's TS type. Nothing is invented:
953
+ // a symbol that isn't in `symbols` is never called, and an import never names a
954
+ // module a documented symbol doesn't already point at.
955
+ // ---------------------------------------------------------------------------
956
+
957
+ /** A sample literal for a documented field, derived from its TS type STRING
958
+ * (T2's `FieldShape.type`) — never from the entity. Enum unions yield a real
959
+ * member; arrays an empty list; scalars a type-appropriate placeholder. */
960
+ function sampleValueForType(type: string): string {
961
+ const t = type.trim();
962
+ // Enum / string-literal union (`"active" | "archived"`): use the first member
963
+ // verbatim so the example is a REAL accepted value.
964
+ const firstLiteral = t.match(/^"([^"]*)"/);
965
+ if (firstLiteral) return `"${firstLiteral[1]}"`;
966
+ if (t.endsWith("[]")) return "[]";
967
+ if (t === "number") return "1";
968
+ if (t === "bigint") return "1n";
969
+ if (t === "boolean") return "true";
970
+ if (t === "Date") return "new Date()";
971
+ if (t === "string") return `"…"`;
972
+ // Unknown / object / nested type: a typed-object placeholder keeps the call
973
+ // shape intact without inventing a fake member set.
974
+ return "{}";
975
+ }
976
+
977
+ /** Build a `{ field: value; … }` object literal from a payload field shape,
978
+ * using only the REQUIRED fields plus the first optional (so an agent sees a
979
+ * minimal-but-real body) — values derived from each field's TS type. */
980
+ function objectLiteralFromFields(fields: FieldShape[] | undefined): string {
981
+ if (fields === undefined || fields.length === 0) return "{}";
982
+ const required = fields.filter((f) => !f.optional);
983
+ // If nothing is strictly required, show the first field so the body isn't `{}`.
984
+ const chosen = required.length > 0 ? required : fields.slice(0, 1);
985
+ const parts = chosen.map((f) => `${f.name}: ${sampleValueForType(f.type)}`);
986
+ return `{ ${parts.join(", ")} }`;
987
+ }
988
+
989
+ /** One `import { … } from "<mod>"` line per module, deduped, reusing each
990
+ * symbol's OWN importPath (so the example import can't drift from the docs). */
991
+ function importLines(picks: { name: string; importPath: string }[]): string[] {
992
+ const order: string[] = [];
993
+ const byMod = new Map<string, string[]>();
994
+ for (const p of picks) {
995
+ let names = byMod.get(p.importPath);
996
+ if (names === undefined) {
997
+ names = [];
998
+ byMod.set(p.importPath, names);
999
+ order.push(p.importPath);
1000
+ }
1001
+ if (!names.includes(p.name)) names.push(p.name);
1002
+ }
1003
+ return order.map((mod) => `import { ${byMod.get(mod)!.join(", ")} } from "${mod}";`);
1004
+ }
1005
+
1006
+ /** A worked create→find→update→delete flow over an entity's documented CRUD
1007
+ * helpers. Only emitted when the entity actually carries those data-access
1008
+ * symbols (a value object / TPH subtype has only a model → no example).
1009
+ *
1010
+ * `pkName` is the entity's REAL primary-key field (from getPkInfo) — the
1011
+ * find/update/delete calls read it back as `created.<pkName>`, so the example
1012
+ * stays accurate-by-construction for an entity whose PK is not named `id`. */
1013
+ function entityExample(name: string, pkName: string, symbols: ApiSymbol[]): UnitExample | undefined {
1014
+ const da = (fn: string) => symbols.find((s) => s.kind === "data-access" && s.name === fn);
1015
+ const create = da(createFnName(name));
1016
+ const find = da(findByIdFnName(name));
1017
+ const update = da(updateFnName(name));
1018
+ const del = da(deleteByIdFnName(name));
1019
+ // Need at least create+find to have a meaningful worked flow.
1020
+ if (create === undefined || find === undefined) return undefined;
1021
+
1022
+ const createBody = objectLiteralFromFields(create.fields);
1023
+ // The handle returned by create<Name> exposes the row's real PK accessor.
1024
+ const createdPk = `created.${pkName}`;
1025
+ const picks: { name: string; importPath: string }[] = [
1026
+ { name: create.name, importPath: create.importPath },
1027
+ { name: find.name, importPath: find.importPath },
1028
+ ];
1029
+ const body: string[] = [
1030
+ `const created = await ${create.name}(db, ${createBody});`,
1031
+ `const found = await ${find.name}(db, ${createdPk});`,
1032
+ ];
1033
+ if (update !== undefined) {
1034
+ picks.push({ name: update.name, importPath: update.importPath });
1035
+ body.push(`const updated = await ${update.name}(db, ${createdPk}, ${objectLiteralFromFields(update.fields)});`);
1036
+ }
1037
+ if (del !== undefined) {
1038
+ picks.push({ name: del.name, importPath: del.importPath });
1039
+ body.push(`const removed = await ${del.name}(db, ${createdPk});`);
1040
+ }
1041
+ return { imports: importLines(picks), body };
1042
+ }
1043
+
1044
+ /** A worked extract / render example for a template unit, over whichever of the
1045
+ * two surfaces the template actually exposes (extract is json/xml-gated). */
1046
+ function templateExample(name: string, symbols: ApiSymbol[]): UnitExample | undefined {
1047
+ const extract = symbols.find((s) => s.kind === "extractor" && s.name === `extract${name}`);
1048
+ const renderSym = symbols.find((s) => s.kind === "render" && s.name === `render${name}`);
1049
+ if (extract === undefined && renderSym === undefined) return undefined;
1050
+
1051
+ const picks: { name: string; importPath: string }[] = [];
1052
+ const body: string[] = [];
1053
+ if (extract !== undefined) {
1054
+ picks.push({ name: extract.name, importPath: extract.importPath });
1055
+ body.push(`const extracted = ${extract.name}(root, llmText);`);
1056
+ }
1057
+ if (renderSym !== undefined) {
1058
+ picks.push({ name: renderSym.name, importPath: renderSym.importPath });
1059
+ // Render's payload object literal comes from the @payloadRef VO shape the
1060
+ // render symbol returns/consumes; reuse the extractor's documented payload
1061
+ // fields when present so the example body is real.
1062
+ const payloadFields = extract?.fields;
1063
+ const payloadLit = objectLiteralFromFields(payloadFields);
1064
+ body.push(`const output = ${renderSym.name}(${payloadLit}, provider);`);
1065
+ }
1066
+ return { imports: importLines(picks), body };
1067
+ }