@metaobjectsdev/codegen-ts 0.24.4 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/README.md +37 -12
  2. package/dist/api-surface.d.ts +29 -0
  3. package/dist/api-surface.d.ts.map +1 -1
  4. package/dist/api-surface.js +47 -0
  5. package/dist/api-surface.js.map +1 -1
  6. package/dist/client-directive.d.ts +18 -0
  7. package/dist/client-directive.d.ts.map +1 -0
  8. package/dist/client-directive.js +37 -0
  9. package/dist/client-directive.js.map +1 -0
  10. package/dist/column-mapper.d.ts +12 -0
  11. package/dist/column-mapper.d.ts.map +1 -1
  12. package/dist/column-mapper.js +1 -0
  13. package/dist/column-mapper.js.map +1 -1
  14. package/dist/constants.d.ts +24 -11
  15. package/dist/constants.d.ts.map +1 -1
  16. package/dist/constants.js +54 -19
  17. package/dist/constants.js.map +1 -1
  18. package/dist/db-emitting.d.ts +6 -0
  19. package/dist/db-emitting.d.ts.map +1 -0
  20. package/dist/db-emitting.js +26 -0
  21. package/dist/db-emitting.js.map +1 -0
  22. package/dist/generator-registry.d.ts.map +1 -1
  23. package/dist/generator-registry.js +7 -1
  24. package/dist/generator-registry.js.map +1 -1
  25. package/dist/generator.d.ts +5 -0
  26. package/dist/generator.d.ts.map +1 -1
  27. package/dist/generator.js.map +1 -1
  28. package/dist/generators/agent-docs-file.d.ts +19 -0
  29. package/dist/generators/agent-docs-file.d.ts.map +1 -0
  30. package/dist/generators/agent-docs-file.js +257 -0
  31. package/dist/generators/agent-docs-file.js.map +1 -0
  32. package/dist/generators/agent-requirements-page.d.ts +22 -0
  33. package/dist/generators/agent-requirements-page.d.ts.map +1 -0
  34. package/dist/generators/agent-requirements-page.js +175 -0
  35. package/dist/generators/agent-requirements-page.js.map +1 -0
  36. package/dist/generators/agent-schema-input.d.ts +69 -0
  37. package/dist/generators/agent-schema-input.d.ts.map +1 -0
  38. package/dist/generators/agent-schema-input.js +23 -0
  39. package/dist/generators/agent-schema-input.js.map +1 -0
  40. package/dist/generators/agent-schema-page.d.ts +21 -0
  41. package/dist/generators/agent-schema-page.d.ts.map +1 -0
  42. package/dist/generators/agent-schema-page.js +241 -0
  43. package/dist/generators/agent-schema-page.js.map +1 -0
  44. package/dist/generators/agent-ui-page.d.ts +22 -0
  45. package/dist/generators/agent-ui-page.d.ts.map +1 -0
  46. package/dist/generators/agent-ui-page.js +253 -0
  47. package/dist/generators/agent-ui-page.js.map +1 -0
  48. package/dist/generators/api-docs-file.d.ts.map +1 -1
  49. package/dist/generators/api-docs-file.js +13 -6
  50. package/dist/generators/api-docs-file.js.map +1 -1
  51. package/dist/generators/api-model.d.ts +6 -0
  52. package/dist/generators/api-model.d.ts.map +1 -1
  53. package/dist/generators/api-model.js +54 -35
  54. package/dist/generators/api-model.js.map +1 -1
  55. package/dist/generators/callable-file.js +1 -1
  56. package/dist/generators/callable-file.js.map +1 -1
  57. package/dist/generators/docs-data-builder.d.ts.map +1 -1
  58. package/dist/generators/docs-data-builder.js +47 -2
  59. package/dist/generators/docs-data-builder.js.map +1 -1
  60. package/dist/generators/docs-data.d.ts +6 -2
  61. package/dist/generators/docs-data.d.ts.map +1 -1
  62. package/dist/generators/index.d.ts +3 -0
  63. package/dist/generators/index.d.ts.map +1 -1
  64. package/dist/generators/index.js +4 -0
  65. package/dist/generators/index.js.map +1 -1
  66. package/dist/generators/names-file.d.ts +17 -0
  67. package/dist/generators/names-file.d.ts.map +1 -0
  68. package/dist/generators/names-file.js +95 -0
  69. package/dist/generators/names-file.js.map +1 -0
  70. package/dist/generators/requirements-markdown.d.ts +18 -1
  71. package/dist/generators/requirements-markdown.d.ts.map +1 -1
  72. package/dist/generators/requirements-markdown.js +11 -9
  73. package/dist/generators/requirements-markdown.js.map +1 -1
  74. package/dist/generators/routes-file-hono.d.ts +17 -2
  75. package/dist/generators/routes-file-hono.d.ts.map +1 -1
  76. package/dist/generators/routes-file-hono.js +15 -12
  77. package/dist/generators/routes-file-hono.js.map +1 -1
  78. package/dist/generators/routes-file.d.ts +17 -2
  79. package/dist/generators/routes-file.d.ts.map +1 -1
  80. package/dist/generators/routes-file.js +10 -8
  81. package/dist/generators/routes-file.js.map +1 -1
  82. package/dist/generators/template-generator.d.ts.map +1 -1
  83. package/dist/generators/template-generator.js +20 -6
  84. package/dist/generators/template-generator.js.map +1 -1
  85. package/dist/index.d.ts +21 -5
  86. package/dist/index.d.ts.map +1 -1
  87. package/dist/index.js +40 -4
  88. package/dist/index.js.map +1 -1
  89. package/dist/metaobjects-config.d.ts +67 -1
  90. package/dist/metaobjects-config.d.ts.map +1 -1
  91. package/dist/metaobjects-config.js +10 -1
  92. package/dist/metaobjects-config.js.map +1 -1
  93. package/dist/names.d.ts +275 -0
  94. package/dist/names.d.ts.map +1 -0
  95. package/dist/names.js +434 -0
  96. package/dist/names.js.map +1 -0
  97. package/dist/naming.d.ts +17 -0
  98. package/dist/naming.d.ts.map +1 -1
  99. package/dist/naming.js +21 -0
  100. package/dist/naming.js.map +1 -1
  101. package/dist/overwrite-policy.d.ts.map +1 -1
  102. package/dist/overwrite-policy.js +15 -3
  103. package/dist/overwrite-policy.js.map +1 -1
  104. package/dist/payload-codegen.d.ts.map +1 -1
  105. package/dist/payload-codegen.js +4 -3
  106. package/dist/payload-codegen.js.map +1 -1
  107. package/dist/projection/extract-view-spec.d.ts +25 -1
  108. package/dist/projection/extract-view-spec.d.ts.map +1 -1
  109. package/dist/projection/extract-view-spec.js +79 -6
  110. package/dist/projection/extract-view-spec.js.map +1 -1
  111. package/dist/prompt-generator-gate.d.ts +27 -0
  112. package/dist/prompt-generator-gate.d.ts.map +1 -0
  113. package/dist/prompt-generator-gate.js +62 -0
  114. package/dist/prompt-generator-gate.js.map +1 -0
  115. package/dist/reference-templates.d.ts +22 -5
  116. package/dist/reference-templates.d.ts.map +1 -1
  117. package/dist/reference-templates.js +55 -21
  118. package/dist/reference-templates.js.map +1 -1
  119. package/dist/render-context.d.ts +20 -1
  120. package/dist/render-context.d.ts.map +1 -1
  121. package/dist/render-context.js +2 -0
  122. package/dist/render-context.js.map +1 -1
  123. package/dist/retired-codegen-attrs.d.ts +32 -0
  124. package/dist/retired-codegen-attrs.d.ts.map +1 -0
  125. package/dist/retired-codegen-attrs.js +47 -0
  126. package/dist/retired-codegen-attrs.js.map +1 -0
  127. package/dist/routes-expose.d.ts +63 -0
  128. package/dist/routes-expose.d.ts.map +1 -0
  129. package/dist/routes-expose.js +57 -0
  130. package/dist/routes-expose.js.map +1 -0
  131. package/dist/runner.d.ts +18 -0
  132. package/dist/runner.d.ts.map +1 -1
  133. package/dist/runner.js +115 -7
  134. package/dist/runner.js.map +1 -1
  135. package/dist/templates/callable-file.d.ts +7 -2
  136. package/dist/templates/callable-file.d.ts.map +1 -1
  137. package/dist/templates/callable-file.js +54 -9
  138. package/dist/templates/callable-file.js.map +1 -1
  139. package/dist/templates/drizzle-schema.d.ts.map +1 -1
  140. package/dist/templates/drizzle-schema.js +236 -65
  141. package/dist/templates/drizzle-schema.js.map +1 -1
  142. package/dist/templates/entity-constants.d.ts +15 -6
  143. package/dist/templates/entity-constants.d.ts.map +1 -1
  144. package/dist/templates/entity-constants.js +85 -161
  145. package/dist/templates/entity-constants.js.map +1 -1
  146. package/dist/templates/entity-file.d.ts.map +1 -1
  147. package/dist/templates/entity-file.js +38 -4
  148. package/dist/templates/entity-file.js.map +1 -1
  149. package/dist/templates/entity-meta-file.d.ts +15 -1
  150. package/dist/templates/entity-meta-file.d.ts.map +1 -1
  151. package/dist/templates/entity-meta-file.js +12 -2
  152. package/dist/templates/entity-meta-file.js.map +1 -1
  153. package/dist/templates/entity-ui-descriptor.d.ts +112 -0
  154. package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
  155. package/dist/templates/entity-ui-descriptor.js +238 -0
  156. package/dist/templates/entity-ui-descriptor.js.map +1 -0
  157. package/dist/templates/field-meta.d.ts +46 -9
  158. package/dist/templates/field-meta.d.ts.map +1 -1
  159. package/dist/templates/field-meta.js +95 -21
  160. package/dist/templates/field-meta.js.map +1 -1
  161. package/dist/templates/names-decl.d.ts +38 -0
  162. package/dist/templates/names-decl.d.ts.map +1 -0
  163. package/dist/templates/names-decl.js +104 -0
  164. package/dist/templates/names-decl.js.map +1 -0
  165. package/dist/templates/output-parser.d.ts.map +1 -1
  166. package/dist/templates/output-parser.js +54 -8
  167. package/dist/templates/output-parser.js.map +1 -1
  168. package/dist/templates/projection-decl.d.ts +21 -0
  169. package/dist/templates/projection-decl.d.ts.map +1 -1
  170. package/dist/templates/projection-decl.js +53 -29
  171. package/dist/templates/projection-decl.js.map +1 -1
  172. package/dist/templates/queries-file.d.ts.map +1 -1
  173. package/dist/templates/queries-file.js +17 -2
  174. package/dist/templates/queries-file.js.map +1 -1
  175. package/dist/templates/render-helper.d.ts.map +1 -1
  176. package/dist/templates/render-helper.js +3 -1
  177. package/dist/templates/render-helper.js.map +1 -1
  178. package/dist/templates/requirement-test.d.ts.map +1 -1
  179. package/dist/templates/requirement-test.js +48 -21
  180. package/dist/templates/requirement-test.js.map +1 -1
  181. package/dist/templates/routes-file-hono.d.ts +2 -1
  182. package/dist/templates/routes-file-hono.d.ts.map +1 -1
  183. package/dist/templates/routes-file-hono.js +11 -2
  184. package/dist/templates/routes-file-hono.js.map +1 -1
  185. package/dist/templates/routes-file.d.ts +2 -1
  186. package/dist/templates/routes-file.d.ts.map +1 -1
  187. package/dist/templates/routes-file.js +54 -21
  188. package/dist/templates/routes-file.js.map +1 -1
  189. package/dist/templates/value-object-file.d.ts.map +1 -1
  190. package/dist/templates/value-object-file.js +15 -1
  191. package/dist/templates/value-object-file.js.map +1 -1
  192. package/dist/templates/view-decl.d.ts +37 -1
  193. package/dist/templates/view-decl.d.ts.map +1 -1
  194. package/dist/templates/view-decl.js +27 -3
  195. package/dist/templates/view-decl.js.map +1 -1
  196. package/dist/templates/zod-validators.d.ts +21 -0
  197. package/dist/templates/zod-validators.d.ts.map +1 -1
  198. package/dist/templates/zod-validators.js +40 -5
  199. package/dist/templates/zod-validators.js.map +1 -1
  200. package/dist/view-context.d.ts +24 -0
  201. package/dist/view-context.d.ts.map +1 -0
  202. package/dist/view-context.js +81 -0
  203. package/dist/view-context.js.map +1 -0
  204. package/package.json +6 -6
  205. package/src/api-surface.ts +53 -0
  206. package/src/client-directive.ts +36 -0
  207. package/src/column-mapper.ts +13 -0
  208. package/src/constants.ts +91 -16
  209. package/src/db-emitting.ts +31 -0
  210. package/src/generator-registry.ts +7 -0
  211. package/src/generator.ts +5 -0
  212. package/src/generators/agent-docs-file.ts +297 -0
  213. package/src/generators/agent-requirements-page.ts +199 -0
  214. package/src/generators/agent-schema-input.ts +88 -0
  215. package/src/generators/agent-schema-page.ts +297 -0
  216. package/src/generators/agent-ui-page.ts +290 -0
  217. package/src/generators/api-docs-file.ts +13 -6
  218. package/src/generators/api-model.ts +63 -34
  219. package/src/generators/callable-file.ts +1 -1
  220. package/src/generators/docs-data-builder.ts +46 -2
  221. package/src/generators/docs-data.ts +6 -2
  222. package/src/generators/index.ts +10 -0
  223. package/src/generators/names-file.ts +98 -0
  224. package/src/generators/requirements-markdown.ts +36 -2
  225. package/src/generators/routes-file-hono.ts +26 -15
  226. package/src/generators/routes-file.ts +20 -7
  227. package/src/generators/template-generator.ts +20 -6
  228. package/src/index.ts +63 -5
  229. package/src/metaobjects-config.ts +78 -2
  230. package/src/names.ts +630 -0
  231. package/src/naming.ts +23 -0
  232. package/src/overwrite-policy.ts +15 -3
  233. package/src/payload-codegen.ts +4 -3
  234. package/src/projection/extract-view-spec.ts +85 -6
  235. package/src/prompt-generator-gate.ts +73 -0
  236. package/src/reference/barrel.ts +3 -0
  237. package/src/reference/entity.ts +28 -2
  238. package/src/reference/names.ts +125 -0
  239. package/src/reference/queries.ts +14 -2
  240. package/src/reference/routes-hono.ts +113 -0
  241. package/src/reference/routes.ts +30 -9
  242. package/src/reference-templates.ts +61 -21
  243. package/src/render-context.ts +22 -1
  244. package/src/retired-codegen-attrs.ts +52 -0
  245. package/src/routes-expose.ts +86 -0
  246. package/src/runner.ts +129 -11
  247. package/src/templates/callable-file.ts +60 -8
  248. package/src/templates/drizzle-schema.ts +248 -60
  249. package/src/templates/entity-constants.ts +95 -184
  250. package/src/templates/entity-file.ts +46 -4
  251. package/src/templates/entity-meta-file.ts +16 -3
  252. package/src/templates/entity-ui-descriptor.ts +305 -0
  253. package/src/templates/field-meta.ts +100 -19
  254. package/src/templates/names-decl.ts +165 -0
  255. package/src/templates/output-parser.ts +54 -7
  256. package/src/templates/projection-decl.ts +72 -31
  257. package/src/templates/queries-file.ts +17 -2
  258. package/src/templates/render-helper.ts +3 -1
  259. package/src/templates/requirement-test.ts +49 -18
  260. package/src/templates/routes-file-hono.ts +14 -2
  261. package/src/templates/routes-file.ts +69 -24
  262. package/src/templates/value-object-file.ts +15 -1
  263. package/src/templates/view-decl.ts +53 -3
  264. package/src/templates/zod-validators.ts +41 -5
  265. package/src/view-context.ts +88 -0
