@metaobjectsdev/cli 1.0.13 → 1.1.1-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 (186) hide show
  1. package/README.md +32 -22
  2. package/dist/bin/meta.js +14 -2
  3. package/dist/bin/meta.js.map +1 -1
  4. package/dist/src/commands/docs.d.ts +1 -14
  5. package/dist/src/commands/docs.d.ts.map +1 -1
  6. package/dist/src/commands/docs.js +11 -835
  7. package/dist/src/commands/docs.js.map +1 -1
  8. package/dist/src/commands/eject.d.ts +24 -1
  9. package/dist/src/commands/eject.d.ts.map +1 -1
  10. package/dist/src/commands/eject.js +240 -69
  11. package/dist/src/commands/eject.js.map +1 -1
  12. package/dist/src/commands/gen.d.ts +7 -0
  13. package/dist/src/commands/gen.d.ts.map +1 -1
  14. package/dist/src/commands/gen.js +91 -21
  15. package/dist/src/commands/gen.js.map +1 -1
  16. package/dist/src/commands/generator.js +2 -2
  17. package/dist/src/commands/generator.js.map +1 -1
  18. package/dist/src/commands/init.d.ts +7 -0
  19. package/dist/src/commands/init.d.ts.map +1 -1
  20. package/dist/src/commands/init.js +71 -36
  21. package/dist/src/commands/init.js.map +1 -1
  22. package/dist/src/commands/migrate.d.ts +1 -1
  23. package/dist/src/commands/migrate.d.ts.map +1 -1
  24. package/dist/src/commands/migrate.js +224 -125
  25. package/dist/src/commands/migrate.js.map +1 -1
  26. package/dist/src/commands/types.d.ts.map +1 -1
  27. package/dist/src/commands/types.js +16 -2
  28. package/dist/src/commands/types.js.map +1 -1
  29. package/dist/src/commands/upgrade.d.ts.map +1 -1
  30. package/dist/src/commands/upgrade.js +4 -1
  31. package/dist/src/commands/upgrade.js.map +1 -1
  32. package/dist/src/commands/verify.d.ts.map +1 -1
  33. package/dist/src/commands/verify.js +189 -125
  34. package/dist/src/commands/verify.js.map +1 -1
  35. package/dist/src/index.d.ts.map +1 -1
  36. package/dist/src/index.js +59 -72
  37. package/dist/src/index.js.map +1 -1
  38. package/dist/src/lib/agent-context-staleness.d.ts +8 -1
  39. package/dist/src/lib/agent-context-staleness.d.ts.map +1 -1
  40. package/dist/src/lib/agent-context-staleness.js +20 -5
  41. package/dist/src/lib/agent-context-staleness.js.map +1 -1
  42. package/dist/src/lib/agent-schema.d.ts +9 -0
  43. package/dist/src/lib/agent-schema.d.ts.map +1 -0
  44. package/dist/src/lib/agent-schema.js +61 -0
  45. package/dist/src/lib/agent-schema.js.map +1 -0
  46. package/dist/src/lib/allow.d.ts +6 -0
  47. package/dist/src/lib/allow.d.ts.map +1 -1
  48. package/dist/src/lib/allow.js +49 -5
  49. package/dist/src/lib/allow.js.map +1 -1
  50. package/dist/src/lib/anti-patterns.d.ts.map +1 -1
  51. package/dist/src/lib/anti-patterns.js +4 -2
  52. package/dist/src/lib/anti-patterns.js.map +1 -1
  53. package/dist/src/lib/args.d.ts +51 -10
  54. package/dist/src/lib/args.d.ts.map +1 -1
  55. package/dist/src/lib/args.js +44 -18
  56. package/dist/src/lib/args.js.map +1 -1
  57. package/dist/src/lib/catalog-listing.d.ts +21 -1
  58. package/dist/src/lib/catalog-listing.d.ts.map +1 -1
  59. package/dist/src/lib/catalog-listing.js +85 -16
  60. package/dist/src/lib/catalog-listing.js.map +1 -1
  61. package/dist/src/lib/catalog.d.ts +30 -0
  62. package/dist/src/lib/catalog.d.ts.map +1 -1
  63. package/dist/src/lib/catalog.js +40 -5
  64. package/dist/src/lib/catalog.js.map +1 -1
  65. package/dist/src/lib/codegen-drift.d.ts +31 -2
  66. package/dist/src/lib/codegen-drift.d.ts.map +1 -1
  67. package/dist/src/lib/codegen-drift.js +109 -4
  68. package/dist/src/lib/codegen-drift.js.map +1 -1
  69. package/dist/src/lib/collection-load-options.d.ts +1 -1
  70. package/dist/src/lib/collection-load-options.d.ts.map +1 -1
  71. package/dist/src/lib/collection-load-options.js +1 -0
  72. package/dist/src/lib/collection-load-options.js.map +1 -1
  73. package/dist/src/lib/config.d.ts +4 -2
  74. package/dist/src/lib/config.d.ts.map +1 -1
  75. package/dist/src/lib/config.js +4 -2
  76. package/dist/src/lib/config.js.map +1 -1
  77. package/dist/src/lib/decimal-precision-advisory.d.ts +10 -0
  78. package/dist/src/lib/decimal-precision-advisory.d.ts.map +1 -0
  79. package/dist/src/lib/decimal-precision-advisory.js +34 -0
  80. package/dist/src/lib/decimal-precision-advisory.js.map +1 -0
  81. package/dist/src/lib/deprecation-lint.d.ts.map +1 -1
  82. package/dist/src/lib/deprecation-lint.js +17 -26
  83. package/dist/src/lib/deprecation-lint.js.map +1 -1
  84. package/dist/src/lib/docs-removed.d.ts +13 -0
  85. package/dist/src/lib/docs-removed.d.ts.map +1 -0
  86. package/dist/src/lib/docs-removed.js +31 -0
  87. package/dist/src/lib/docs-removed.js.map +1 -0
  88. package/dist/src/lib/docs-site-eject.d.ts +13 -0
  89. package/dist/src/lib/docs-site-eject.d.ts.map +1 -0
  90. package/dist/src/lib/docs-site-eject.js +40 -0
  91. package/dist/src/lib/docs-site-eject.js.map +1 -0
  92. package/dist/src/lib/field-lint.d.ts +25 -0
  93. package/dist/src/lib/field-lint.d.ts.map +1 -0
  94. package/dist/src/lib/field-lint.js +102 -0
  95. package/dist/src/lib/field-lint.js.map +1 -0
  96. package/dist/src/lib/fk-index-advisory.d.ts +1 -2
  97. package/dist/src/lib/fk-index-advisory.d.ts.map +1 -1
  98. package/dist/src/lib/fk-index-advisory.js +4 -3
  99. package/dist/src/lib/fk-index-advisory.js.map +1 -1
  100. package/dist/src/lib/format.d.ts +7 -0
  101. package/dist/src/lib/format.d.ts.map +1 -1
  102. package/dist/src/lib/format.js +13 -0
  103. package/dist/src/lib/format.js.map +1 -1
  104. package/dist/src/lib/gen-migrate-advice.d.ts +3 -1
  105. package/dist/src/lib/gen-migrate-advice.d.ts.map +1 -1
  106. package/dist/src/lib/gen-migrate-advice.js +3 -4
  107. package/dist/src/lib/gen-migrate-advice.js.map +1 -1
  108. package/dist/src/lib/install-set.d.ts +10 -1
  109. package/dist/src/lib/install-set.d.ts.map +1 -1
  110. package/dist/src/lib/install-set.js +20 -2
  111. package/dist/src/lib/install-set.js.map +1 -1
  112. package/dist/src/lib/load-metaobjects-config.d.ts +13 -11
  113. package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
  114. package/dist/src/lib/load-metaobjects-config.js +53 -75
  115. package/dist/src/lib/load-metaobjects-config.js.map +1 -1
  116. package/dist/src/lib/manifest-ignored-check.d.ts +4 -1
  117. package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -1
  118. package/dist/src/lib/manifest-ignored-check.js +9 -4
  119. package/dist/src/lib/manifest-ignored-check.js.map +1 -1
  120. package/dist/src/lib/output.d.ts +15 -0
  121. package/dist/src/lib/output.d.ts.map +1 -1
  122. package/dist/src/lib/output.js +9 -1
  123. package/dist/src/lib/output.js.map +1 -1
  124. package/dist/src/lib/payload-field-tree.d.ts +4 -5
  125. package/dist/src/lib/payload-field-tree.d.ts.map +1 -1
  126. package/dist/src/lib/payload-field-tree.js +16 -31
  127. package/dist/src/lib/payload-field-tree.js.map +1 -1
  128. package/dist/src/lib/requirement-check.d.ts +7 -0
  129. package/dist/src/lib/requirement-check.d.ts.map +1 -1
  130. package/dist/src/lib/requirement-check.js +14 -7
  131. package/dist/src/lib/requirement-check.js.map +1 -1
  132. package/dist/src/lib/strict-args.d.ts +28 -0
  133. package/dist/src/lib/strict-args.d.ts.map +1 -0
  134. package/dist/src/lib/strict-args.js +49 -0
  135. package/dist/src/lib/strict-args.js.map +1 -0
  136. package/dist/src/lib/template-eject.d.ts +108 -0
  137. package/dist/src/lib/template-eject.d.ts.map +1 -0
  138. package/dist/src/lib/template-eject.js +438 -0
  139. package/dist/src/lib/template-eject.js.map +1 -0
  140. package/dist/src/lib/version.d.ts +9 -0
  141. package/dist/src/lib/version.d.ts.map +1 -1
  142. package/dist/src/lib/version.js +15 -0
  143. package/dist/src/lib/version.js.map +1 -1
  144. package/package.json +10 -10
  145. package/src/commands/docs.ts +11 -952
  146. package/src/commands/eject.ts +318 -73
  147. package/src/commands/gen.ts +94 -22
  148. package/src/commands/generator.ts +2 -2
  149. package/src/commands/init.ts +80 -38
  150. package/src/commands/migrate.ts +248 -117
  151. package/src/commands/types.ts +18 -2
  152. package/src/commands/upgrade.ts +5 -1
  153. package/src/commands/verify.ts +229 -136
  154. package/src/index.ts +59 -72
  155. package/src/lib/agent-context-staleness.ts +20 -5
  156. package/src/lib/agent-schema.ts +74 -0
  157. package/src/lib/allow.ts +57 -6
  158. package/src/lib/anti-patterns.ts +4 -2
  159. package/src/lib/args.ts +83 -28
  160. package/src/lib/catalog-listing.ts +105 -25
  161. package/src/lib/catalog.ts +51 -6
  162. package/src/lib/codegen-drift.ts +138 -3
  163. package/src/lib/collection-load-options.ts +2 -1
  164. package/src/lib/config.ts +5 -3
  165. package/src/lib/decimal-precision-advisory.ts +50 -0
  166. package/src/lib/deprecation-lint.ts +16 -24
  167. package/src/lib/docs-removed.ts +41 -0
  168. package/src/lib/docs-site-eject.ts +48 -0
  169. package/src/lib/field-lint.ts +122 -0
  170. package/src/lib/fk-index-advisory.ts +4 -4
  171. package/src/lib/format.ts +12 -0
  172. package/src/lib/gen-migrate-advice.ts +6 -4
  173. package/src/lib/install-set.ts +20 -1
  174. package/src/lib/load-metaobjects-config.ts +57 -78
  175. package/src/lib/manifest-ignored-check.ts +14 -10
  176. package/src/lib/output.ts +18 -1
  177. package/src/lib/payload-field-tree.ts +18 -35
  178. package/src/lib/requirement-check.ts +21 -6
  179. package/src/lib/strict-args.ts +52 -0
  180. package/src/lib/template-eject.ts +526 -0
  181. package/src/lib/version.ts +18 -0
  182. package/dist/src/lib/docs-drift.d.ts +0 -42
  183. package/dist/src/lib/docs-drift.d.ts.map +0 -1
  184. package/dist/src/lib/docs-drift.js +0 -264
  185. package/dist/src/lib/docs-drift.js.map +0 -1
  186. package/src/lib/docs-drift.ts +0 -312
