@metaobjectsdev/codegen-ts 0.24.5 → 1.0.0-rc.1

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 (237) hide show
  1. package/README.md +37 -12
  2. package/dist/api-surface.d.ts +29 -0
  3. package/dist/api-surface.d.ts.map +1 -1
  4. package/dist/api-surface.js +47 -0
  5. package/dist/api-surface.js.map +1 -1
  6. package/dist/column-mapper.d.ts +12 -0
  7. package/dist/column-mapper.d.ts.map +1 -1
  8. package/dist/column-mapper.js +1 -0
  9. package/dist/column-mapper.js.map +1 -1
  10. package/dist/constants.d.ts +24 -11
  11. package/dist/constants.d.ts.map +1 -1
  12. package/dist/constants.js +54 -19
  13. package/dist/constants.js.map +1 -1
  14. package/dist/db-emitting.d.ts +6 -0
  15. package/dist/db-emitting.d.ts.map +1 -0
  16. package/dist/db-emitting.js +26 -0
  17. package/dist/db-emitting.js.map +1 -0
  18. package/dist/generator-registry.d.ts.map +1 -1
  19. package/dist/generator-registry.js +14 -1
  20. package/dist/generator-registry.js.map +1 -1
  21. package/dist/generator.d.ts +5 -0
  22. package/dist/generator.d.ts.map +1 -1
  23. package/dist/generator.js.map +1 -1
  24. package/dist/generators/agent-docs-file.d.ts +19 -0
  25. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  26. package/dist/generators/agent-docs-file.js +257 -0
  27. package/dist/generators/agent-docs-file.js.map +1 -0
  28. package/dist/generators/agent-requirements-page.d.ts +22 -0
  29. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  30. package/dist/generators/agent-requirements-page.js +175 -0
  31. package/dist/generators/agent-requirements-page.js.map +1 -0
  32. package/dist/generators/agent-schema-input.d.ts +69 -0
  33. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  34. package/dist/generators/agent-schema-input.js +23 -0
  35. package/dist/generators/agent-schema-input.js.map +1 -0
  36. package/dist/generators/agent-schema-page.d.ts +21 -0
  37. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-page.js +241 -0
  39. package/dist/generators/agent-schema-page.js.map +1 -0
  40. package/dist/generators/agent-ui-page.d.ts +22 -0
  41. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  42. package/dist/generators/agent-ui-page.js +253 -0
  43. package/dist/generators/agent-ui-page.js.map +1 -0
  44. package/dist/generators/api-docs-file.d.ts.map +1 -1
  45. package/dist/generators/api-docs-file.js +13 -6
  46. package/dist/generators/api-docs-file.js.map +1 -1
  47. package/dist/generators/api-model.d.ts +6 -0
  48. package/dist/generators/api-model.d.ts.map +1 -1
  49. package/dist/generators/api-model.js +54 -35
  50. package/dist/generators/api-model.js.map +1 -1
  51. package/dist/generators/callable-file.js +1 -1
  52. package/dist/generators/callable-file.js.map +1 -1
  53. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  54. package/dist/generators/docs-data-builder.js +47 -2
  55. package/dist/generators/docs-data-builder.js.map +1 -1
  56. package/dist/generators/docs-data.d.ts +6 -2
  57. package/dist/generators/docs-data.d.ts.map +1 -1
  58. package/dist/generators/index.d.ts +3 -8
  59. package/dist/generators/index.d.ts.map +1 -1
  60. package/dist/generators/index.js +19 -14
  61. package/dist/generators/index.js.map +1 -1
  62. package/dist/generators/names-file.d.ts +17 -0
  63. package/dist/generators/names-file.d.ts.map +1 -0
  64. package/dist/generators/names-file.js +95 -0
  65. package/dist/generators/names-file.js.map +1 -0
  66. package/dist/generators/requirements-markdown.d.ts +18 -1
  67. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  68. package/dist/generators/requirements-markdown.js +11 -9
  69. package/dist/generators/requirements-markdown.js.map +1 -1
  70. package/dist/generators/routes-file-hono.d.ts +17 -2
  71. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  72. package/dist/generators/routes-file-hono.js +9 -11
  73. package/dist/generators/routes-file-hono.js.map +1 -1
  74. package/dist/generators/routes-file.d.ts +17 -2
  75. package/dist/generators/routes-file.d.ts.map +1 -1
  76. package/dist/generators/routes-file.js +10 -8
  77. package/dist/generators/routes-file.js.map +1 -1
  78. package/dist/generators/template-generator.d.ts.map +1 -1
  79. package/dist/generators/template-generator.js +20 -6
  80. package/dist/generators/template-generator.js.map +1 -1
  81. package/dist/index.d.ts +20 -5
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +43 -4
  84. package/dist/index.js.map +1 -1
  85. package/dist/metaobjects-config.d.ts +38 -1
  86. package/dist/metaobjects-config.d.ts.map +1 -1
  87. package/dist/metaobjects-config.js +9 -1
  88. package/dist/metaobjects-config.js.map +1 -1
  89. package/dist/names.d.ts +275 -0
  90. package/dist/names.d.ts.map +1 -0
  91. package/dist/names.js +433 -0
  92. package/dist/names.js.map +1 -0
  93. package/dist/overwrite-policy.d.ts.map +1 -1
  94. package/dist/overwrite-policy.js +15 -3
  95. package/dist/overwrite-policy.js.map +1 -1
  96. package/dist/projection/extract-view-spec.d.ts +25 -1
  97. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  98. package/dist/projection/extract-view-spec.js +79 -6
  99. package/dist/projection/extract-view-spec.js.map +1 -1
  100. package/dist/reference-templates.d.ts +1 -1
  101. package/dist/reference-templates.d.ts.map +1 -1
  102. package/dist/reference-templates.js +1 -1
  103. package/dist/reference-templates.js.map +1 -1
  104. package/dist/render-context.d.ts +12 -1
  105. package/dist/render-context.d.ts.map +1 -1
  106. package/dist/render-context.js +1 -0
  107. package/dist/render-context.js.map +1 -1
  108. package/dist/retired-codegen-attrs.d.ts +32 -0
  109. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  110. package/dist/retired-codegen-attrs.js +47 -0
  111. package/dist/retired-codegen-attrs.js.map +1 -0
  112. package/dist/routes-expose.d.ts +63 -0
  113. package/dist/routes-expose.d.ts.map +1 -0
  114. package/dist/routes-expose.js +57 -0
  115. package/dist/routes-expose.js.map +1 -0
  116. package/dist/runner.d.ts +18 -0
  117. package/dist/runner.d.ts.map +1 -1
  118. package/dist/runner.js +109 -7
  119. package/dist/runner.js.map +1 -1
  120. package/dist/templates/callable-file.d.ts +7 -2
  121. package/dist/templates/callable-file.d.ts.map +1 -1
  122. package/dist/templates/callable-file.js +54 -9
  123. package/dist/templates/callable-file.js.map +1 -1
  124. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  125. package/dist/templates/drizzle-schema.js +236 -65
  126. package/dist/templates/drizzle-schema.js.map +1 -1
  127. package/dist/templates/entity-constants.d.ts +15 -6
  128. package/dist/templates/entity-constants.d.ts.map +1 -1
  129. package/dist/templates/entity-constants.js +85 -161
  130. package/dist/templates/entity-constants.js.map +1 -1
  131. package/dist/templates/entity-file.d.ts.map +1 -1
  132. package/dist/templates/entity-file.js +38 -4
  133. package/dist/templates/entity-file.js.map +1 -1
  134. package/dist/templates/entity-meta-file.d.ts +15 -1
  135. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  136. package/dist/templates/entity-meta-file.js +12 -2
  137. package/dist/templates/entity-meta-file.js.map +1 -1
  138. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  139. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  140. package/dist/templates/entity-ui-descriptor.js +238 -0
  141. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  142. package/dist/templates/field-meta.d.ts +46 -9
  143. package/dist/templates/field-meta.d.ts.map +1 -1
  144. package/dist/templates/field-meta.js +95 -21
  145. package/dist/templates/field-meta.js.map +1 -1
  146. package/dist/templates/names-decl.d.ts +38 -0
  147. package/dist/templates/names-decl.d.ts.map +1 -0
  148. package/dist/templates/names-decl.js +104 -0
  149. package/dist/templates/names-decl.js.map +1 -0
  150. package/dist/templates/projection-decl.d.ts +21 -0
  151. package/dist/templates/projection-decl.d.ts.map +1 -1
  152. package/dist/templates/projection-decl.js +53 -29
  153. package/dist/templates/projection-decl.js.map +1 -1
  154. package/dist/templates/queries-file.d.ts.map +1 -1
  155. package/dist/templates/queries-file.js +17 -2
  156. package/dist/templates/queries-file.js.map +1 -1
  157. package/dist/templates/requirement-test.d.ts.map +1 -1
  158. package/dist/templates/requirement-test.js +13 -2
  159. package/dist/templates/requirement-test.js.map +1 -1
  160. package/dist/templates/routes-file-hono.d.ts +2 -1
  161. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  162. package/dist/templates/routes-file-hono.js +11 -2
  163. package/dist/templates/routes-file-hono.js.map +1 -1
  164. package/dist/templates/routes-file.d.ts +2 -1
  165. package/dist/templates/routes-file.d.ts.map +1 -1
  166. package/dist/templates/routes-file.js +54 -21
  167. package/dist/templates/routes-file.js.map +1 -1
  168. package/dist/templates/value-object-file.d.ts.map +1 -1
  169. package/dist/templates/value-object-file.js +15 -1
  170. package/dist/templates/value-object-file.js.map +1 -1
  171. package/dist/templates/view-decl.d.ts +37 -1
  172. package/dist/templates/view-decl.d.ts.map +1 -1
  173. package/dist/templates/view-decl.js +27 -3
  174. package/dist/templates/view-decl.js.map +1 -1
  175. package/dist/templates/zod-validators.d.ts +21 -0
  176. package/dist/templates/zod-validators.d.ts.map +1 -1
  177. package/dist/templates/zod-validators.js +40 -5
  178. package/dist/templates/zod-validators.js.map +1 -1
  179. package/dist/view-context.d.ts +24 -0
  180. package/dist/view-context.d.ts.map +1 -0
  181. package/dist/view-context.js +81 -0
  182. package/dist/view-context.js.map +1 -0
  183. package/package.json +6 -6
  184. package/src/api-surface.ts +53 -0
  185. package/src/column-mapper.ts +13 -0
  186. package/src/constants.ts +91 -16
  187. package/src/db-emitting.ts +31 -0
  188. package/src/generator-registry.ts +14 -4
  189. package/src/generator.ts +5 -0
  190. package/src/generators/agent-docs-file.ts +297 -0
  191. package/src/generators/agent-requirements-page.ts +199 -0
  192. package/src/generators/agent-schema-input.ts +88 -0
  193. package/src/generators/agent-schema-page.ts +297 -0
  194. package/src/generators/agent-ui-page.ts +290 -0
  195. package/src/generators/api-docs-file.ts +13 -6
  196. package/src/generators/api-model.ts +63 -34
  197. package/src/generators/callable-file.ts +1 -1
  198. package/src/generators/docs-data-builder.ts +46 -2
  199. package/src/generators/docs-data.ts +6 -2
  200. package/src/generators/index.ts +25 -14
  201. package/src/generators/names-file.ts +98 -0
  202. package/src/generators/requirements-markdown.ts +36 -2
  203. package/src/generators/routes-file-hono.ts +19 -10
  204. package/src/generators/routes-file.ts +20 -7
  205. package/src/generators/template-generator.ts +20 -6
  206. package/src/index.ts +64 -4
  207. package/src/metaobjects-config.ts +48 -2
  208. package/src/names.ts +629 -0
  209. package/src/overwrite-policy.ts +15 -3
  210. package/src/projection/extract-view-spec.ts +85 -6
  211. package/src/reference/entity.ts +21 -2
  212. package/src/reference/names.ts +125 -0
  213. package/src/reference/queries.ts +19 -3
  214. package/src/reference/routes-hono.ts +19 -9
  215. package/src/reference/routes.ts +25 -9
  216. package/src/reference-templates.ts +1 -1
  217. package/src/render-context.ts +13 -1
  218. package/src/retired-codegen-attrs.ts +52 -0
  219. package/src/routes-expose.ts +86 -0
  220. package/src/runner.ts +122 -11
  221. package/src/templates/callable-file.ts +60 -8
  222. package/src/templates/drizzle-schema.ts +248 -60
  223. package/src/templates/entity-constants.ts +95 -184
  224. package/src/templates/entity-file.ts +46 -4
  225. package/src/templates/entity-meta-file.ts +16 -3
  226. package/src/templates/entity-ui-descriptor.ts +305 -0
  227. package/src/templates/field-meta.ts +100 -19
  228. package/src/templates/names-decl.ts +165 -0
  229. package/src/templates/projection-decl.ts +72 -31
  230. package/src/templates/queries-file.ts +17 -2
  231. package/src/templates/requirement-test.ts +13 -2
  232. package/src/templates/routes-file-hono.ts +14 -2
  233. package/src/templates/routes-file.ts +69 -24
  234. package/src/templates/value-object-file.ts +15 -1
  235. package/src/templates/view-decl.ts +53 -3
  236. package/src/templates/zod-validators.ts +41 -5
  237. package/src/view-context.ts +88 -0