@@ -0,0 +1,290 @@
1
+ // `agent/ui.md` — the generated UI surface, for an agent about to touch a form or a grid.
2
+ //
3
+ // The gap this closes is narrow and real: the neutral model entity page renders NO
4
+ // `view.*` or `layout.*` metadata at all, so an agent asked to change a form had nowhere
5
+ // to learn what the form already is and would read the generated TSX — the disposable
6
+ // artifact — as if it were the source.
7
+ //
8
+ // EVERY FIELD ROW COMES FROM `buildEntityUiDescriptor`, the same derivation
9
+ // `renderEntityConstants` emits as the `<Entity>` const in `<Entity>.meta.ts`, which is
10
+ // what `useEntityForm` reads at runtime. One derivation, two renderings: the page cannot
11
+ // describe a control the form does not render, because both answers come from the same
12
+ // call. That is the point of the descriptor extraction, not a nicety.
13
+ //
14
+ // The three columns the descriptor does NOT carry — `@formExclude`, `@filterable` and
15
+ // `@sortable` — are read here from the field, and `@sortable`'s inherit-from-`@filterable`
16
+ // default resolves through `isSortableField`, the SAME predicate the generated
17
+ // `<Entity>SortAllowlist` is built from, so the page cannot say a field is sortable that
18
+ // the endpoint will reject. They are deliberately absent from the descriptor: the
19
+ // descriptor describes how a field is PRESENTED, while those three say whether it appears
20
+ // at all and what the LIST endpoint will accept, which is a different question the form
21
+ // control has no opinion on.
22
+ //
23
+ // THREE THINGS THE DESCRIPTOR ALONE CANNOT ANSWER, each read from the predicate the
24
+ // generator that decides it uses, never re-derived here:
25
+ //
26
+ // • WHERE the object is served — `restPath`, because a TPH subtype is mounted under its
27
+ // discriminator base and its own `$path` names nothing;
28
+ // • WHETHER a form exists at all — `hasGeneratedForm`, the form generator's own filter,
29
+ // because a discriminator base has a write endpoint and still gets no form;
30
+ // • WHAT the control is for a `field.object` — `valueObjectFor`, shared with the form
31
+ // generator, because a resolvable `@objectRef` is rendered as a nested sub-form and
32
+ // the field's view kind is not consulted.
33
+
34
+ import {
35
+ FIELD_ATTR_FILTERABLE,
36
+ FIELD_ATTR_FORM_EXCLUDE,
37
+ LAYOUT_DATA_GRID_ATTR_COLUMNS,
38
+ LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
39
+ LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER,
40
+ LAYOUT_DATA_GRID_ATTR_PAGE_SIZE,
41
+ LAYOUT_SUBTYPE_DATA_GRID,
42
+ TYPE_LAYOUT,
43
+ } from "@metaobjectsdev/metadata";
44
+ import type { MetaData, MetaObject, MetaRoot } from "@metaobjectsdev/metadata";
45
+ import { GENERATED_HEADER } from "../constants.js";
46
+ import { hasGeneratedForm, restPath, servedPath, servesReadApi } from "../api-surface.js";
47
+ import {
48
+ buildEntityUiDescriptor,
49
+ type UiFieldDescriptor,
50
+ type UiRule,
51
+ } from "../templates/entity-ui-descriptor.js";
52
+ import { isSortableField } from "../templates/filter-shared.js";
53
+ import { isTphDiscriminatorBase } from "../templates/tph-discriminator.js";
54
+ import { declaresTphDiscriminator } from "../templates/zod-validators.js";
55
+
56
+ const GENERATED_MARKER = `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`;
57
+
58
+ function mdCell(text: string): string {
59
+ return text.replace(/\|/g, "\\|");
60
+ }
61
+
62
+ /** One rule, as a reader wants to see it — the value, not the message. */
63
+ function ruleText(r: UiRule): string {
64
+ switch (r.kind) {
65
+ case "required":
66
+ return "required";
67
+ case "minLength":
68
+ return `minLength ${r.value}`;
69
+ case "maxLength":
70
+ return `maxLength ${r.value}`;
71
+ case "pattern":
72
+ return `pattern \`${mdCell(r.pattern)}\``;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * The CONTROL cell — what the generated form renders, which is not always a `view.*`
78
+ * subtype.
79
+ *
80
+ * A `field.object` whose `@objectRef` resolves is emitted as a nested `<fieldset>`
81
+ * sub-form (a `useFieldArray` repeatable group when the field is an array), and the form
82
+ * generator never consults the field's view kind for it. This column used to print that
83
+ * view kind — `text`, the `defaultViewForSubType` fallback — which said the control was a
84
+ * free-text input for a field that has no input at all. Same shape as the `field.enum`
85
+ * descriptor bug, one subtype family over; the predicate is shared with the form
86
+ * generator (`valueObjectFor`) so the two cannot answer differently again.
87
+ */
88
+ function controlCell(f: UiFieldDescriptor): string {
89
+ if (f.nested === undefined) return `\`${f.view}\``;
90
+ return f.nested.isArray ? "nested sub-form (repeatable)" : "nested sub-form";
91
+ }
92
+
93
+ /** The HTML type cell — empty for a nested sub-form, which is not an input at all. */
94
+ function htmlTypeCell(f: UiFieldDescriptor): string {
95
+ if (f.nested !== undefined || f.htmlType === undefined) return "";
96
+ return `\`${f.htmlType}\``;
97
+ }
98
+
99
+ /** `yes` / `` — an empty cell reads better than a column of "no". */
100
+ function flag(value: unknown): string {
101
+ return value === true ? "yes" : "";
102
+ }
103
+
104
+ /**
105
+ * The endpoint line under an object's heading.
106
+ *
107
+ * THE `apiPrefix` IS PART OF THE ADDRESS. `routes-file.ts` registers every mount inside
108
+ * `fastify.register(…, { prefix: apiPrefix })`, so a project configuring `/api` serves
109
+ * `/api/owners` and nothing at `/owners`. This page states that its heading is the address
110
+ * the routes mount at, so omitting the prefix made that promise false for every project
111
+ * that sets one — and a reader following it gets a 404.
112
+ *
113
+ * `restPath`, NOT `descriptor.path`: a TPH subtype's own `$path` names an address
114
+ * nothing mounts — the hierarchy is mounted from the discriminator base, each subtype
115
+ * at `<base>/<segment>` — and this page printed that non-existent address as fact.
116
+ * Whether a form exists is `hasGeneratedForm` — the form generator's OWN filter —
117
+ * never `servesWriteApi`. A read-only object (a projection, a view-backed entity) has
118
+ * no Insert/Update schema; a TPH discriminator BASE has both and still gets no form,
119
+ * because you cannot create a base and its polymorphic mount is read-only by
120
+ * construction. Saying so is the difference between "the form is missing" and "there
121
+ * is deliberately no form"; asking the endpoint question announced one for every base.
122
+ */
123
+ function endpointLine(obj: MetaObject, root: MetaRoot, apiPrefix: string): string {
124
+ const endpoint = servedPath(obj, apiPrefix);
125
+ if (hasGeneratedForm(obj)) return `Endpoint \`${endpoint}\`.`;
126
+ // `isTphDiscriminatorBase`, which requires at least one CONCRETE subtype — the same
127
+ // predicate `routes-file.ts` switches on. `@discriminator` with no subtype yet is a
128
+ // refactor-in-progress shape: the routes generator emits the VANILLA full-CRUD file for
129
+ // it, so "each concrete subtype below has its own" would name subtypes that do not
130
+ // exist, on an object that does have a write endpoint.
131
+ // THREE reasons, not two. `isTphDiscriminatorBase` requires at least one CONCRETE
132
+ // subtype — the same predicate `routes-file.ts` switches on — and the case it excludes
133
+ // is real: an object declaring `@discriminator` with no subtype yet gets the VANILLA
134
+ // full-CRUD routes file, so it is NOT read-only, while the form generator declines it
135
+ // for declaring `@discriminator` at all. Calling that "read-only" is false in the one
136
+ // direction that matters, since a reader would conclude it cannot be written to.
137
+ const reason = isTphDiscriminatorBase(obj, root)
138
+ ? "for a discriminator base — its own mount is list/get only, and each concrete " +
139
+ "subtype below has its own form"
140
+ : declaresTphDiscriminator(obj)
141
+ ? "— the form generator declines any object declaring `@discriminator`, and this " +
142
+ "one has no concrete subtype yet. Its routes are the ordinary full CRUD set"
143
+ : "(read-only)";
144
+ return (
145
+ `Endpoint \`${endpoint}\` — **no form is generated** ${reason}. ` +
146
+ "The fields below describe the grid and the filters."
147
+ );
148
+ }
149
+
150
+ /** The `layout.dataGrid` children an object declares. ADR-0039: resolving. */
151
+ function dataGrids(obj: MetaObject): MetaData[] {
152
+ return obj
153
+ .children()
154
+ .filter((c) => c.type === TYPE_LAYOUT && c.subType === LAYOUT_SUBTYPE_DATA_GRID);
155
+ }
156
+
157
+ /**
158
+ * True when a UI generator would emit for this object.
159
+ *
160
+ * `servesReadApi` — the api-surface predicate the hook, grid and form generators
161
+ * themselves gate on — NOT "has fields" and never an object-subtype test. A form, a grid
162
+ * and a hook are all clients of a generated endpoint, so an object with no endpoint has no
163
+ * UI to document.
164
+ *
165
+ * Getting this wrong is not cosmetic. Gating on "has fields" put a prompt payload
166
+ * (`object.value`, no source, no routes) on the page under a heading that announced an
167
+ * endpoint derived from its name — an address that does not exist, stated as fact, on the
168
+ * page an agent is told to trust. Instance artifacts derive from a declared SOURCE; the
169
+ * UI tier asks the endpoint question and never a storage or subtype one.
170
+ */
171
+ export function hasUiSurface(obj: MetaObject): boolean {
172
+ return servesReadApi(obj);
173
+ }
174
+
175
+ function gridSection(grid: MetaData): string[] {
176
+ const out: string[] = [];
177
+ out.push(`**Grid \`${grid.name}\`**`);
178
+ out.push("");
179
+ const columns = grid.attr(LAYOUT_DATA_GRID_ATTR_COLUMNS);
180
+ if (Array.isArray(columns) && columns.length > 0) {
181
+ out.push(`- columns: ${columns.map((c) => `\`${String(c)}\``).join(", ")}`);
182
+ }
183
+ const sortField = grid.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD);
184
+ if (typeof sortField === "string" && sortField !== "") {
185
+ const order = grid.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER);
186
+ const suffix = typeof order === "string" && order !== "" ? `:${order}` : "";
187
+ out.push(`- default sort: \`${sortField}${suffix}\``);
188
+ }
189
+ const pageSize = grid.attr(LAYOUT_DATA_GRID_ATTR_PAGE_SIZE);
190
+ if (typeof pageSize === "number") out.push(`- page size: ${pageSize}`);
191
+ return out;
192
+ }
193
+
194
+ /**
195
+ * Render the page. Returns "" when no object in the model has a UI surface — the surface
196
+ * then emits no FILE, so a headless project sees nothing rather than an empty page.
197
+ */
198
+ export function renderAgentUiPage(root: MetaRoot, apiPrefix = ""): string {
199
+ const objects = root.objects();
200
+ const withUi = objects.filter(hasUiSurface);
201
+ if (withUi.length === 0) return "";
202
+
203
+ const out: string[] = [];
204
+ out.push(GENERATED_MARKER);
205
+ out.push("");
206
+ out.push("# UI");
207
+ out.push("");
208
+ out.push(
209
+ "What the generated forms and grids already are. Read it before changing a form, a " +
210
+ "grid or a filter — the generated `.tsx` is the disposable artifact, this is what " +
211
+ "produced it.",
212
+ );
213
+ out.push("");
214
+ out.push(
215
+ "- Every row here is the SAME derivation the runtime reads — `buildEntityUiDescriptor`, " +
216
+ "which is emitted as the `<Entity>` const in `<Entity>.meta.ts` and consumed by " +
217
+ "`useEntityForm`. Changing the metadata changes both. A READ-ONLY projection's const " +
218
+ "carries the subset that applies to it (no form, so no rules and no HTML type) plus " +
219
+ "the `dbCol` only a view-backed const has.",
220
+ );
221
+ out.push(
222
+ "- `Control` is what the FORM renders. A field declaring several views is described by " +
223
+ "the one named `form`; a `field.object` whose `@objectRef` resolves is not an input " +
224
+ "at all but a nested sub-form over that value object, and says so.",
225
+ );
226
+ out.push(
227
+ "- `Filter` / `Sort` are what the generated LIST endpoint accepts. `@sortable` " +
228
+ "defaults to `@filterable`, and the resolved answer is what is shown.",
229
+ );
230
+ out.push("");
231
+
232
+ for (const obj of withUi) {
233
+ out.push(`## \`${obj.resolutionKey()}\``);
234
+ out.push("");
235
+ const descriptor = buildEntityUiDescriptor(obj, root);
236
+ out.push(endpointLine(obj, root, apiPrefix));
237
+ out.push("");
238
+ if (descriptor.fields.length > 0) {
239
+ // The field nodes, keyed by name, so the three non-descriptor columns can be read
240
+ // off the field the descriptor row came from.
241
+ const byName = new Map(obj.fields().map((f) => [f.name, f]));
242
+ out.push("| Field | Label | Control | HTML type | Rules | Excluded | Filter | Sort |");
243
+ out.push("|---|---|---|---|---|---|---|---|");
244
+ for (const f of descriptor.fields) {
245
+ const node = byName.get(f.name);
246
+ const cells = [
247
+ `\`${f.name}\``,
248
+ mdCell(f.label),
249
+ controlCell(f),
250
+ htmlTypeCell(f),
251
+ f.rules.map(ruleText).join(" · "),
252
+ node === undefined ? "" : flag(node.attr(FIELD_ATTR_FORM_EXCLUDE)),
253
+ node === undefined ? "" : flag(node.attr(FIELD_ATTR_FILTERABLE)),
254
+ node === undefined ? "" : flag(isSortableField(node)),
255
+ ];
256
+ out.push(`| ${cells.join(" | ")} |`);
257
+ }
258
+ // Which value object a nested field expands into rides below the table: it is the
259
+ // one thing a reader needs to go and edit, and it does not fit a cell.
260
+ const nested = descriptor.fields.filter((f) => f.nested !== undefined);
261
+ if (nested.length > 0) {
262
+ out.push("");
263
+ for (const f of nested) {
264
+ out.push(
265
+ `- \`${f.name}\` — expands \`${f.nested?.objectRef}\`` +
266
+ `${f.nested?.isArray === true ? ", one group per element" : ""}. Change the ` +
267
+ "fields inside it on that value object, not here.",
268
+ );
269
+ }
270
+ }
271
+ const money = descriptor.fields.filter((f) => f.currency !== undefined);
272
+ if (money.length > 0) {
273
+ out.push("");
274
+ for (const f of money) {
275
+ out.push(
276
+ `- \`${f.name}\` — money: \`${f.currency?.currency}\` formatted for ` +
277
+ `\`${f.currency?.locale}\`. Stored and sent as INTEGER MINOR UNITS; never format it server-side.`,
278
+ );
279
+ }
280
+ }
281
+ }
282
+ for (const grid of dataGrids(obj)) {
283
+ out.push("");
284
+ out.push(...gridSection(grid));
285
+ }
286
+ out.push("");
287
+ }
288
+
289
+ return `${out.join("\n").replace(/\n+$/, "")}\n`;
290
+ }
@@ -18,12 +18,16 @@
18
18
  // Part 3) — so it appears in `gen --list` and is selectable by its stable name
