@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,199 @@
1
+ // `agent/requirements.md` — the ledger, plus the index that answers the question an agent
2
+ // actually asks.
3
+ //
4
+ // The `requirements` docs surface is REQUIREMENT-KEYED: it lists entries and, under each,
5
+ // what the entry claims. That is the right shape for reading a ledger and the wrong shape
6
+ // for the question that arises while editing code — *"I am about to change this node; does
7
+ // anything claim it?"* Answering that from the requirement-keyed page means reading all of
8
+ // it, which for this repository's own ledger is 175 rows.
9
+ //
10
+ // So this page carries the same index PLUS a NODE index: every claimed node → the
11
+ // requirements claiming it, at every grain. It is an inversion of `walkRequirements`, not
12
+ // a second walk — the same resolution, read the other way, so the two indexes cannot
13
+ // disagree about what resolves.
14
+ //
15
+ // LITERAL FQNs ON EVERY LINE, deliberately and even where it is repetitive. An agent
16
+ // retrieving over a long context finds a node by matching the token it is holding; a row
17
+ // that says "the field above" is unreachable to it. This is also why the node index is
18
+ // FLAT rather than nested under its owning object.
19
+ //
20
+ // A CLAIM THAT DOES NOT RESOLVE IS OMITTED, silently, exactly as `walkRequirements` omits
21
+ // it. Resolution severity depends on `@status` and belongs to `meta verify`; a docs page
22
+ // that rendered a dangling reference as though it pointed somewhere would be asserting
23
+ // the opposite of what the gate says.
24
+ //
25
+ // THE LEDGER IS EMBEDDED BELOW THE INDEX, and that duplication is deliberate rather than
26
+ // an oversight — the same content also lives at `requirements.md` when that surface is on.
27
+ // The node index alone would omit exactly the entries the mechanism is MEASURED on: a
28
+ // `retired` capability may carry no `@implementedBy` at all (FR-039 forbids it), so it
29
+ // resolves to no node and appears in no node index. The one controlled finding behind this
30
+ // whole vocabulary is that a retired capability goes unnoticed without a ledger — 0 of 24
31
+ // against 19 of 40 — and the page an agent is told to read *before adding a capability* is
32
+ // the last place to drop it. The cost is bytes on a large estate; the alternative is a page
33
+ // that silently loses the only part with evidence behind it.
34
+
35
+ import { REQUIREMENT_ATTR_STATEMENT } from "@metaobjectsdev/metadata";
36
+ import type { MetaData } from "@metaobjectsdev/metadata";
37
+ import { GENERATED_HEADER } from "../constants.js";
38
+ import { concernOf, walkRequirements } from "../requirement-walk.js";
39
+ import { requirementRows } from "./requirements-view.js";
40
+ import { renderRequirementsMarkdown } from "./requirements-markdown.js";
41
+
42
+ const GENERATED_MARKER = `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`;
43
+
44
+ /**
45
+ * The address of any claimed node: the ROOT node's `resolutionKey()` (which carries the
46
+ * effective package), then the child-name path down to it.
47
+ *
48
+ * Built by walking parents rather than read off the node, because only a root-level node
49
+ * has a resolution key — a claimed FIELD has a bare name, and `Subscriber.status` and
50
+ * `Order.status` would collide in this index if the owner were dropped.
51
+ */
52
+ function nodeAddress(node: MetaData): string {
53
+ const segments: string[] = [];
54
+ let cur: MetaData | undefined = node;
55
+ while (cur !== undefined) {
56
+ const parent: MetaData | undefined = cur.parent;
57
+ // The last node with a parent is the root-level one; the metadata ROOT itself has no
58
+ // name worth printing, so the walk stops when the next step would leave the model.
59
+ if (parent === undefined || parent.parent === undefined) {
60
+ segments.push(cur.resolutionKey());
61
+ break;
62
+ }
63
+ segments.push(cur.name);
64
+ cur = parent;
65
+ }
66
+ return segments.reverse().join(".");
67
+ }
68
+
69
+ interface NodeIndexEntry {
70
+ readonly address: string;
71
+ readonly concern: string;
72
+ readonly claims: { path: string; subType: string; level: number | undefined; status: string | undefined; statement: string | undefined }[];
73
+ }
74
+
75
+ /** Invert the walk: claimed node → the requirements that claim it. */
76
+ export function buildNodeIndex(root: MetaData): NodeIndexEntry[] {
77
+ const byAddress = new Map<string, NodeIndexEntry>();
78
+ for (const walked of walkRequirements(root)) {
79
+ const statement = walked.node.attr(REQUIREMENT_ATTR_STATEMENT);
80
+ for (const target of walked.targets) {
81
+ const address = nodeAddress(target.node);
82
+ let entry = byAddress.get(address);
83
+ if (entry === undefined) {
84
+ entry = { address, concern: concernOf(target.node), claims: [] };
85
+ byAddress.set(address, entry);
86
+ }
87
+ entry.claims.push({
88
+ path: walked.view.path,
89
+ subType: walked.view.subType,
90
+ level: walked.view.level,
91
+ status: walked.view.status,
92
+ statement: typeof statement === "string" && statement !== "" ? statement : undefined,
93
+ });
94
+ }
95
+ }
96
+ return [...byAddress.values()].sort((a, b) => a.address.localeCompare(b.address));
97
+ }
98
+
99
+ function mdCell(text: string): string {
100
+ return text.replace(/\|/g, "\\|");
101
+ }
102
+
103
+ /** Collapse to one line — a statement with a newline in it would end the table row. */
104
+ function oneLine(text: string): string {
105
+ return text.replace(/\s+/g, " ").trim();
106
+ }
107
+
108
+ /**
109
+ * Render the page. Returns "" for a model declaring no requirements — the same contract
110
+ * `requirementsFile()` keeps, and the only reason a surface that is on by default is a
111
+ * no-op for a project without a ledger.
112
+ */
113
+ export function renderAgentRequirementsPage(root: MetaData): string {
114
+ const rows = requirementRows(root);
115
+ if (rows.length === 0) return "";
116
+
117
+ const index = buildNodeIndex(root);
118
+ const out: string[] = [];
119
+ out.push(GENERATED_MARKER);
120
+ out.push("");
121
+ out.push("# Requirements");
122
+ out.push("");
123
+ out.push(
124
+ "Read this before adding a capability, and before extending anything that looks like " +
125
+ "it already does what you were asked for.",
126
+ );
127
+ out.push("");
128
+ out.push(
129
+ "- A requirement is **prescriptive**: it states what should be true, never what " +
130
+ "happened. `status: retired` means the capability was BUILT and deliberately " +
131
+ "REMOVED — that entry is a prohibition, not history, and the right response to it " +
132
+ "is to stop, not to rebuild.",
133
+ );
134
+ out.push(
135
+ "- The **node index** below answers *\"does anything claim the thing I am about to " +
136
+ "change?\"*. Match the literal FQN.",
137
+ );
138
+ out.push(
139
+ "- **The ledger under it is not the same list.** A retired capability carries no " +
140
+ "`@implementedBy` — there is nothing left to point at — so it appears there and " +
141
+ "in no index. Read it before you conclude a capability does not exist yet.",
142
+ );
143
+ out.push("");
144
+
145
+ if (index.length > 0) {
146
+ out.push("## Node index");
147
+ out.push("");
148
+ out.push("| Node | Kind | Claimed by |");
149
+ out.push("|---|---|---|");
150
+ for (const entry of index) {
151
+ const claims = entry.claims
152
+ .map((c) => {
153
+ const level = c.level === undefined ? "" : ` L${c.level}`;
154
+ const status = c.status === undefined ? "" : ` (${c.status})`;
155
+ return `\`${c.path}\`${level}${status}`;
156
+ })
157
+ .join("<br>");
158
+ out.push(`| \`${entry.address}\` | \`${entry.concern}\` | ${claims} |`);
159
+ }
160
+ out.push("");
161
+ // The statements ride below the table: they are prose, they repeat across nodes a
162
+ // shared architectural entry claims, and putting one in a cell forces the table wide.
163
+ const stated = new Map<string, { subType: string; statement: string }>();
164
+ for (const entry of index) {
165
+ for (const c of entry.claims) {
166
+ if (c.statement !== undefined) stated.set(c.path, { subType: c.subType, statement: c.statement });
167
+ }
168
+ }
169
+ if (stated.size > 0) {
170
+ out.push("### What those requirements say");
171
+ out.push("");
172
+ for (const [path, { subType, statement }] of [...stated.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
173
+ out.push(`- \`${path}\` (\`${subType}\`) — ${mdCell(oneLine(statement))}`);
174
+ }
175
+ out.push("");
176
+ }
177
+ } else {
178
+ out.push("## Node index");
179
+ out.push("");
180
+ out.push(
181
+ "No requirement resolves to a model node. The common reason is the ordinary one: an " +
182
+ "entry at L4 or L5 that simply declares no `@implementedBy` yet, which is legal " +
183
+ "and is what a `planned` entry looks like. The others are an entry sitting ABOVE " +
184
+ "the L4 link floor (where a claim is not permitted), a `retired` entry (where it " +
185
+ "is forbidden outright), and a claim that names a node nothing resolves — " +
186
+ "`meta verify` is what says which.",
187
+ );
188
+ out.push("");
189
+ }
190
+
191
+ out.push("## The ledger");
192
+ out.push("");
193
+ // The SAME renderer the `requirements` surface uses. A second rendering of the ledger
194
+ // would be a second thing to keep true. `embedded` drops its own H1 and demotes every
195
+ // heading one level, so the entries nest under this section instead of escaping it.
196
+ out.push(renderRequirementsMarkdown(rows, { embedded: true }).replace(/\n+$/, ""));
197
+
198
+ return `${out.join("\n").replace(/\n+$/, "")}\n`;
199
+ }
@@ -0,0 +1,88 @@
1
+ // The `agent/schema.md` surface's INPUT contract — the physical schema, supplied by
2
+ // whoever owns it.
3
+ //
4
+ // codegen-ts does not compute any of this and must not. The expected schema, the
5
+ // dialect SQL type of a column, and the qualified name a table is keyed by are all
6
+ // `@metaobjectsdev/migrate-ts`'s answers: it builds the snapshot the diff compares, it
7
+ // renders the DDL, and its `qualifiedDbName` is the one key three separate suppression
8
+ // sets already agree on. Re-deriving any of them here would give an adopter a page
9
+ // describing a schema the tool does not produce — and the disagreement would be
10
+ // invisible, because a documentation page looks authoritative and nothing compares it
11
+ // to the migration.
12
+ //
13
+ // So the surface takes the schema as an ARGUMENT, with the resolvers injected. The
14
+ // types below are structural on purpose: `migrate-ts`'s own `SchemaSnapshot`,
15
+ // `TableDescriptor` and `ColumnDescriptor` satisfy them without codegen-ts taking a
16
+ // dependency on that package (it has none today, and the docs surface is not a reason
17
+ // to add one — `meta docs` in the CLI depends on both and is where they meet).
18
+ //
19
+ // ABSENT INPUT IS A SUPPORTED STATE, not a failure. `meta docs` runs without a gen
20
+ // config, and a project with no dialect has no physical schema to describe; the surface
21
+ // then emits nothing rather than a page full of unknowns.
22
+
23
+ /** A canonical SQL type as `migrate-ts` models it. Opaque here — only `columnType` reads it. */
24
+ export interface SchemaColumnLike {
25
+ readonly name: string;
26
+ readonly nullable: boolean;
27
+ readonly default?: { readonly kind: "literal" | "expr"; readonly value: string } | undefined;
28
+ readonly identity?: "increment" | "uuid" | undefined;
29
+ /** Threaded from the field's `@description` — the business semantics beside the column. */
30
+ readonly description?: string | undefined;
31
+ }
32
+
33
+ export interface SchemaIndexLike {
34
+ readonly name: string;
35
+ readonly columns: readonly string[];
36
+ readonly unique: boolean;
37
+ readonly where?: string | undefined;
38
+ readonly expr?: string | undefined;
39
+ readonly using?: string | undefined;
40
+ }
41
+
42
+ export interface SchemaFkLike {
43
+ readonly name: string;
44
+ readonly columns: readonly string[];
45
+ readonly refTable: string;
46
+ readonly refColumns: readonly string[];
47
+ readonly onDelete?: string | undefined;
48
+ readonly onUpdate?: string | undefined;
49
+ }
50
+
51
+ export interface SchemaCheckLike {
52
+ readonly name: string;
53
+ readonly expression: string;
54
+ }
55
+
56
+ export interface SchemaTableLike {
57
+ readonly name: string;
58
+ readonly schema?: string | undefined;
59
+ readonly columns: readonly SchemaColumnLike[];
60
+ readonly indexes: readonly SchemaIndexLike[];
61
+ readonly foreignKeys: readonly SchemaFkLike[];
62
+ readonly checks: readonly SchemaCheckLike[];
63
+ readonly primaryKey: readonly string[];
64
+ /** Threaded from the entity's `@description`. */
65
+ readonly description?: string | undefined;
66
+ }
67
+
68
+ export interface SchemaViewLike {
69
+ readonly name: string;
70
+ readonly schema?: string | undefined;
71
+ readonly columns?: readonly { readonly name: string }[] | undefined;
72
+ }
73
+
74
+ /**
75
+ * Everything `agent/schema.md` renders from, plus the two resolvers it refuses to own.
76
+ */
77
+ export interface AgentSchemaInput {
78
+ /** `postgres` | `sqlite` | `d1` — named on the page so a reader knows which SQL they are reading. */
79
+ readonly dialect: string;
80
+ readonly tables: readonly SchemaTableLike[];
81
+ readonly views: readonly SchemaViewLike[];
82
+ /** Qualified physical name → the declaring object's `resolutionKey()`. */
83
+ readonly provenance: ReadonlyMap<string, string>;
84
+ /** `migrate-ts`'s `columnTypeSql`, bound to `dialect`. */
85
+ readonly columnType: (column: SchemaColumnLike) => string;
86
+ /** `migrate-ts`'s `qualifiedDbName` — the ONE key the provenance map is built with. */
87
+ readonly qualify: (obj: { name: string; schema?: string | undefined }) => string;
88
+ }
@@ -0,0 +1,297 @@
1
+ // `agent/schema.md` — the physical schema, for an agent about to touch persistence.
2
+ //
3
+ // WHY THIS PAGE EXISTS AND WHY IT LOOKS LIKE THIS
4
+ //
5
+ // The strongest measured finding behind this surface is not about format: roughly 2K
6
+ // tokens of BUSINESS SEMANTICS beside the schema is worth an order of magnitude more
7
+ // than any table-versus-YAML-versus-DDL choice. So every table and column carries its
8
+ // `@description`, and the descriptions come from the snapshot itself — `migrate-ts`
9
+ // already threads them there to emit `COMMENT ON`, which means the page and the database
10
+ // comment cannot disagree.
11
+ //
12
+ // IT DOES NOT RESTATE THE DDL. The migration files ARE the DDL, they are generated, and
13
+ // they are what actually runs. A page that reproduces `CREATE TABLE` is a second spelling
14
+ // of the same fact that goes stale the first time someone regenerates without it — so
15
+ // this page describes the schema and CITES the migrations for the statements.
16
+ //
17
+ // It is TABLE-DRIVEN, not entity-driven, and that is deliberate: a TPH hierarchy folds
18
+ // several entities into one table, an abstract base has none, and an `@unmanaged` object
19
+ // is excluded — all rules `buildExpectedSchema`'s Pass 1 already owns. Walking entities
20
+ // here would mean re-implementing those skip rules and drifting from them. The declaring
21
+ // object is named per table from the provenance map instead.
22
+
23
+ import { GENERATION_INCREMENT, GENERATION_UUID } from "@metaobjectsdev/metadata";
24
+ import { GENERATED_HEADER } from "../constants.js";
25
+ import type {
26
+ AgentSchemaInput,
27
+ SchemaColumnLike,
28
+ SchemaFkLike,
29
+ SchemaIndexLike,
30
+ SchemaTableLike,
31
+ } from "./agent-schema-input.js";
32
+
33
+ const GENERATED_MARKER = `<!-- ${GENERATED_HEADER} — DO NOT EDIT. -->`;
34
+
35
+ /** Markdown-escape a cell whose text may contain a `|` (a CHECK expression can). */
36
+ function mdCell(text: string): string {
37
+ return text.replace(/\|/g, "\\|");
38
+ }
39
+
40
+ /** Collapse a description to one line — a newline inside a table cell ends the row. */
41
+ function oneLine(text: string): string {
42
+ return text.replace(/\s+/g, " ").trim();
43
+ }
44
+
45
+ /** `` `a`, `b` `` — a column list, or an em-dash when empty. */
46
+ function cols(list: readonly string[]): string {
47
+ return list.length === 0 ? "—" : list.map((c) => `\`${c}\``).join(", ");
48
+ }
49
+
50
+ /**
51
+ * The KEY cell: primary / foreign / unique / indexed, most-specific first.
52
+ *
53
+ * A COMPOSITE index is labelled as one. A bare `unique` on a column that is merely one
54
+ * member of a two-column unique index asserts that the column alone is unique — which is
55
+ * false, and false in the direction that makes a reader write a lookup that returns more
56
+ * than one row. The Indexes section below carries the full key; this cell only has to stop
57
+ * being wrong about it.
58
+ */
59
+ function keyCell(table: SchemaTableLike, column: SchemaColumnLike): string {
60
+ const roles: string[] = [];
61
+ if (table.primaryKey.includes(column.name)) {
62
+ roles.push(table.primaryKey.length > 1 ? "PK (composite)" : "PK");
63
+ }
64
+ const fk = table.foreignKeys.find((f) => f.columns.includes(column.name));
65
+ if (fk !== undefined) roles.push(`FK → \`${fk.refTable}\``);
66
+ const covering = table.indexes.filter((i) => i.columns.includes(column.name));
67
+ const unique = covering.find((i) => i.unique);
68
+ const index = unique ?? covering[0];
69
+ if (index !== undefined) {
70
+ const label = unique !== undefined ? "unique" : "indexed";
71
+ roles.push(index.columns.length > 1 ? `${label} (composite)` : label);
72
+ }
73
+ return roles.length === 0 ? "" : roles.join(" · ");
74
+ }
75
+
76
+ /** The DEFAULT cell. An `expr` default is shown as SQL; a literal as a literal. */
77
+ function defaultCell(column: SchemaColumnLike): string {
78
+ if (column.identity === GENERATION_INCREMENT) return "auto-increment";
79
+ if (column.identity === GENERATION_UUID) return "generated uuid";
80
+ if (column.default === undefined) return "";
81
+ return `\`${mdCell(column.default.value)}\``;
82
+ }
83
+
84
+ function indexLine(ix: SchemaIndexLike): string {
85
+ const key = ix.expr !== undefined ? `\`${mdCell(ix.expr)}\`` : cols(ix.columns);
86
+ const parts = [`\`${ix.name}\``, ix.unique ? "unique" : "index", `on ${key}`];
87
+ if (ix.using !== undefined && ix.using !== "" && ix.using !== "btree") parts.push(`using \`${ix.using}\``);
88
+ if (ix.where !== undefined && ix.where !== "") parts.push(`where \`${mdCell(ix.where)}\``);
89
+ return `- ${parts.join(" · ")}`;
90
+ }
91
+
92
+ function fkLine(fk: SchemaFkLike): string {
93
+ const parts = [
94
+ `\`${fk.name}\``,
95
+ `${cols(fk.columns)} → \`${fk.refTable}\`(${cols(fk.refColumns)})`,
96
+ ];
97
+ if (fk.onDelete !== undefined && fk.onDelete !== "") parts.push(`on delete \`${fk.onDelete}\``);
98
+ if (fk.onUpdate !== undefined && fk.onUpdate !== "") parts.push(`on update \`${fk.onUpdate}\``);
99
+ return `- ${parts.join(" · ")}`;
100
+ }
101
+
102
+ /**
103
+ * One table's section: the declaring object, its description, the column table, then
104
+ * whichever of indexes / foreign keys / checks the table actually has.
105
+ *
106
+ * `declaredBy` maps a physical column name back to the FIELD that declared it — the
107
+ * mapping an agent needs to go from a query it is reading to the metadata it must edit,
108
+ * and the one thing the snapshot alone cannot supply.
109
+ */
110
+ function tableSection(
111
+ table: SchemaTableLike,
112
+ input: AgentSchemaInput,
113
+ fqn: string | undefined,
114
+ declaredBy: ReadonlyMap<string, { field: string; type: string }>,
115
+ ): string[] {
116
+ const out: string[] = [];
117
+ const qualified = table.schema === undefined ? `\`${table.name}\`` : `\`${table.schema}.${table.name}\``;
118
+ out.push(`### ${qualified}`);
119
+ out.push("");
120
+ if (fqn !== undefined) out.push(`Declared by \`${fqn}\`.`);
121
+ if (table.description !== undefined && table.description !== "") {
122
+ out.push("");
123
+ out.push(`> ${oneLine(table.description)}`);
124
+ }
125
+ out.push("");
126
+ out.push("| Column | Field | Declared | SQL type | Null | Default | Key |");
127
+ out.push("|---|---|---|---|---|---|---|");
128
+ for (const c of table.columns) {
129
+ const declared = declaredBy.get(c.name);
130
+ out.push(
131
+ `| \`${c.name}\` | ${declared === undefined ? "" : `\`${declared.field}\``} | ` +
132
+ `${declared === undefined ? "" : `\`${declared.type}\``} | \`${mdCell(input.columnType(c))}\` | ` +
133
+ `${c.nullable ? "yes" : "no"} | ${defaultCell(c)} | ${keyCell(table, c)} |`,
134
+ );
135
+ }
136
+ // Column descriptions ride BELOW the table rather than as an eighth cell: a sentence
137
+ // in a cell forces the whole table wide, and most columns have none.
138
+ const described = table.columns.filter((c) => c.description !== undefined && c.description !== "");
139
+ if (described.length > 0) {
140
+ out.push("");
141
+ for (const c of described) out.push(`- \`${c.name}\` — ${oneLine(c.description ?? "")}`);
142
+ }
143
+ if (table.indexes.length > 0) {
144
+ out.push("");
145
+ out.push("**Indexes**");
146
+ out.push("");
147
+ for (const ix of table.indexes) out.push(indexLine(ix));
148
+ }
149
+ if (table.foreignKeys.length > 0) {
150
+ out.push("");
151
+ out.push("**Foreign keys**");
152
+ out.push("");
153
+ for (const fk of table.foreignKeys) out.push(fkLine(fk));
154
+ }
155
+ if (table.checks.length > 0) {
156
+ out.push("");
157
+ out.push("**Checks**");
158
+ out.push("");
159
+ for (const ck of table.checks) out.push(`- \`${ck.name}\` — \`${mdCell(ck.expression)}\``);
160
+ }
161
+ out.push("");
162
+ return out;
163
+ }
164
+
165
+ export interface AgentSchemaPageOptions {
166
+ /** Column → declaring field, per QUALIFIED table name. Supplied by the generator, which
167
+ * holds the loaded model; see `agentDocsFile`. */
168
+ readonly declaredBy: ReadonlyMap<string, ReadonlyMap<string, { field: string; type: string }>>;
169
+ /** Per-projection lineage lines, keyed by QUALIFIED view name. */
170
+ readonly viewLineage: ReadonlyMap<string, readonly string[]>;
171
+ /** Relationship lines, already rendered from the model. */
172
+ readonly relationships: readonly string[];
173
+ /** Enum lines, already rendered from the model. */
174
+ readonly enums: readonly string[];
175
+ }
176
+
177
+ /**
178
+ * Render the whole page. Returns "" when there is no physical schema at all, which is
179
+ * what lets the surface emit no FILE rather than a page describing nothing.
180
+ */
181
+ export function renderAgentSchemaPage(
182
+ input: AgentSchemaInput,
183
+ opts: AgentSchemaPageOptions,
184
+ ): string {
185
+ if (input.tables.length === 0 && input.views.length === 0) return "";
186
+
187
+ const out: string[] = [];
188
+ out.push(GENERATED_MARKER);
189
+ out.push("");
190
+ out.push("# Schema");
191
+ out.push("");
192
+ out.push(
193
+ `The physical shape of the \`${input.dialect}\` database this model generates. ` +
194
+ "Read it before writing a query, a migration, or anything that names a table or a column.",
195
+ );
196
+ out.push("");
197
+ out.push(
198
+ "- The **DDL is not repeated here.** The migration files are the DDL, they are " +
199
+ "generated, and they are what runs — this page describes the schema they produce.",
200
+ );
201
+ out.push(
202
+ "- Change the schema by changing the **metadata** and running `meta migrate`. Never " +
203
+ "hand-apply SQL to a live database: it drifts from the migration history and " +
204
+ "collides at the next migrate.",
205
+ );
206
+ out.push(
207
+ "- `Field` and `Declared` are the METADATA names. Edit those; the column name follows.",
208
+ );
209
+ out.push("");
210
+
211
+ // The semantics nudge. A schema page carrying business meaning beside the columns is
212
+ // worth more to a reader working from it than any format choice this page could make —
213
+ // and those sentences come from `@description` on the entity and the field, which the
214
+ // snapshot already threads here to emit `COMMENT ON`. A model declaring none renders a
215
+ // page of names with nothing to disambiguate them, and says so, because silently
216
+ // omitting the highest-value content teaches an adopter that it does not exist.
217
+ const anyDescription =
218
+ input.tables.some(
219
+ (t) =>
220
+ (t.description !== undefined && t.description !== "") ||
221
+ t.columns.some((c) => c.description !== undefined && c.description !== ""),
222
+ );
223
+ if (!anyDescription && input.tables.length > 0) {
224
+ out.push(
225
+ "> **Nothing in this model declares a `description`.** The tables and columns below " +
226
+ "are named but not explained. Adding `description` to an entity or a field is the " +
227
+ "single highest-value change to this page: it is what tells a reader *which* of " +
228
+ "two plausible columns to use, and it is the same text `meta migrate` emits as a " +
229
+ "`COMMENT ON`, so it lands in the database too.",
230
+ );
231
+ out.push("");
232
+ }
233
+
234
+ if (input.tables.length > 0) {
235
+ out.push("## Tables");
236
+ out.push("");
237
+ for (const table of [...input.tables].sort((a, b) => a.name.localeCompare(b.name))) {
238
+ const key = input.qualify(table);
239
+ out.push(
240
+ ...tableSection(
241
+ table,
242
+ input,
243
+ input.provenance.get(key),
244
+ opts.declaredBy.get(key) ?? new Map(),
245
+ ),
246
+ );
247
+ }
248
+ }
249
+
250
+ if (input.views.length > 0) {
251
+ out.push("## Views");
252
+ out.push("");
253
+ out.push(
254
+ "A view is generated from its projection's `origin.*` children — it is derived, " +
255
+ "never hand-written. Editing the view SQL directly is drift the tool cannot see.",
256
+ );
257
+ out.push("");
258
+ for (const view of [...input.views].sort((a, b) => a.name.localeCompare(b.name))) {
259
+ const key = input.qualify(view);
260
+ const qualified = view.schema === undefined ? `\`${view.name}\`` : `\`${view.schema}.${view.name}\``;
261
+ out.push(`### ${qualified}`);
262
+ out.push("");
263
+ const fqn = input.provenance.get(key);
264
+ if (fqn !== undefined) out.push(`Declared by \`${fqn}\`.`);
265
+ const lineage = opts.viewLineage.get(key) ?? [];
266
+ if (lineage.length > 0) {
267
+ out.push("");
268
+ out.push("| Column | Lineage |");
269
+ out.push("|---|---|");
270
+ out.push(...lineage);
271
+ }
272
+ out.push("");
273
+ }
274
+ }
275
+
276
+ if (opts.relationships.length > 0) {
277
+ out.push("## Relationships");
278
+ out.push("");
279
+ out.push(...opts.relationships);
280
+ out.push("");
281
+ }
282
+
283
+ if (opts.enums.length > 0) {
284
+ out.push("## Enums");
285
+ out.push("");
286
+ out.push(
287
+ "Members are the values the wire and the generated types use. Whether the DATABASE " +
288
+ "also refuses a value outside the set is that table's **Checks** above — an " +
289
+ "`@isArray` enum carries none, and neither does a view column.",
290
+ );
291
+ out.push("");
292
+ out.push(...opts.enums);
293
+ out.push("");
294
+ }
295
+
296
+ return `${out.join("\n").replace(/\n+$/, "")}\n`;
297
+ }