@@ -35,7 +35,7 @@
35
35
  // (templates/routes-file-hono.ts) — the OPT-IN Hono variant
36
36
  // of the Fastify REST surface. Documented ONLY when the
37
37
  // adopter wires routesFileHono() (ctx.includeHonoRoutes), and
38
- // gated by the SAME @emitRoutes:false filter.
38
+ // gated by the SAME source + TPH rules as the Fastify surface.
39
39
  // • template.prompt (T5):
40
40
  // - prompt : render<Name> (payload, provider): string — the prompt
41
41
  // render handle promptRender() emits into a single
@@ -75,10 +75,17 @@
75
75
  // / update<Sub>ById / delete<Sub>ById scoped to the shared table) and the
76
76
  // subtype REST subpaths are NOT YET documented by this builder — that fuller
77
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.
78
+ //
79
+ // NOT modelled here (stated so the gap is known + intentional): a generator's own
80
+ // `filter` option. `routesFile({ filter })` narrows what the routes generator emits,
81
+ // and this builder reads the MODEL, not the wired generator set, so it cannot see
82
+ // that narrowing and will over-document REST for a filtered-out entity. That is
83
+ // under-documentation's opposite and it is a real limitation — but it is inherent to
84
+ // a docs pass that runs off metadata, and it applies equally to every generator's
85
+ // filter. (It used to be dodged for routes ALONE via an @emitRoutes:false metadata
86
+ // read; that attribute was never registered vocabulary, so `meta verify` rejected the
87
+ // very models this builder was reading it from. The read is gone — see
88
+ // constants.ts.)
82
89
 