19
19
  // `api-docs`. It is NOT a `meta docs` mode.
20
20
  //
21
- // It is NOT (yet) part of the default `meta gen` scaffold suite: it is
22
- // registry-listed but not auto-run. Turning it on by default in the scaffold,
23
- // and surfacing the agent form (AGENT-API.md) to a coding agent via a pointer
24
- // from the installed `.metaobjects/` context, are tracked as agent-context-
25
- // coordination follow-ups deliberately deferred here to avoid colliding with
26
- // the live agent-context work.
21
+ // It is NOT part of the default `meta gen` scaffold suite: it is registry-listed but not
22
+ // auto-run. `meta docs` is the door that emits it.
23
+ //
24
+ // THE POINTER IS NO LONGER DEFERRED. This header used to record "surfacing the agent form
25
+ // (AGENT-API.md) to a coding agent via a pointer from the installed `.metaobjects/`
26
+ // context" as an agent-context-coordination follow-up. It was deferred, no roadmap item
27
+ // tracked it, and for as long as that held, the page existed and nothing routed an agent
28
+ // to it. `agent-context/templates/always-on.md.mustache` now names this file and the three
29
+ // `agent/` pages, and says when to read each. If you rename or move this artifact, that
30
+ // template is the other half.
27
31
 
28
32
  import type { MetaObject } from "@metaobjectsdev/metadata";