@@ -1,264 +0,0 @@
1
- // `meta verify --docs` — the docs-drift gate.
2
- //
3
- // It runs `meta docs` into a throwaway temp directory and diffs what that produces
4
- // against the committed docs tree. Same shape as `--codegen`, pointed at `docs.outDir`,
5
- // and it exists because nothing checked the docs tree at all: `--codegen` regenerates only
6
- // the config's `outDir`/`targets`, so a model could move and every committed page keep
7
- // describing the previous one indefinitely, with every gate green.
8
- //
9
- // IT CALLS THE DOCS COMMAND, NOT A REIMPLEMENTATION OF IT. The gate and the door must not
10
- // be two answers to "what are the docs" — that is the defect this whole surface is meant to
11
- // prevent, and it would be embarrassing to introduce it in the checker.
12
- //
13
- // TWO DELIBERATE DIFFERENCES FROM `--codegen`, both in the direction of not convicting the
14
- // innocent:
15
- //
16
- // 1. NO HAND-EDIT PRESERVATION, so a byte difference IS drift. Docs pages are read, never
17
- // imported: there is no three-way merge, nothing records what was written, and the
18
- // documented workflow never invites an edit inside one. `--codegen` needs
19
- // `.gen-state/.hashes.json` to tell a preserved hand edit from stale output; here
20
- // there is no such offer to honour.
21
- //
22
- // 2. IT REPORTS A FILE AS EXTRA ONLY WHERE IT OWNS THE DIRECTORY. `docs.outDir` is a
23
- // directory an adopter chooses, and it may well be one full of hand-written
24
- // documentation — this repository's own `docs/` holds a hundred such files. (The
25
- // default moved to `./docs/generated` for exactly that reason: defaulting into the
26
- // most-owned directory name in the ecosystem contradicted this paragraph.) Its
27
- // `api/` subtree is not ours either: on a multi-port project those pages are written
28
- // by the OTHER port's docs command (`mvn metaobjects:docs`, `metaobjects docs`,
29
- // `dotnet meta docs`), which this gate never runs, so a fresh run here legitimately
30
- // emits none of them. `--codegen` can convict a committed-but-not-regenerated file
31
- // because `.gen-state` proves the generator wrote it; over the docs root there is no
32
- // such manifest, and convicting anyway is precisely the jurisdiction mistake
33
- // `--codegen`'s orphan branch was corrected for in 0.24.3.
34
- //
35
- // `agent/` IS ours, and there the ownership question has an answer on disk: the Node
36
- // `meta docs` command is the only thing that writes that directory (its name is not
37
- // configurable), and every page it writes opens with the `@generated` marker. That
38
- // marker is the proof `--codegen` has to consult `.gen-state` for, so BOTH conditions
39
- // are required — under `agent/` AND carrying the marker. A hand-written note dropped
40
- // in `agent/` carries no marker and is left alone, exactly as one in the docs root is.
41
- //
42
- // It matters because the schema page is SKIPPED rather than failed when the expected
43
- // schema cannot be built or no dialect is declared — so without this, a committed
44
- // `agent/schema.md` describing the previous schema passed the gate on exactly the
45
- // change it most needs to flag.
46
- //
47
- // The residual cost is stated rather than hidden: outside `agent/`, a page for an
48
- // entity that was DELETED stays committed and this gate stays green.
49
- import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, lstatSync } from "node:fs";
50
- import { tmpdir } from "node:os";
51
- import { join, relative, resolve, isAbsolute } from "node:path";
52
- import { docsCommand } from "../commands/docs.js";
53
- import { gitIgnored } from "./git-ignore.js";
54
- import { toPosix } from "./rel-posix.js";
55
- /**
56
- * Recursively list files under `dir`, relative to it.
57
- *
58
- * `lstatSync`, NOT `statSync`, and symlinks are SKIPPED. This walks the committed docs
59
- * tree as well as the fresh one now, and a real repository's `docs/` may hold a symlink to
60
- * a build output that is absent on CI — `statSync` follows it and throws `ENOENT`, which
61
- * the caller reports as "regeneration failed", blaming the fresh run for a dangling link
62
- * in the committed tree. A directory symlink would also let the walk recurse without
63
- * bound. Nothing MetaObjects writes is a symlink, so skipping them cannot hide a page of
64
- * ours; a symlinked page is somebody else's file, which this gate does not judge anyway.
65
- */
66
- function listFiles(dir) {
67
- if (!existsSync(dir))
68
- return [];
69
- const out = [];
70
- const walk = (d) => {
71
- for (const entry of readdirSync(d)) {
72
- const full = join(d, entry);
73
- const st = lstatSync(full);
74
- if (st.isSymbolicLink())
75
- continue;
76
- if (st.isDirectory())
77
- walk(full);
78
- else
79
- out.push(relative(dir, full));
80
- }
81
- };
82
- walk(dir);
83
- return out;
84
- }
85
- /**
86
- * Docs-root-relative prefixes the Node `meta docs` command owns outright — the only
87
- * places a committed page that a fresh run does not emit is drift rather than somebody
88
- * else's file. `agent` is fixed (it is not a configurable subdirectory), which is what
89
- * makes the claim checkable; `api` deliberately is NOT here, because its subdirectory IS
90
- * configurable and on a multi-port project another port's docs command writes it.
91
- */
92
- const OWNED_PREFIXES = ["agent/"];
93
- /** The marker every generated docs page opens with — the on-disk ownership proof. */
94
- const GENERATED_MARKER = "@generated";
95
- /**
96
- * True when this committed file is one WE wrote: under a directory this command owns, and
97
- * carrying the generated marker in its opening lines. Both halves are load-bearing — the
98
- * prefix keeps another port's `api/` pages out of jurisdiction, the marker keeps a
99
- * hand-written note inside `agent/` out of it.
100
- */
101
- function isOurs(docsDir, rel) {
102
- const normalized = toPosix(rel);
103
- if (!OWNED_PREFIXES.some((p) => normalized.startsWith(p)))
104
- return false;
105
- try {
106
- return readFileSync(join(docsDir, rel), "utf8")
107
- .split("\n", 3)
108
- .some((line) => line.includes(GENERATED_MARKER));
109
- }
110
- catch {
111
- // Unreadable is not proof of ownership, and this gate never convicts without it.
112
- return false;
113
- }
114
- }
115
- /**
116
- * Regenerate the docs into a temp tree and diff it against the committed one.
117
- *
118
- * A non-zero exit from the docs command is returned as `error` rather than as drift: a run
119
- * that could not produce the pages has not shown that the committed ones are wrong, and
120
- * reporting it as drift would send a reader to edit files that are probably fine.
121
- */
122
- export async function computeDocsDrift(args) {
123
- const root = isAbsolute(args.projectRoot) ? args.projectRoot : resolve(args.projectRoot);
124
- const docsDir = isAbsolute(args.docsDir) ? args.docsDir : resolve(root, args.docsDir);
125
- const tempRoot = mkdtempSync(join(tmpdir(), "meta-verify-docs-"));
126
- try {
127
- // NO `<project-root>` POSITIONAL, deliberately. Passing one is not the same command:
128
- // an explicit path PINS the source set to that directory's own sources (#327), while a
129
- // bare run discovers the project by walking up. The committed pages were produced by
130
- // whatever the user actually ran, and a bare run is what `meta docs` means by default —
131
- // so the gate reproduces that and takes its project root from `cwd`, the way the docs
132
- // command itself does. A gate that resolved a different source set would report pages
133
- // as drifted that a plain regeneration would reproduce exactly.
134
- //
135
- // `--out` IS passed, because the whole point is to write somewhere else; everything
136
- // else — layout, surfaces, api surfaces, base URL — still resolves from the project's
137
- // own config.
138
- const exit = await docsCommand(["--out", tempRoot], args.cwd, { silent: true });
139
- if (exit !== 0) {
140
- return {
141
- clean: false,
142
- driftedFiles: [],
143
- lines: [],
144
- checked: 0,
145
- ignored: 0,
146
- error: `verify --docs: 'meta docs' exited ${exit}, so the committed pages could not be ` +
147
- `compared against a fresh run. Fix that first — the error is above.`,
148
- };
149
- }
150
- const fresh = listFiles(tempRoot).sort();
151
- if (fresh.length === 0) {
152
- return {
153
- clean: false,
154
- driftedFiles: [],
155
- lines: [],
156
- checked: 0,
157
- ignored: 0,
158
- error: "verify --docs: a fresh 'meta docs' produced no pages, so there is nothing to " +
159
- "compare. Check that this project declares metadata the docs surfaces cover.",
160
- };
161
- }
162
- // Committed-but-not-regenerated, inside the directories this command owns.
163
- const freshSet = new Set(fresh);
164
- const ownedOrphans = listFiles(docsDir)
165
- .filter((rel) => !freshSet.has(rel) && isOurs(docsDir, rel))
166
- .sort();
167
- // A page the project deliberately does not commit is not drift.
168
- //
169
- // `docs.outDir` is a directory, not a namespace MetaObjects owns — the same
170
- // jurisdiction ruling 0.24.3 made for `verify --codegen`. But `--codegen` could key
171
- // on `.gen-state/.hashes.json`, a record of what the generator WROTE, and `meta
172
- // docs` keeps no such manifest. What it can ask instead is what the PROJECT says:
173
- // a page under `docs.outDir` that git reports ignored is one the project has
174
- // declared it does not commit.
175
- //
176
- // This is not a guess about intent, it is the project's own statement of it. And it
177
- // cannot be used to hide a file the project actually tracks: git never reports a
178
- // TRACKED path as ignored, whatever pattern matches it, so a committed page is
179
- // always compared.
180
- //
181
- // The exemption is computed over the FRESH set by NAME, present on disk or not.
182
- // That is what makes the verdict machine-independent: a developer box that has run
183
- // `meta docs` locally has all 589 pages on disk and a CI runner has two, and both
184
- // must report the same denominator. Deciding per-file-existence would have made
185
- // `checked` a property of the machine.
186
- //
187
- // Without it: an estate generating three surfaces and committing 2 files of 589 —
188
- // gitignoring the rest with a comment explaining they are a derived view of
189
- // metadata that is already the source of truth — was told it had 588 drifted pages,
190
- // of which exactly ONE was real.
191
- const ignoreCandidates = [...fresh, ...ownedOrphans];
192
- // honourGlobalExcludes: false — this verdict must be a property of the repository.
193
- // A gate that passes for the author and fails in CI because of a personal ignore
194
- // file is worse than no gate.
195
- const ignoreLookup = gitIgnored(docsDir, ignoreCandidates, {
196
- honourGlobalExcludes: false,
197
- });
198
- const ignored = "ignored" in ignoreLookup ? ignoreLookup.ignored : new Set();
199
- const ignoreReason = "unavailable" in ignoreLookup ? ignoreLookup.unavailable : undefined;
200
- const isIgnored = (rel) => ignored.has(toPosix(rel));
201
- const toCheck = fresh.filter((rel) => !isIgnored(rel));
202
- const orphans = ownedOrphans.filter((rel) => !isIgnored(rel));
203
- // BOTH halves. `ignoreCandidates` above tests fresh pages AND owned orphans, so an
204
- // ignored orphan is dropped from the check exactly like an ignored fresh page — but
205
- // counting only the fresh half reported a number SMALLER than the work actually
206
- // skipped. That is the `verify --templates` denominator mistake in miniature (a line
207
- // describing work the command did not do), which is the very thing the orphan loop
208
- // below is written to avoid; the two must be consistent about the same set.
209
- const ignoredCount = fresh.length - toCheck.length + (ownedOrphans.length - orphans.length);
210
- // A gate asked to check pages that could check NONE must not answer "no drift".
211
- if (toCheck.length === 0 && orphans.length === 0) {
212
- return {
213
- clean: false,
214
- driftedFiles: [],
215
- lines: [],
216
- checked: 0,
217
- ignored: ignoredCount,
218
- ignoreReason,
219
- error: `verify --docs: a fresh 'meta docs' produced ${fresh.length} page(s) and every ` +
220
- `one of them is git-ignored, so there is nothing to compare. Commit the pages ` +
221
- `you want checked, or run without --docs.`,
222
- };
223
- }
224
- const driftedFiles = [];
225
- const lines = [];
226
- for (const rel of toCheck) {
227
- const committedPath = join(docsDir, rel);
228
- if (!existsSync(committedPath)) {
229
- driftedFiles.push(rel);
230
- // When the rules could not be consulted the last clause would be a claim we
231
- // have not checked, so it is dropped rather than asserted.
232
- lines.push(ignoreReason === undefined
233
- ? `+ ${rel} (a fresh 'meta docs' emits it; not committed, not git-ignored)`
234
- : `+ ${rel} (a fresh 'meta docs' emits it; not committed)`);
235
- continue;
236
- }
237
- const a = readFileSync(committedPath, "utf8");
238
- const b = readFileSync(join(tempRoot, rel), "utf8");
239
- if (a !== b) {
240
- driftedFiles.push(rel);
241
- lines.push(`~ ${rel} (committed content differs from a fresh 'meta docs')`);
242
- }
243
- }
244
- // Counted into `checked` as well as `driftedFiles` so the failing line and the
245
- // passing line keep dividing by the same set — the `verify --templates` mistake,
246
- // where a red run and a green run reported different denominators for one project.
247
- for (const rel of orphans) {
248
- driftedFiles.push(rel);
249
- lines.push(`- ${rel} (committed; a fresh 'meta docs' no longer emits it)`);
250
- }
251
- return {
252
- clean: driftedFiles.length === 0,
253
- driftedFiles: driftedFiles.sort(),
254
- lines,
255
- checked: toCheck.length + orphans.length,
256
- ignored: ignoredCount,
257
- ignoreReason,
258
- };
259
- }
260
- finally {
261
- rmSync(tempRoot, { recursive: true, force: true });
262
- }
263
- }
264
- //# sourceMappingURL=docs-drift.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"docs-drift.js","sourceRoot":"","sources":["../../../src/lib/docs-drift.ts"],"names":[],"mappings":"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"}
@@ -1,312 +0,0 @@
1
- // `meta verify --docs` — the docs-drift gate.
2
- //
3
- // It runs `meta docs` into a throwaway temp directory and diffs what that produces
4
- // against the committed docs tree. Same shape as `--codegen`, pointed at `docs.outDir`,
5
- // and it exists because nothing checked the docs tree at all: `--codegen` regenerates only
6
- // the config's `outDir`/`targets`, so a model could move and every committed page keep
7
- // describing the previous one indefinitely, with every gate green.
8
- //
9
- // IT CALLS THE DOCS COMMAND, NOT A REIMPLEMENTATION OF IT. The gate and the door must not
10
- // be two answers to "what are the docs" — that is the defect this whole surface is meant to
11
- // prevent, and it would be embarrassing to introduce it in the checker.
12
- //
13
- // TWO DELIBERATE DIFFERENCES FROM `--codegen`, both in the direction of not convicting the
14
- // innocent:
15
- //
16
- // 1. NO HAND-EDIT PRESERVATION, so a byte difference IS drift. Docs pages are read, never
17
- // imported: there is no three-way merge, nothing records what was written, and the
18
- // documented workflow never invites an edit inside one. `--codegen` needs
19
- // `.gen-state/.hashes.json` to tell a preserved hand edit from stale output; here
20
- // there is no such offer to honour.
21
- //
22
- // 2. IT REPORTS A FILE AS EXTRA ONLY WHERE IT OWNS THE DIRECTORY. `docs.outDir` is a
23
- // directory an adopter chooses, and it may well be one full of hand-written
24
- // documentation — this repository's own `docs/` holds a hundred such files. (The
25
- // default moved to `./docs/generated` for exactly that reason: defaulting into the
26
- // most-owned directory name in the ecosystem contradicted this paragraph.) Its
27
- // `api/` subtree is not ours either: on a multi-port project those pages are written
28
- // by the OTHER port's docs command (`mvn metaobjects:docs`, `metaobjects docs`,
29
- // `dotnet meta docs`), which this gate never runs, so a fresh run here legitimately
30
- // emits none of them. `--codegen` can convict a committed-but-not-regenerated file
31
- // because `.gen-state` proves the generator wrote it; over the docs root there is no
32
- // such manifest, and convicting anyway is precisely the jurisdiction mistake
33
- // `--codegen`'s orphan branch was corrected for in 0.24.3.
34
- //
35
- // `agent/` IS ours, and there the ownership question has an answer on disk: the Node
36
- // `meta docs` command is the only thing that writes that directory (its name is not
37
- // configurable), and every page it writes opens with the `@generated` marker. That
38
- // marker is the proof `--codegen` has to consult `.gen-state` for, so BOTH conditions
39
- // are required — under `agent/` AND carrying the marker. A hand-written note dropped
40
- // in `agent/` carries no marker and is left alone, exactly as one in the docs root is.
41
- //
42
- // It matters because the schema page is SKIPPED rather than failed when the expected
43
- // schema cannot be built or no dialect is declared — so without this, a committed
44
- // `agent/schema.md` describing the previous schema passed the gate on exactly the
45
- // change it most needs to flag.
46
- //
47
- // The residual cost is stated rather than hidden: outside `agent/`, a page for an
48
- // entity that was DELETED stays committed and this gate stays green.
49
-
50
- import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, lstatSync } from "node:fs";
51
- import { tmpdir } from "node:os";
52
- import { join, relative, resolve, isAbsolute } from "node:path";
53
- import { docsCommand } from "../commands/docs.js";
54
- import { gitIgnored } from "./git-ignore.js";
55
- import { toPosix } from "./rel-posix.js";
56
-
57
- export interface DocsDriftResult {
58
- /** True when every page a fresh `meta docs` would write is committed and identical. */
59
- clean: boolean;
60
- /** Docs-dir-relative paths that differ — changed, missing, or (under `agent/`) committed
61
- * when a fresh run no longer emits them. Sorted. */
62
- driftedFiles: string[];
63
- /** Human-readable, one line per file. */
64
- lines: string[];
65
- /** The denominator both the passing and the failing report divide by: the pages a fresh
66
- * run produced, plus any committed page under `agent/` it no longer emits, MINUS the
67
- * git-ignored ones. A pure function of (fresh set, repository ignore rules) — never of
68
- * which files happen to be on this machine's disk. */
69
- checked: number;
70
- /** How many pages a fresh run emits that the project git-ignores, so did not check.
71
- * Reported on BOTH the passing and the failing line: a gate that quietly checked two
72
- * of 589 pages and said "no drift" would be indistinguishable from one that checked
73
- * everything. */
74
- ignored: number;
75
- /** Why the ignore rules could not be consulted, when they could not be. Undefined means
76
- * they were. The gate then falls back to checking EVERY page a fresh run emits — the
77
- * pre-existing behaviour — and says so rather than degrading silently. */
78
- ignoreReason?: string | undefined;
79
- /** Set when the gate could not run at all. */
80
- error?: string;
81
- }
82
-
83
- /**
84
- * Recursively list files under `dir`, relative to it.
85
- *
86
- * `lstatSync`, NOT `statSync`, and symlinks are SKIPPED. This walks the committed docs
87
- * tree as well as the fresh one now, and a real repository's `docs/` may hold a symlink to
88
- * a build output that is absent on CI — `statSync` follows it and throws `ENOENT`, which
89
- * the caller reports as "regeneration failed", blaming the fresh run for a dangling link
90
- * in the committed tree. A directory symlink would also let the walk recurse without
91
- * bound. Nothing MetaObjects writes is a symlink, so skipping them cannot hide a page of
92
- * ours; a symlinked page is somebody else's file, which this gate does not judge anyway.
93
- */
94
- function listFiles(dir: string): string[] {
95
- if (!existsSync(dir)) return [];
96
- const out: string[] = [];
97
- const walk = (d: string): void => {
98
- for (const entry of readdirSync(d)) {
99
- const full = join(d, entry);
100
- const st = lstatSync(full);
101
- if (st.isSymbolicLink()) continue;
102
- if (st.isDirectory()) walk(full);
103
- else out.push(relative(dir, full));
104
- }
105
- };
106
- walk(dir);
107
- return out;
108
- }
109
-
110
- /**
111
- * Docs-root-relative prefixes the Node `meta docs` command owns outright — the only
112
- * places a committed page that a fresh run does not emit is drift rather than somebody
113
- * else's file. `agent` is fixed (it is not a configurable subdirectory), which is what
114
- * makes the claim checkable; `api` deliberately is NOT here, because its subdirectory IS
115
- * configurable and on a multi-port project another port's docs command writes it.
116
- */
117
- const OWNED_PREFIXES = ["agent/"] as const;
118
-
119
- /** The marker every generated docs page opens with — the on-disk ownership proof. */
120
- const GENERATED_MARKER = "@generated";
121
-
122
- /**
123
- * True when this committed file is one WE wrote: under a directory this command owns, and
124
- * carrying the generated marker in its opening lines. Both halves are load-bearing — the
125
- * prefix keeps another port's `api/` pages out of jurisdiction, the marker keeps a
126
- * hand-written note inside `agent/` out of it.
127
- */
128
- function isOurs(docsDir: string, rel: string): boolean {
129
- const normalized = toPosix(rel);
130
- if (!OWNED_PREFIXES.some((p) => normalized.startsWith(p))) return false;
131
- try {
132
- return readFileSync(join(docsDir, rel), "utf8")
133
- .split("\n", 3)
134
- .some((line) => line.includes(GENERATED_MARKER));
135
- } catch {
136
- // Unreadable is not proof of ownership, and this gate never convicts without it.
137
- return false;
138
- }
139
- }
140
-
141
- export interface ComputeDocsDriftArgs {
142
- /** Absolute project root — what `meta docs` would be pointed at. */
143
- projectRoot: string;
144
- /** The committed docs directory, already resolved (the project's `docs.outDir`). */
145
- docsDir: string;
146
- /** Process cwd to hand the docs command, for parity with a direct invocation. */
147
- cwd: string;
148
- }
149
-
150
- /**
151
- * Regenerate the docs into a temp tree and diff it against the committed one.
152
- *
153
- * A non-zero exit from the docs command is returned as `error` rather than as drift: a run
154
- * that could not produce the pages has not shown that the committed ones are wrong, and
155
- * reporting it as drift would send a reader to edit files that are probably fine.
156
- */
157
- export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<DocsDriftResult> {
158
- const root = isAbsolute(args.projectRoot) ? args.projectRoot : resolve(args.projectRoot);
159
- const docsDir = isAbsolute(args.docsDir) ? args.docsDir : resolve(root, args.docsDir);
160
-
161
- const tempRoot = mkdtempSync(join(tmpdir(), "meta-verify-docs-"));
162
- try {
163
- // NO `<project-root>` POSITIONAL, deliberately. Passing one is not the same command:
164
- // an explicit path PINS the source set to that directory's own sources (#327), while a
165
- // bare run discovers the project by walking up. The committed pages were produced by
166
- // whatever the user actually ran, and a bare run is what `meta docs` means by default —
167
- // so the gate reproduces that and takes its project root from `cwd`, the way the docs
168
- // command itself does. A gate that resolved a different source set would report pages
169
- // as drifted that a plain regeneration would reproduce exactly.
170
- //
171
- // `--out` IS passed, because the whole point is to write somewhere else; everything
172
- // else — layout, surfaces, api surfaces, base URL — still resolves from the project's
173
- // own config.
174
- const exit = await docsCommand(["--out", tempRoot], args.cwd, { silent: true });
175
- if (exit !== 0) {
176
- return {
177
- clean: false,
178
- driftedFiles: [],
179
- lines: [],
180
- checked: 0,
181
- ignored: 0,
182
- error:
183
- `verify --docs: 'meta docs' exited ${exit}, so the committed pages could not be ` +
184
- `compared against a fresh run. Fix that first — the error is above.`,
185
- };
186
- }
187
-
188
- const fresh = listFiles(tempRoot).sort();
189
- if (fresh.length === 0) {
190
- return {
191
- clean: false,
192
- driftedFiles: [],
193
- lines: [],
194
- checked: 0,
195
- ignored: 0,
196
- error:
197
- "verify --docs: a fresh 'meta docs' produced no pages, so there is nothing to " +
198
- "compare. Check that this project declares metadata the docs surfaces cover.",
199
- };
200
- }
201
-
202
- // Committed-but-not-regenerated, inside the directories this command owns.
203
- const freshSet = new Set(fresh);
204
- const ownedOrphans = listFiles(docsDir)
205
- .filter((rel) => !freshSet.has(rel) && isOurs(docsDir, rel))
206
- .sort();
207
-
208
- // A page the project deliberately does not commit is not drift.
209
- //
210
- // `docs.outDir` is a directory, not a namespace MetaObjects owns — the same
211
- // jurisdiction ruling 0.24.3 made for `verify --codegen`. But `--codegen` could key
212
- // on `.gen-state/.hashes.json`, a record of what the generator WROTE, and `meta
213
- // docs` keeps no such manifest. What it can ask instead is what the PROJECT says:
214
- // a page under `docs.outDir` that git reports ignored is one the project has
215
- // declared it does not commit.
216
- //
217
- // This is not a guess about intent, it is the project's own statement of it. And it
218
- // cannot be used to hide a file the project actually tracks: git never reports a
219
- // TRACKED path as ignored, whatever pattern matches it, so a committed page is
220
- // always compared.
221
- //
222
- // The exemption is computed over the FRESH set by NAME, present on disk or not.
223
- // That is what makes the verdict machine-independent: a developer box that has run
224
- // `meta docs` locally has all 589 pages on disk and a CI runner has two, and both
225
- // must report the same denominator. Deciding per-file-existence would have made
226
- // `checked` a property of the machine.
227
- //
228
- // Without it: an estate generating three surfaces and committing 2 files of 589 —
229
- // gitignoring the rest with a comment explaining they are a derived view of
230
- // metadata that is already the source of truth — was told it had 588 drifted pages,
231
- // of which exactly ONE was real.
232
- const ignoreCandidates = [...fresh, ...ownedOrphans];
233
- // honourGlobalExcludes: false — this verdict must be a property of the repository.
234
- // A gate that passes for the author and fails in CI because of a personal ignore
235
- // file is worse than no gate.
236
- const ignoreLookup = gitIgnored(docsDir, ignoreCandidates, {
237
- honourGlobalExcludes: false,
238
- });
239
- const ignored: ReadonlySet<string> =
240
- "ignored" in ignoreLookup ? ignoreLookup.ignored : new Set<string>();
241
- const ignoreReason: string | undefined =
242
- "unavailable" in ignoreLookup ? ignoreLookup.unavailable : undefined;
243
- const isIgnored = (rel: string): boolean => ignored.has(toPosix(rel));
244
-
245
- const toCheck = fresh.filter((rel) => !isIgnored(rel));
246
- const orphans = ownedOrphans.filter((rel) => !isIgnored(rel));
247
- // BOTH halves. `ignoreCandidates` above tests fresh pages AND owned orphans, so an
248
- // ignored orphan is dropped from the check exactly like an ignored fresh page — but
249
- // counting only the fresh half reported a number SMALLER than the work actually
250
- // skipped. That is the `verify --templates` denominator mistake in miniature (a line
251
- // describing work the command did not do), which is the very thing the orphan loop
252
- // below is written to avoid; the two must be consistent about the same set.
253
- const ignoredCount =
254
- fresh.length - toCheck.length + (ownedOrphans.length - orphans.length);
255
-
256
- // A gate asked to check pages that could check NONE must not answer "no drift".
257
- if (toCheck.length === 0 && orphans.length === 0) {
258
- return {
259
- clean: false,
260
- driftedFiles: [],
261
- lines: [],
262
- checked: 0,
263
- ignored: ignoredCount,
264
- ignoreReason,
265
- error:
266
- `verify --docs: a fresh 'meta docs' produced ${fresh.length} page(s) and every ` +
267
- `one of them is git-ignored, so there is nothing to compare. Commit the pages ` +
268
- `you want checked, or run without --docs.`,
269
- };
270
- }
271
-
272
- const driftedFiles: string[] = [];
273
- const lines: string[] = [];
274
- for (const rel of toCheck) {
275
- const committedPath = join(docsDir, rel);
276
- if (!existsSync(committedPath)) {
277
- driftedFiles.push(rel);
278
- // When the rules could not be consulted the last clause would be a claim we
279
- // have not checked, so it is dropped rather than asserted.
280
- lines.push(
281
- ignoreReason === undefined
282
- ? `+ ${rel} (a fresh 'meta docs' emits it; not committed, not git-ignored)`
283
- : `+ ${rel} (a fresh 'meta docs' emits it; not committed)`,
284
- );
285
- continue;
286
- }
287
- const a = readFileSync(committedPath, "utf8");
288
- const b = readFileSync(join(tempRoot, rel), "utf8");
289
- if (a !== b) {
290
- driftedFiles.push(rel);
291
- lines.push(`~ ${rel} (committed content differs from a fresh 'meta docs')`);
292
- }
293
- }
294
- // Counted into `checked` as well as `driftedFiles` so the failing line and the
295
- // passing line keep dividing by the same set — the `verify --templates` mistake,
296
- // where a red run and a green run reported different denominators for one project.
297
- for (const rel of orphans) {
298
- driftedFiles.push(rel);
299
- lines.push(`- ${rel} (committed; a fresh 'meta docs' no longer emits it)`);
300
- }
301
- return {
302
- clean: driftedFiles.length === 0,
303
- driftedFiles: driftedFiles.sort(),
304
- lines,
305
- checked: toCheck.length + orphans.length,
306
- ignored: ignoredCount,
307
- ignoreReason,
308
- };
309
- } finally {
310
- rmSync(tempRoot, { recursive: true, force: true });
311
- }
312
- }