83
90
  import {
84
91
  type MetaRoot,
@@ -112,8 +119,7 @@ import { isTphSubtype } from "../templates/zod-validators.js";
112
119
  import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
113
120
  import { isCallableEntity } from "../templates/callable-file.js";
114
121
  import { hasAnyRdbSource } from "../source-detect.js";
115
- import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
116
- import { resourcePath } from "../templates/entity-constants.js";
122
+ import { servedPath } from "../api-surface.js";
117
123
  import { isProjection } from "../projection/projection-detector.js";
118
124
  import { buildPkMap } from "../pk-resolver.js";
119
125
  import { buildRelationMap, type RelationEntry, type RelationMap } from "../relation-resolver.js";
@@ -263,6 +269,12 @@ export interface ApiModelContext {
263
269
  * run didn't configure). The Fastify REST surface is always documented (it is
264
270
  * the default-suite routes generator). Defaults to false. */
265
271
  includeHonoRoutes?: boolean;
272
+ /** The project's `apiPrefix`. Every generated route is mounted inside
273
+ * `fastify.register(…, { prefix: apiPrefix })`, so it is part of the address a
274
+ * documented endpoint is served at — a page that omits it is one path segment
275
+ * short. Threaded explicitly rather than read off the RenderContext below,
276
+ * which is a `{ pkMap }` shim and carries nothing else. Defaults to "". */
277
+ apiPrefix?: string;
266
278
  }
267
279
 
268
280
  // ---------------------------------------------------------------------------
@@ -275,13 +287,14 @@ export function buildApiModel(root: MetaRoot, ctx: ApiModelContext): ApiModel {
275
287
  // shim is sufficient (and avoids forcing callers to build a full context).
276
288
  const pkCtx = { pkMap } as RenderContext;
277
289
  const layout = ctx.outputLayout ?? "flat";
290
+ const apiPrefix = ctx.apiPrefix ?? "";
278
291
  const relationMap = ctx.relationMap ?? buildRelationMap(root);
279
292
  const includeHono = ctx.includeHonoRoutes ?? false;
280
293
 
281
294
  const units: ApiUnitDoc[] = [];
282
295
 
283
296
  for (const obj of root.objects()) {
284
- units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono));
297
+ units.push(buildEntityUnit(obj, pkCtx, root, layout, relationMap, includeHono, apiPrefix));
285
298
  }
286
299
 