29
33
  import type { Generator, GeneratorFactory, EmittedFile } from "../generator.js";
@@ -95,6 +99,9 @@ export const apiDocsFile = function apiDocsFile(opts?: ApiDocsFileOpts): Generat
95
99
  loadedRoot: ctx.loadedRoot,
96
100
  outputLayout: layout,
97
101
  includeHonoRoutes: ctx.config.includeHonoRoutes ?? false,
102
+ // The project's prefix, because every route is mounted inside it. Without
103
+ // this the page documents `/authors` for routes served at `/api/authors`.
104
+ apiPrefix: ctx.renderContext?.apiPrefix ?? "",
98
105
  ...(ctx.renderContext?.pkMap !== undefined && { pkMap: ctx.renderContext.pkMap }),
99
106
  });
100
107
 
@@ -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;
@@ -13,6 +13,7 @@ export { callableFile, type CallableFileOpts } from "./callable-file.js";
13
13
  /** @deprecated ADR-0034 — own a copy instead: `import { routesFile } from "./codegen/generators/routes"` (scaffolded by `meta init`). */
14
14
  export { routesFile, type RoutesFileOpts } from "./routes-file.js";
15
15
  export { routesFileHono, type RoutesFileHonoOpts } from "./routes-file-hono.js";
16
+ export { namesFile } from "./names-file.js";
16
17
  /** @deprecated ADR-0034 — own a copy instead: `import { barrel } from "./codegen/generators/barrel"` (scaffolded by `meta init`). */
17
18
  export { barrel, type BarrelOpts } from "./barrel.js";
18
19
  /** @deprecated ADR-0021 D1 — neutral artifact owned by `meta docs` (ADR-0020); not part of the recommended `meta gen` suite. */
@@ -50,6 +51,15 @@ export { buildTemplateDocData } from "./template-doc-builder.js";
50
51
  // behaviour composes its own generator instead of filing an issue here.
51
52
  export { requirementTests } from "./requirement-tests.js";
52
53
  export { requirementsFile } from "./requirements-file.js";
54
+ // The `agent` docs surface — internal engine of `meta docs --agent`, like docsFile()
55
+ // and apiDocsFile(). Not a `meta gen` config generator.
56
+ export { agentDocsFile, type AgentDocsFileOpts } from "./agent-docs-file.js";
57
+ export type {
58
+ AgentSchemaInput,
59
+ SchemaColumnLike,
60
+ SchemaTableLike,
61
+ SchemaViewLike,
62
+ } from "./agent-schema-input.js";
53
63
  export type { RequirementRow } from "./requirements-view.js";
54
64
  export type {
55
65
  RequirementTestsOpts,