287
300
  for (const tmpl of templateOutputs(root)) {
@@ -353,15 +366,6 @@ function isQueryable(obj: MetaObject): boolean {
353
366
  return hasAnyRdbSource(obj) && !isTphSubtype(obj);
354
367
  }
355
368
 
356
- /** Whether the routes generator emits REST routes for this entity. It filters
357
- * out @emitRoutes:false (routes-file.ts:27), unlike the queries + validator
358
- * generators which always emit. So REST symbols are gated separately from the
359
- * other queryable kinds. */
360
- function emitsRoutes(obj: MetaObject): boolean {
361
- // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
362
- return obj.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false;
363
- }
364
-
365
369
  function buildEntityUnit(
366
370
  obj: MetaObject,
367
371
  ctx: RenderContext,
@@ -369,6 +373,7 @@ function buildEntityUnit(
369
373
  layout: OutputLayout,
370
374
  relationMap: RelationMap,
371
375
  includeHono: boolean,
376
+ apiPrefix: string,
372
377
  ): ApiUnitDoc {
373
378
  const name = obj.name;
374
379
  const symbols: ApiSymbol[] = [];
@@ -392,13 +397,15 @@ function buildEntityUnit(
392
397
  if (isQueryable(obj)) {
393
398
  symbols.push(...dataAccessSymbols(obj, ctx, root, layout));
394
399
  symbols.push(...validationSymbols(obj, entityMod));
395
- // REST is additionally gated: @emitRoutes:false suppresses routes only.
396
- if (emitsRoutes(obj)) {
397
- symbols.push(...restSymbols(obj, layout));
398
- // The OPT-IN Hono variant mounts the SAME CRUD verbs under the SAME
399
- // @emitRoutes filter — documented only when the adopter wired it.
400
- if (includeHono) symbols.push(...restHonoSymbols(obj, layout));
401
- }
400
+ // REST needs no gate of its own: the routes generator's built-in filter is
401
+ // `hasAnyRdbSource && !isTphSubtype` — exactly isQueryable — so every queryable
402
+ // object gets routes. (Its `filter` option can narrow that further; this builder
403
+ // reads the model, not the wired generator set, and cannot see it. See the module
404
+ // header.)
405
+ symbols.push(...restSymbols(obj, layout, root, apiPrefix));
406
+ // The OPT-IN Hono variant mounts the SAME CRUD verbs under the same rules —
407
+ // documented only when the adopter wired it.
408
+ if (includeHono) symbols.push(...restHonoSymbols(obj, layout, apiPrefix));
402
409
  }
403
410
 
404
411
  // --- relation: the drizzle relations() export, when the resolver derives a
@@ -565,15 +572,35 @@ function validationSymbols(obj: MetaObject, entityMod: string): ApiSymbol[] {
565
572
  * generator does NOT emit one function per verb — it emits a single
566
573
  * `<name>Routes(fastify)` handler that mounts the standard CRUD verb set at the
567
574
  * entity's $path via mountCrudRoutes (or the read-only subset via
568
- * mountReadOnlyCrudRoutes for a projection). We reuse resourcePath() the same
569
- * function entity-constants.ts uses to compute $path — so the documented paths
570
- * match the generated routes exactly. The verb→path mapping mirrors the runtime
571
- * mountCrudRoutes contract referenced in routes-file.ts's comments.
575
+ * mountReadOnlyCrudRoutes for a projection). The verb→path mapping mirrors the
576
+ * runtime mountCrudRoutes contract referenced in routes-file.ts's comments.
577
+ *
578
+ * THE DOCUMENTED PATH IS THE ADDRESS THE ROUTES ACTUALLY SERVE, which takes two
579
+ * things this comment used to claim it got from one. It said it reused
580
+ * `resourcePath()` — "the same function entity-constants.ts uses to compute
581
+ * $path" — and both halves of that had stopped being true:
582
+ *
583
+ * • `$path` is `restPath()` now, not `resourcePath()`. A TPH subtype is mounted
584
+ * at `<base>/<segment>`, so `resourcePath` named an address nothing serves —
585
+ * the defect fixed for `$path` itself, still live here.
586
+ * • Every mount sits inside `fastify.register(…, { prefix: apiPrefix })`, so a
587
+ * project configuring "/api" was documented one path segment short.
588
+ *
589
+ * A TPH DISCRIMINATOR BASE serves only `TPH_POLYMORPHIC_VERBS` at its own path, and that
590
+ * set is imported rather than restated. `isProjection` alone was the read-only test, so a
591
+ * base — which is not a projection — was documented with POST/PATCH/DELETE against a mount
592
+ * `routes-file.ts` builds as `intersectExpose(TPH_POLYMORPHIC_VERBS, expose)`: three verbs
593
+ * that 404, published in the file an adopter is told to call generated code from, under the
594
+ * comment three lines above claiming these paths match the routes exactly. The per-subtype
595
+ * mounts at `<base path>/<segment>`, which is where writes actually live, remain a
596
+ * documented deferral (see the module header).
572
597
  */
573
- function restSymbols(obj: MetaObject, layout: OutputLayout): ApiSymbol[] {
598
+ function restSymbols(
599
+ obj: MetaObject, layout: OutputLayout, root: MetaRoot, apiPrefix: string,
600
+ ): ApiSymbol[] {
574
601
  const name = obj.name;
575
- const path = resourcePath(obj);
576
- const readOnly = isProjection(obj);
602
+ const path = servedPath(obj, apiPrefix);
603
+ const readOnly = isProjection(obj) || isTphDiscriminatorBase(obj, root);
577
604
 
578
605
  // REST endpoints are not importable functions — to WIRE them an adopter
579
606
  // imports the entity's route registrar (`<entity>Routes`) from the routes
@@ -776,13 +803,15 @@ function callableArgsRef(obj: MetaObject, root: MetaRoot): string | undefined {
776
803
  /**
777
804
  * The OPT-IN Hono CRUD registrar `register<Entity>Routes(app, deps)` the
778
805
  * routesFileHono generator emits into `<Entity>.routes.hono.ts`. Parallels the
779
- * Fastify restSymbols (same verb set, same resourcePath, read-only for
806
+ * Fastify restSymbols (same verb set, same served address, read-only for
780
807
  * projections) but carries the Hono registrar name + import module. Documented
781
808
  * only when the adopter opts into the Hono variant (includeHonoRoutes).
782
809
  */
783
- function restHonoSymbols(obj: MetaObject, layout: OutputLayout): ApiSymbol[] {
810
+ function restHonoSymbols(
811
+ obj: MetaObject, layout: OutputLayout, apiPrefix: string,
812
+ ): ApiSymbol[] {
784
813
  const name = obj.name;
785
- const path = resourcePath(obj);
814
+ const path = servedPath(obj, apiPrefix);
786
815
  const readOnly = isProjection(obj);
787
816
 
788
817
  const honoMod = entityModulePath(layout, obj, `${name}.routes.hono`);
@@ -33,7 +33,7 @@ export const callableFile = function callableFile(opts?: CallableFileOpts): Gene
33
33
  entity.package,
34
34
  `${entity.name}.callable.ts`,
35
35
  ),
36
- content: await formatTs(renderCallableFile(entity)),
36
+ content: await formatTs(renderCallableFile(entity, ctx.renderContext)),
37
37
  };
38
38
  }),
39
39
  };
@@ -4,6 +4,7 @@
4
4
  // markdown structure now lives in templates/docs/entity-page.md.mustache.
5
5
 
6
6
  import {
7
+ type MetaData,
7
8
  type MetaObject,
8
9
  type MetaField,
9
10
  type MetaIdentity,
@@ -699,17 +700,60 @@ export function buildEntityDocData(
699
700
  //
700
701
  // ENTITY-GRAIN. Object coverage is entity-grain, so a claimed value/projection gets
701
702
  // nothing — surfacing one would imply a coverage rule the ledger does not have.
703
+ //
704
+ // "Entity-grain" is about which PAGE the row lands on, NOT about how deep the claim may
705
+ // point. A claim is authored at whatever grain the requirement is really about, and an L5
706
+ // one names a MEMBER — `acme::Subscriber.status` in the shipped showcase. `walkRequirements`
707
+ // resolves that to the FIELD node, so an identity test against `entity` missed it and the
708
+ // showcase's only claim produced no "Required by" at all. The claim belongs on the owning
709
+ // entity's page either way: that is where a reader looks, and the member is named in the row
710
+ // rather than being the reason the row disappears.
711
+ //
712
+ // Hence: match the claimed node OR any ANCESTOR of it up to and including this entity. The
713
+ // walk stops AT the entity, so a claim on some other object's member cannot reach this page
714
+ // by climbing past its own root.
715
+ const memberPathTo = (target: MetaData): string | undefined => {
716
+ const segments: string[] = [];
717
+ for (let n: MetaData | undefined = target; n !== undefined; n = n.parent) {
718
+ if (n === entity) return segments.length > 0 ? segments.reverse().join(".") : "";
719
+ segments.push(n.name);
720
+ }
721
+ return undefined; // not under this entity at all
722
+ };
723
+ /** Dotted-path depth. "" is the entity itself — depth 0, not the 1 a naive split gives. */
724
+ const memberDepth = (path: string): number => (path === "" ? 0 : path.split(".").length);
702
725
  const claimedByMatches: UsedByDoc[] = [];
703
726
  if (entity.subType === OBJECT_SUBTYPE_ENTITY) {
704
727
  for (const walked of walkRequirements(root)) {
705
- if (!walked.targets.some((t) => t.node === entity)) continue;
728
+ // The SHALLOWEST member path among this requirement's targets that lands on this
729
+ // entity: "" for an entity-grain claim, "status" for an L5 member claim. undefined
730
+ // when no target is under this entity.
731
+ let member: string | undefined;
732
+ let memberD = Number.POSITIVE_INFINITY;
733
+ for (const t of walked.targets) {
734
+ const path = memberPathTo(t.node);
735
+ if (path === undefined) continue;
736
+ const d = memberDepth(path);
737
+ // Shallowest by SEGMENT COUNT, not string length. `path.length` made "a.b" — two
738
+ // segments, three characters — beat "status", one segment and six, so the row
739
+ // named the deeper member. Ties break lexicographically so the page does not
740
+ // depend on the order the requirement walk happens to return targets in.
741
+ if (member === undefined || d < memberD || (d === memberD && path < member)) {
742
+ member = path;
743
+ memberD = d;
744
+ }
745
+ }
746
+ if (member === undefined) continue;
706
747
  const v = walked.view;
707
748
  const level = v.level === undefined ? "" : ` · **L${v.level}**`;
708
749
  const status = v.status === undefined ? "" : ` · status: \`${v.status}\``;
709
750
  const statement = walked.node.attr(REQUIREMENT_ATTR_STATEMENT);
710
751
  const said = typeof statement === "string" && statement.length > 0 ? ` — ${statement}` : "";
752
+ // A member-grain claim says WHICH member, so the row is actionable rather than
753
+ // merely present. An entity-grain claim renders exactly as before (byte-identical).
754
+ const on = member === "" ? "" : ` · on \`${member}\``;
711
755
  claimedByMatches.push({
712
- bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${said}`,
756
+ bullet: `\`requirement.${v.subType} ${v.path}\`${level}${status}${on}${said}`,
713
757
  });
714
758
  }
715
759
  }
@@ -264,9 +264,13 @@ export interface EntityDocData {
264
264
  * BYTE-IDENTICAL to its pre-feature output. That is the no-churn contract:
265
265
  * a project with no ledger, or an entity nothing claims, must see no diff.
266
266
  *
267
- * ENTITY-GRAIN ONLY. Object coverage is entity-grain (`spec/capability-ledger.md`),
267
+ * ENTITY PAGES ONLY. Object coverage is entity-grain (`spec/capability-ledger.md`),
268
268
  * so a claimed `object.value` / `object.projection` gets nothing here — surfacing
269
- * one would imply a coverage rule the ledger does not actually have. */
269
+ * one would imply a coverage rule the ledger does not actually have.
270
+ *
271
+ * That governs which PAGE a row lands on, not how deep the CLAIM may point: an L5
272
+ * claim names a member, resolves to the FIELD, and still renders on the owning
273
+ * entity's page with the member named in the row (`· on \`status\``). */
270
274
  claimedBy?: UsedByDoc[];
271
275
  /** Present-and-non-empty flag for the claimedBy section. */
272
276
  hasClaimedBy?: boolean;
@@ -1,20 +1,22 @@
1
- // ADR-0034 scaffold-and-own — these built-in generator factories remain the engine's
2
- // internal composers, but importing them from `@metaobjectsdev/codegen-ts/generators`
3
- // into a consumer's `metaobjects.config.ts` is DEPRECATED. The recommended path is to
4
- // own copyable reference templates in your repo (`meta init` scaffolds them into
5
- // `codegen/generators/*.ts`) and import those locally. This package export will be
6
- // removed in a future major. See spec/decisions/ADR-0034-codegen-scaffold-and-own.md.
1
+ // Built-in generator factories.
2
+ //
3
+ // ADR-0034 scaffold-and-own `entityFile`, `queriesFile`, `routesFile` and `barrel` were
4
+ // exported here and DEPRECATED; 1.0 REMOVED them (ADR-0035 A3, `docs/1.0-readiness.md` G2,
5
+ // `docs/features/migrations/0.x-to-1.0.md` §11). Own a copy instead: `meta init` scaffolds
6
+ // them into `codegen/generators/*.ts` and `meta eject <name>` copies one at any time. They
7
+ // remain in this directory as the engine's internal composers and as the oracle the
8
+ // byte-identity gate holds each reference template to — they are no longer public API.
9
+ //
10
+ // The factories BELOW are not deprecated and this subpath is their supported public home:
11
+ // the prompt/output tier (`promptRender`, `outputParser`, `outputPrompt`, `extractor`,
12
+ // `renderHelper`, `traceHelperFile`) is upstream-owned and has no ownable copy — the CLI's
13
+ // own prompt-gate warning names this import path — and `routesFileHono` / `namesFile` /
14
+ // `callableFile` are stock generators a consumer wires directly.
15
+ // See spec/decisions/ADR-0034-codegen-scaffold-and-own.md.
7
16
 
8
- /** @deprecated ADR-0034 — own a copy instead: `import { entityFile } from "./codegen/generators/entity"` (scaffolded by `meta init`). */
9
- export { entityFile, type EntityFileOpts } from "./entity-file.js";
10
- /** @deprecated ADR-0034 — own a copy instead: `import { queriesFile } from "./codegen/generators/queries"` (scaffolded by `meta init`). */
11
- export { queriesFile, type QueriesFileOpts } from "./queries-file.js";
12
17
  export { callableFile, type CallableFileOpts } from "./callable-file.js";
13
- /** @deprecated ADR-0034 — own a copy instead: `import { routesFile } from "./codegen/generators/routes"` (scaffolded by `meta init`). */
14
- export { routesFile, type RoutesFileOpts } from "./routes-file.js";
15
18
  export { routesFileHono, type RoutesFileHonoOpts } from "./routes-file-hono.js";
16
- /** @deprecated ADR-0034 — own a copy instead: `import { barrel } from "./codegen/generators/barrel"` (scaffolded by `meta init`). */
17
- export { barrel, type BarrelOpts } from "./barrel.js";
19
+ export { namesFile } from "./names-file.js";
18
20
  /** @deprecated ADR-0021 D1 — neutral artifact owned by `meta docs` (ADR-0020); not part of the recommended `meta gen` suite. */
19
21
  export { mermaidErDiagram, type MermaidErOptions } from "./mermaid-er.js";
20
22
  export { promptRender, type PromptRenderOpts } from "./prompt-render-file.js";
@@ -50,6 +52,15 @@ export { buildTemplateDocData } from "./template-doc-builder.js";
50
52
  // behaviour composes its own generator instead of filing an issue here.
51
53
  export { requirementTests } from "./requirement-tests.js";
52
54
  export { requirementsFile } from "./requirements-file.js";
55
+ // The `agent` docs surface — internal engine of `meta docs --agent`, like docsFile()
56
+ // and apiDocsFile(). Not a `meta gen` config generator.
57
+ export { agentDocsFile, type AgentDocsFileOpts } from "./agent-docs-file.js";
58
+ export type {
59
+ AgentSchemaInput,
60
+ SchemaColumnLike,
61
+ SchemaTableLike,
62
+ SchemaViewLike,
63
+ } from "./agent-schema-input.js";
53
64
  export type { RequirementRow } from "./requirements-view.js";
54
65
  export type {
55
66
  RequirementTestsOpts,
@@ -0,0 +1,98 @@
1
+ import type { Generator, GenContext, EmittedFile } from "../generator.js";
2
+ import { entityOutputPath, crossEntitySpecifier } from "../import-path.js";
3
+ import { renderNamesDecl } from "../templates/names-decl.js";
4
+ import { namesArtifactSuperOf, resolveObjectNames } from "../names.js";
5
+ import type { MetaObject } from "@metaobjectsdev/metadata";
6
+
7
+ /**
8
+ * §A1/§A2/§A6 — `<Entity>Names`: the physical database names for one object, as constants a
9
+ * hand-written consumer references instead of a string literal.
10
+ *
11
+ * This is the built-in twin of `src/reference/names.ts` (ADR-0034 scaffold-and-own —
12
+ * `meta init` copies the reference file into the adopter's repo; this one stays the
13
+ * engine's internal composer and the reference-byte-identical gate's other half). Same
14
+ * `generate` body; only the import paths differ (relative package-internal here, the
15
+ * public `@metaobjectsdev/codegen-ts` barrel there).
16
+ *
17
+ * Deliberately a SEPARATE generator, never a boolean on the entity generator — a new
18
+ * artifact is a MINOR under docs/compatibility-policy.md and adds zero bytes to existing
19
+ * files, where a flag would move every $table-carrying golden for the same functionality.
20
+ */
21
+ export function namesFile(): Generator {
22
+ return {
23
+ name: "names",
24
+ // §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
25
+ // entity generator can tell whether this artifact will exist. Exactly the mechanism
26
+ // routesFileHono already uses via emitsHonoRoutes/includeHonoRoutes.
27
+ emitsNames: true,
28
+ generate: (ctx: GenContext): EmittedFile[] => {
29
+ const layout = ctx.config.outputLayout ?? "flat";
30
+ const extStyle = ctx.config.extStyle ?? "js";
31
+ // The strategy lives on the RENDER CONTEXT, not on ResolvedGenConfig — `ctx.config`
32
+ // carries outDir/extStyle/dbImport/dialect and nothing about naming.
33
+ const strategy = ctx.renderContext?.columnNamingStrategy;
34
+
35
+ const pathOf = (obj: MetaObject): string =>
36
+ // entityOutputPath, not a bare filename: §A6 makes the entity module IMPORT these
37
+ // constants, so the artifact has to land in the same directory the entity module
38
+ // does. Under outputLayout: "package" a bare name puts it at the target ROOT while
39
+ // its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
40
+ // conflicting-duplicate-path failure as soon as two packages declare a
41
+ // same-bare-named entity.
42
+ entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
43
+
44
+ const superSpecifierFor = (obj: MetaObject): string | undefined => {
45
+ const sup = namesArtifactSuperOf(obj);
46
+ return sup === undefined
47
+ ? undefined
48
+ : crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
49
+ };
50
+
51
+ const out: EmittedFile[] = [];
52
+ // Pass 1 — every matched object that participates in the database (#248).
53
+ // `emitted` tracks what pass 1 actually WROTE, not what it looked at: a matched
54
+ // abstract base emits nothing here, and seeding it as already-emitted is what would
55
+ // make pass 2 skip the very object it exists to produce.
56
+ const emitted = new Set<string>();
57
+ const participants = ctx.entities.filter(ctx.matches);
58
+ for (const entity of participants) {
59
+ const content = renderNamesDecl(entity, {
60
+ strategy, superSpecifier: superSpecifierFor(entity),
61
+ });
62
+ if (content === "") continue; // no primary source ⇒ no names artifact (#248)
63
+ emitted.add(entity.resolutionKey());
64
+ out.push({ path: pathOf(entity), content });
65
+ }
66
+
67
+ // Pass 2 — the abstract bases those participants EXTEND. Each carries the columns it
68
+ // declares, so a child states them once rather than restating its parent's.
69
+ //
70
+ // Reached by walking UP from a participant, never by scanning for abstracts: that is
71
+ // what keeps #248 intact. A sourceless object nothing persistable extends — an
72
+ // `object.value`, say — is not reached, so it acquires no artifact and no phantom
73
+ // participation. Two children of one base both reach it and emit the same file at
74
+ // the same path with the same bytes; the runner collapses byte-identical duplicates
75
+ // (#266), so it is written once. `emitted` keeps that from even arising, and keeps
76
+ // the walk from re-rendering the same base once per child.
77
+ for (const entity of participants) {
78
+ if (resolveObjectNames(entity, strategy) === undefined) continue;
79
+ for (let sup = namesArtifactSuperOf(entity); sup !== undefined;
80
+ sup = namesArtifactSuperOf(sup)) {
81
+ const key = sup.resolutionKey();
82
+ if (emitted.has(key)) break; // already emitted, and so is everything above it
83
+ emitted.add(key);
84
+ // `fragment` says "this is an ancestor render", not which shape to render:
85
+ // `renderNamesDecl` derives that from the object itself, so a TPH base reached
86
+ // by this walk keeps the shared table it owns. Deciding it there rather than
87
+ // here is what makes the EJECTED copy of this generator correct too.
88
+ const content = renderNamesDecl(sup, {
89
+ strategy, superSpecifier: superSpecifierFor(sup), fragment: true,
90
+ });
91
+ if (content === "") continue;
92
+ out.push({ path: pathOf(sup), content });
93
+ }
94
+ }
95
+ return out;
96
+ },
97
+ };
98
+ }
@@ -86,14 +86,48 @@ function renderOne(r: RequirementRow): string {
86
86
  * The generator keys on that to emit no file at all, which is in turn what lets the
87
87
  * surface default to on without changing output for any project lacking a ledger.
88
88
  */
89
- export function renderRequirementsMarkdown(rows: readonly RequirementRow[]): string {
89
+ export interface RequirementsMarkdownOpts {
90
+ /**
91
+ * Render the ledger as a SECTION of a larger page rather than as the page.
92
+ *
93
+ * The `agent/requirements.md` surface carries this ledger under its own `## The ledger`
94
+ * heading, and an embedded copy that keeps its `# Requirements` title gives the page two
95
+ * H1s and reparents every entry as a sibling of the section that contains it — the
96
+ * document silently loses its outline. So the title is dropped and every heading moves
97
+ * down one level.
98
+ *
99
+ * A POST-HOC REGEX OVER THIS FUNCTION'S OUTPUT would do the same thing and would be the
100
+ * wrong shape: heading depth is this renderer's decision (it already caps at h6), and a
101
+ * caller rewriting it from outside would have to re-derive that cap and would drift from
102
+ * it the next time this file changes.
103
+ */
104
+ readonly embedded?: boolean;
105
+ }
106
+
107
+ export function renderRequirementsMarkdown(
108
+ rows: readonly RequirementRow[],
109
+ opts?: RequirementsMarkdownOpts,
110
+ ): string {
90
111
  if (rows.length === 0) return "";
91
112
 
113
+ const body = rows.map(renderOne);
114
+ if (opts?.embedded === true) {
115
+ return [
116
+ `${rows.length} declared requirement${rows.length === 1 ? "" : "s"}, in declaration order.`,
117
+ "",
118
+ // One extra `#` on every heading line, capped at h6 exactly as `heading()` caps.
119
+ ...body.map((entry) =>
120
+ entry.replace(/^(#{1,6}) /gm, (_m, hashes: string) =>
121
+ `${"#".repeat(Math.min(hashes.length + 1, 6))} `,
122
+ ),
123
+ ),
124
+ ].join("\n");
125
+ }
92
126
  return [
93
127
  "# Requirements",
94
128
  "",
95
129
  `${rows.length} declared requirement${rows.length === 1 ? "" : "s"}, in declaration order.`,
96
130
  "",
97
- ...rows.map(renderOne),
131
+ ...body,
98
132
  ].join("\n");
99
133
  }
@@ -4,11 +4,21 @@ import { renderRoutesFileHono } from "../templates/routes-file-hono.js";
4
4
  import { hasAnyRdbSource } from "../source-detect.js";
5
5
  import { formatTs } from "../format.js";
6
6
  import { entityOutputPath } from "../import-path.js";
7
- import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
8
7
  import { isTphSubtype } from "../templates/zod-validators.js";
8
+ import { resolveExpose, type ExposeOption } from "../routes-expose.js";
9
9
 
10
10
  export interface RoutesFileHonoOpts {
11
11
  filter?: (entity: MetaObject) => boolean;
12
+ /**
13
+ * Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
14
+ * absent means all five and emits byte-identical output.
15
+ *
16
+ * routesFileHono({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
17
+ *
18
+ * A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
19
+ * only remove the whole surface, not restrict it to a subset of verbs.
20
+ */
21
+ expose?: ExposeOption;
12
22
  target?: string;
13
23
  }
14
24
 
@@ -22,8 +32,12 @@ export interface RoutesFileHonoOpts {
22
32
  * consumers running Hono can replace hand-written route registration with
23
33
  * this generator output one entity at a time.
24
34
  *
25
- * Per-entity opt-out via `@emitRoutes: false` is honored. If the user
26
- * supplies their own filter, both must pass (AND).
35
+ * If the user supplies their own filter, it AND-composes with the built-in gates.
36
+ *
37
+ * Decide per generator what you consume: wire only the generators whose output you
38
+ * actually import, and narrow this one with its `filter` option. There is no `@emit*`
39
+ * metadata attribute — those were never registered vocabulary, so `meta verify` rejects
40
+ * them (ERR_UNKNOWN_ATTR).
27
41
  *
28
42
  * #248 R2: an object with no declared/inherited source.rdb (of ANY kind) isn't
29
43
  * backed by any store — gated by `hasAnyRdbSource` (does NOT add TPH handling;
@@ -36,17 +50,12 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
36
50
  // written out twice a later edit to one silently makes an entity either stop emitting
37
51
  // without being named as held back, or get warned about while still emitting.
38
52
  const passesOtherGates = (e: MetaObject): boolean =>
39
- // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
40
- e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false
41
- && hasAnyRdbSource(e)
42
- && userFilter(e);
53
+ hasAnyRdbSource(e) && userFilter(e);
43
54
  const generator: Generator = {
44
55
  name: "routes-file-hono",
45
56
  // Marks this as the Hono routes generator so the runner can aggregate
46
57
  // `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
47
58
  emitsHonoRoutes: true,
48
- // ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
49
- //
50
59
  // TPH subtypes are EXCLUDED, matching the Fastify generator. A TPH subtype shares
51
60
  // its base's table, so mounting vanilla CRUD for it produced routes with no
52
61
  // discriminator scoping at all: the list returned EVERY subtype's rows, and
@@ -81,7 +90,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
81
90
  entity.package,
82
91
  `${entity.name}.routes.hono.ts`,
83
92
  ),
84
- content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext)),
93
+ content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
85
94
  };
86
95
  });
87
96
  if (opts?.target) {
@@ -5,16 +5,30 @@ import { isTphSubtype } from "../templates/zod-validators.js";
5
5
  import { hasAnyRdbSource } from "../source-detect.js";
6
6
  import { formatTs } from "../format.js";
7
7
  import { entityOutputPath } from "../import-path.js";
8
- import { CODEGEN_ATTR_EMIT_ROUTES } from "../constants.js";
8
+ import { resolveExpose, type ExposeOption } from "../routes-expose.js";
9
9
 
10
10
  export interface RoutesFileOpts {
11
11
  filter?: (entity: MetaObject) => boolean;
12
+ /**
13
+ * Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
14
+ * absent means all five and emits byte-identical output.
15
+ *
16
+ * routesFile({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
17
+ *
18
+ * A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
19
+ * only remove the whole surface, not restrict it to a subset of verbs.
20
+ */
21
+ expose?: ExposeOption;
12
22
  target?: string;
13
23
  }
14
24
 
15
25
  /**
16
- * Per-entity opt-out via `@emitRoutes: false` is honored. If the user supplies
17
- * their own filter, both must pass (AND).
26
+ * If the user supplies their own filter, it AND-composes with the built-in gates.
27
+ *
28
+ * Decide per generator what you consume: wire only the generators whose output you
29
+ * actually import, and narrow this one with its `filter` option. There is no `@emit*`
30
+ * metadata attribute — those were never registered vocabulary, so `meta verify` rejects
31
+ * them (ERR_UNKNOWN_ATTR).
18
32
  *
19
33
  * #248 R2: an object with no declared/inherited source.rdb (of ANY kind) isn't
20
34
  * backed by any store — routes against it would import Drizzle table/allowlist
@@ -27,17 +41,16 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
27
41
  const userFilter = opts?.filter ?? (() => true);
28
42
  const generator: Generator = {
29
43
  name: "routes-file",
30
- // Always set: AND-composes metadata opt-out with optional user filter.
44
+ // Always set: AND-composes the built-in gates with the optional user filter.
31
45
  filter: (e: MetaObject) =>
32
- // ADR-0039: resolving a concrete entity may inherit its @emit* opt-out flag via extends.
33
- e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
46
+ hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
34
47
  generate: perEntity(async (entity, ctx) => {
35
48
  if (!ctx.renderContext) {
36
49
  throw new Error("routes-file: renderContext is required (provided by runGen)");
37
50
  }
38
51
  return {
39
52
  path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.routes.ts`),
40
- content: await formatTs(renderRoutesFile(entity, ctx.renderContext)),
53
+ content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
41
54
  };
42
55
  }),
43
56
  };
@@ -5,13 +5,27 @@
5
5
  // generators; just adds the "Mustache template" + "walk that yields a
6
6
  // data dict per output" primitives.
7
7
  //
8
- // Design line we adopted (from the design doc):
9
- // Code → hand-coded generators (ts-poet, idiomatic per-port).
10
- // Documents templateGenerator (shared Mustache templates).
8
+ // SUPERSEDED, and left here as a correction because the old line still circulates:
9
+ // the original 2026-05-28 split was "Code → hand-coded generators, Documents
10
+ // templateGenerator", with code emission an explicit NON-GOAL for this primitive.
11
+ // SP-1 (docs/superpowers/specs/2026-06-28-mustache-codegen-parity-design.md §2)
12
+ // moved past it — its goal is a consumer authoring "a working code generator, on ANY
13
+ // port, with NO generator code", and its worked outputPattern is
14
+ // "{package}/{name}Service.java". Source, not a document.
11
15
  //
12
- // docsFile() is the first templateGenerator instance (rc.12). OpenAPI specs,
13
- // Mermaid diagrams, HTML doc sites, etc. follow as templates + a walk
14
- // function each.
16
+ // The axis is NOT what kind of file comes out. It is: pick a template when the output
17
+ // SHAPE is what you are iterating on, or when you want the same output across
18
+ // languages (that is what the neutral data dict buys); pick a hand-coded generator
19
+ // when the logic is gnarly or the run is hot. Tradeoff table:
20
+ // docs/features/codegen-concepts.md §3.
21
+ //
22
+ // This matters most where a port has no choice: C# and Python expose closed built-in
23
+ // generator registries, so the declarative spec is the ONLY consumer authoring path
24
+ // there — and a documents-only reading of this primitive would tell those adopters
25
+ // their port cannot emit a custom shape at all.
26
+ //
27
+ // docsFile() is the first templateGenerator instance (rc.12), which is why the
28
+ // documents framing stuck; it is the first instance, not the boundary.
15
29
 
16
30
  import type { MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
17
31
  import { render, type Provider, type RenderFormat } from "@metaobjectsdev/render";