@metaobjectsdev/cli 0.24.5 → 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.
- package/README.md +2 -1
- package/dist/src/commands/docs.d.ts +14 -1
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +153 -8
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/eject.d.ts +1 -1
- package/dist/src/commands/eject.js +3 -3
- package/dist/src/commands/eject.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +43 -20
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +4 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +31 -6
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/types.d.ts +2 -1
- package/dist/src/commands/types.d.ts.map +1 -1
- package/dist/src/commands/types.js +165 -28
- package/dist/src/commands/types.js.map +1 -1
- package/dist/src/commands/verify.d.ts +9 -1
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +291 -50
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +82 -5
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/advisory.d.ts +77 -0
- package/dist/src/lib/advisory.d.ts.map +1 -0
- package/dist/src/lib/advisory.js +97 -0
- package/dist/src/lib/advisory.js.map +1 -0
- package/dist/src/lib/anti-patterns.d.ts +27 -3
- package/dist/src/lib/anti-patterns.d.ts.map +1 -1
- package/dist/src/lib/anti-patterns.js +145 -8
- package/dist/src/lib/anti-patterns.js.map +1 -1
- package/dist/src/lib/args.d.ts +27 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +13 -1
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/docs-drift.d.ts +31 -0
- package/dist/src/lib/docs-drift.d.ts.map +1 -0
- package/dist/src/lib/docs-drift.js +195 -0
- package/dist/src/lib/docs-drift.js.map +1 -0
- package/dist/src/lib/format.d.ts +10 -0
- package/dist/src/lib/format.d.ts.map +1 -1
- package/dist/src/lib/format.js +15 -0
- package/dist/src/lib/format.js.map +1 -1
- package/dist/src/lib/output.d.ts +13 -0
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +16 -1
- package/dist/src/lib/output.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/docs.ts +194 -8
- package/src/commands/eject.ts +3 -3
- package/src/commands/gen.ts +54 -19
- package/src/commands/init.ts +35 -6
- package/src/commands/types.ts +185 -34
- package/src/commands/verify.ts +350 -46
- package/src/index.ts +91 -6
- package/src/lib/advisory.ts +150 -0
- package/src/lib/anti-patterns.ts +163 -8
- package/src/lib/args.ts +40 -2
- package/src/lib/docs-drift.ts +222 -0
- package/src/lib/format.ts +14 -0
- package/src/lib/output.ts +33 -2
package/src/lib/args.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import { parseAdvisoryLimit } from "./advisory.js";
|
|
2
3
|
|
|
3
4
|
// ---------------------------------------------------------------------------
|
|
4
5
|
// init flags
|
|
@@ -102,6 +103,12 @@ export interface GenFlags {
|
|
|
102
103
|
list: boolean;
|
|
103
104
|
/** Suppress the advisory anti-pattern (verify-as-teacher) pass. */
|
|
104
105
|
noAntipatterns: boolean;
|
|
106
|
+
/**
|
|
107
|
+
* How many advisory lines TEXT output prints before truncating
|
|
108
|
+
* (`DEFAULT_ADVISORY_LIMIT`, or Infinity for `--limit all`). It never applies to
|
|
109
|
+
* a structured payload, which carries every finding.
|
|
110
|
+
*/
|
|
111
|
+
limit: number;
|
|
105
112
|
}
|
|
106
113
|
|
|
107
114
|
export function parseGenArgs(argv: string[]): GenFlags {
|
|
@@ -112,6 +119,7 @@ export function parseGenArgs(argv: string[]): GenFlags {
|
|
|
112
119
|
"baseline": { type: "string" },
|
|
113
120
|
"list": { type: "boolean", default: false },
|
|
114
121
|
"no-antipatterns": { type: "boolean", default: false },
|
|
122
|
+
"limit": { type: "string" },
|
|
115
123
|
},
|
|
116
124
|
strict: true,
|
|
117
125
|
allowPositionals: true,
|
|
@@ -128,6 +136,9 @@ export function parseGenArgs(argv: string[]): GenFlags {
|
|
|
128
136
|
baseline: (baselineRaw as "default" | "fresh" | undefined) ?? "default",
|
|
129
137
|
list: !!values.list,
|
|
130
138
|
noAntipatterns: !!values["no-antipatterns"],
|
|
139
|
+
// Throws on a bad value; the command layer reports it and exits 2, exactly as
|
|
140
|
+
// it does for --baseline above.
|
|
141
|
+
limit: parseAdvisoryLimit(values.limit as string | undefined),
|
|
131
142
|
};
|
|
132
143
|
}
|
|
133
144
|
|
|
@@ -242,6 +253,18 @@ export interface VerifyFlags {
|
|
|
242
253
|
templates: boolean;
|
|
243
254
|
/** Run the codegen-drift gate (regenerate-to-temp and diff committed output). */
|
|
244
255
|
codegen: boolean;
|
|
256
|
+
/**
|
|
257
|
+
* Run the docs-drift gate: regenerate the `meta docs` surfaces into a temp dir and diff
|
|
258
|
+
* them against the committed docs tree.
|
|
259
|
+
*
|
|
260
|
+
* A SEPARATE subverb rather than part of `--codegen`, because they check different
|
|
261
|
+
* trees under different ownership rules: `--codegen` regenerates `outDir`/`targets` and
|
|
262
|
+
* must respect the hand edits `meta gen` preserves, while the docs tree has no merge,
|
|
263
|
+
* no manifest, and lives in a directory (`./docs` by default) full of hand-written
|
|
264
|
+
* files it does not own. Folding them together would have to pick one of those rules
|
|
265
|
+
* for both.
|
|
266
|
+
*/
|
|
267
|
+
docs: boolean;
|
|
245
268
|
/**
|
|
246
269
|
* Replay the committed migration chain into an empty throwaway database and assert
|
|
247
270
|
* it applies (#313). Needs no `--db`: the engine is local and disposable (PGlite
|
|
@@ -255,7 +278,7 @@ export interface VerifyFlags {
|
|
|
255
278
|
* read as that flag's opposite rather than as a replay depth.
|
|
256
279
|
*/
|
|
257
280
|
replaySnapshot: boolean;
|
|
258
|
-
/** Whether ANY explicit subverb flag (--templates/--db/--codegen/--replay*) was passed. */
|
|
281
|
+
/** Whether ANY explicit subverb flag (--templates/--db/--codegen/--docs/--replay*) was passed. */
|
|
259
282
|
anyExplicit: boolean;
|
|
260
283
|
/** Suppress the advisory anti-pattern (verify-as-teacher) pass. */
|
|
261
284
|
noAntipatterns: boolean;
|
|
@@ -273,6 +296,14 @@ export interface VerifyFlags {
|
|
|
273
296
|
* restores the legacy open-attr load (today's behavior). Default false (strict).
|
|
274
297
|
*/
|
|
275
298
|
lax: boolean;
|
|
299
|
+
/**
|
|
300
|
+
* How many advisory lines TEXT output prints per SECTION before truncating
|
|
301
|
+
* (`DEFAULT_ADVISORY_LIMIT`, or Infinity for `--limit all`). Per-section, never
|
|
302
|
+
* shared across them — the reason the two caps were separate in the first place
|
|
303
|
+
* is that one shared budget lets the authoring lint push every gate warning off
|
|
304
|
+
* the end. It never applies to a structured payload, which carries everything.
|
|
305
|
+
*/
|
|
306
|
+
limit: number;
|
|
276
307
|
}
|
|
277
308
|
|
|
278
309
|
export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
@@ -286,6 +317,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
286
317
|
"skip-schema": { type: "boolean", default: false },
|
|
287
318
|
templates: { type: "boolean", default: false },
|
|
288
319
|
codegen: { type: "boolean", default: false },
|
|
320
|
+
docs: { type: "boolean", default: false },
|
|
289
321
|
replay: { type: "boolean", default: false },
|
|
290
322
|
"replay-snapshot": { type: "boolean", default: false },
|
|
291
323
|
"no-antipatterns": { type: "boolean", default: false },
|
|
@@ -293,6 +325,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
293
325
|
lax: { type: "boolean", default: false },
|
|
294
326
|
"d1": { type: "string" },
|
|
295
327
|
"remote": { type: "boolean", default: false },
|
|
328
|
+
"limit": { type: "string" },
|
|
296
329
|
},
|
|
297
330
|
strict: true,
|
|
298
331
|
allowPositionals: false,
|
|
@@ -317,6 +350,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
317
350
|
|
|
318
351
|
const templates = !!values.templates;
|
|
319
352
|
const codegen = !!values.codegen;
|
|
353
|
+
const docs = !!values.docs;
|
|
320
354
|
const replay = !!values.replay;
|
|
321
355
|
const replaySnapshot = !!values["replay-snapshot"];
|
|
322
356
|
// --db is itself an explicit subverb selector: passing a connection URL means
|
|
@@ -325,7 +359,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
325
359
|
// must be listed here or `meta verify --replay` would ALSO run the template gate
|
|
326
360
|
// as the bare-verify default.
|
|
327
361
|
const anyExplicit =
|
|
328
|
-
templates || codegen || values.db !== undefined || dialect === "d1" || replay || replaySnapshot;
|
|
362
|
+
templates || codegen || docs || values.db !== undefined || dialect === "d1" || replay || replaySnapshot;
|
|
329
363
|
|
|
330
364
|
return {
|
|
331
365
|
prompts: values.prompts,
|
|
@@ -335,6 +369,7 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
335
369
|
skipSchema: !!values["skip-schema"],
|
|
336
370
|
templates,
|
|
337
371
|
codegen,
|
|
372
|
+
docs,
|
|
338
373
|
replay,
|
|
339
374
|
replaySnapshot,
|
|
340
375
|
anyExplicit,
|
|
@@ -343,6 +378,9 @@ export function parseVerifyArgs(argv: string[]): VerifyFlags {
|
|
|
343
378
|
lax: !!values.lax,
|
|
344
379
|
d1: values.d1 as string | undefined,
|
|
345
380
|
remote: !!values.remote,
|
|
381
|
+
// Throws on a bad value; the command layer reports it and exits 2, exactly as
|
|
382
|
+
// it does for --dialect and --allow above.
|
|
383
|
+
limit: parseAdvisoryLimit(values.limit as string | undefined),
|
|
346
384
|
};
|
|
347
385
|
}
|
|
348
386
|
|
|
@@ -0,0 +1,222 @@
|
|
|
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`
|
|
23
|
+
// defaults to `./docs`, which in a real repository is full of hand-written
|
|
24
|
+
// documentation — this repository's own `docs/` holds a hundred such files. Its
|
|
25
|
+
// `api/` subtree is not ours either: on a multi-port project those pages are written
|
|
26
|
+
// by the OTHER port's docs command (`mvn metaobjects:docs`, `metaobjects docs`,
|
|
27
|
+
// `dotnet meta docs`), which this gate never runs, so a fresh run here legitimately
|
|
28
|
+
// emits none of them. `--codegen` can convict a committed-but-not-regenerated file
|
|
29
|
+
// because `.gen-state` proves the generator wrote it; over the docs root there is no
|
|
30
|
+
// such manifest, and convicting anyway is precisely the jurisdiction mistake
|
|
31
|
+
// `--codegen`'s orphan branch was corrected for in 0.24.3.
|
|
32
|
+
//
|
|
33
|
+
// `agent/` IS ours, and there the ownership question has an answer on disk: the Node
|
|
34
|
+
// `meta docs` command is the only thing that writes that directory (its name is not
|
|
35
|
+
// configurable), and every page it writes opens with the `@generated` marker. That
|
|
36
|
+
// marker is the proof `--codegen` has to consult `.gen-state` for, so BOTH conditions
|
|
37
|
+
// are required — under `agent/` AND carrying the marker. A hand-written note dropped
|
|
38
|
+
// in `agent/` carries no marker and is left alone, exactly as one in the docs root is.
|
|
39
|
+
//
|
|
40
|
+
// It matters because the schema page is SKIPPED rather than failed when the expected
|
|
41
|
+
// schema cannot be built or no dialect is declared — so without this, a committed
|
|
42
|
+
// `agent/schema.md` describing the previous schema passed the gate on exactly the
|
|
43
|
+
// change it most needs to flag.
|
|
44
|
+
//
|
|
45
|
+
// The residual cost is stated rather than hidden: outside `agent/`, a page for an
|
|
46
|
+
// entity that was DELETED stays committed and this gate stays green.
|
|
47
|
+
|
|
48
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, readdirSync, lstatSync } from "node:fs";
|
|
49
|
+
import { tmpdir } from "node:os";
|
|
50
|
+
import { join, relative, resolve, isAbsolute, sep } from "node:path";
|
|
51
|
+
import { docsCommand } from "../commands/docs.js";
|
|
52
|
+
|
|
53
|
+
export interface DocsDriftResult {
|
|
54
|
+
/** True when every page a fresh `meta docs` would write is committed and identical. */
|
|
55
|
+
clean: boolean;
|
|
56
|
+
/** Docs-dir-relative paths that differ — changed, missing, or (under `agent/`) committed
|
|
57
|
+
* when a fresh run no longer emits them. Sorted. */
|
|
58
|
+
driftedFiles: string[];
|
|
59
|
+
/** Human-readable, one line per file. */
|
|
60
|
+
lines: string[];
|
|
61
|
+
/** The denominator both the passing and the failing report divide by: the pages a fresh
|
|
62
|
+
* run produced, plus any committed page under `agent/` it no longer emits. */
|
|
63
|
+
checked: number;
|
|
64
|
+
/** Set when the gate could not run at all. */
|
|
65
|
+
error?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Recursively list files under `dir`, relative to it.
|
|
70
|
+
*
|
|
71
|
+
* `lstatSync`, NOT `statSync`, and symlinks are SKIPPED. This walks the committed docs
|
|
72
|
+
* tree as well as the fresh one now, and a real repository's `docs/` may hold a symlink to
|
|
73
|
+
* a build output that is absent on CI — `statSync` follows it and throws `ENOENT`, which
|
|
74
|
+
* the caller reports as "regeneration failed", blaming the fresh run for a dangling link
|
|
75
|
+
* in the committed tree. A directory symlink would also let the walk recurse without
|
|
76
|
+
* bound. Nothing MetaObjects writes is a symlink, so skipping them cannot hide a page of
|
|
77
|
+
* ours; a symlinked page is somebody else's file, which this gate does not judge anyway.
|
|
78
|
+
*/
|
|
79
|
+
function listFiles(dir: string): string[] {
|
|
80
|
+
if (!existsSync(dir)) return [];
|
|
81
|
+
const out: string[] = [];
|
|
82
|
+
const walk = (d: string): void => {
|
|
83
|
+
for (const entry of readdirSync(d)) {
|
|
84
|
+
const full = join(d, entry);
|
|
85
|
+
const st = lstatSync(full);
|
|
86
|
+
if (st.isSymbolicLink()) continue;
|
|
87
|
+
if (st.isDirectory()) walk(full);
|
|
88
|
+
else out.push(relative(dir, full));
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
walk(dir);
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Docs-root-relative prefixes the Node `meta docs` command owns outright — the only
|
|
97
|
+
* places a committed page that a fresh run does not emit is drift rather than somebody
|
|
98
|
+
* else's file. `agent` is fixed (it is not a configurable subdirectory), which is what
|
|
99
|
+
* makes the claim checkable; `api` deliberately is NOT here, because its subdirectory IS
|
|
100
|
+
* configurable and on a multi-port project another port's docs command writes it.
|
|
101
|
+
*/
|
|
102
|
+
const OWNED_PREFIXES = ["agent/"] as const;
|
|
103
|
+
|
|
104
|
+
/** The marker every generated docs page opens with — the on-disk ownership proof. */
|
|
105
|
+
const GENERATED_MARKER = "@generated";
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* True when this committed file is one WE wrote: under a directory this command owns, and
|
|
109
|
+
* carrying the generated marker in its opening lines. Both halves are load-bearing — the
|
|
110
|
+
* prefix keeps another port's `api/` pages out of jurisdiction, the marker keeps a
|
|
111
|
+
* hand-written note inside `agent/` out of it.
|
|
112
|
+
*/
|
|
113
|
+
function isOurs(docsDir: string, rel: string): boolean {
|
|
114
|
+
const normalized = rel.split(sep).join("/");
|
|
115
|
+
if (!OWNED_PREFIXES.some((p) => normalized.startsWith(p))) return false;
|
|
116
|
+
try {
|
|
117
|
+
return readFileSync(join(docsDir, rel), "utf8")
|
|
118
|
+
.split("\n", 3)
|
|
119
|
+
.some((line) => line.includes(GENERATED_MARKER));
|
|
120
|
+
} catch {
|
|
121
|
+
// Unreadable is not proof of ownership, and this gate never convicts without it.
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface ComputeDocsDriftArgs {
|
|
127
|
+
/** Absolute project root — what `meta docs` would be pointed at. */
|
|
128
|
+
projectRoot: string;
|
|
129
|
+
/** The committed docs directory, already resolved (the project's `docs.outDir`). */
|
|
130
|
+
docsDir: string;
|
|
131
|
+
/** Process cwd to hand the docs command, for parity with a direct invocation. */
|
|
132
|
+
cwd: string;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Regenerate the docs into a temp tree and diff it against the committed one.
|
|
137
|
+
*
|
|
138
|
+
* A non-zero exit from the docs command is returned as `error` rather than as drift: a run
|
|
139
|
+
* that could not produce the pages has not shown that the committed ones are wrong, and
|
|
140
|
+
* reporting it as drift would send a reader to edit files that are probably fine.
|
|
141
|
+
*/
|
|
142
|
+
export async function computeDocsDrift(args: ComputeDocsDriftArgs): Promise<DocsDriftResult> {
|
|
143
|
+
const root = isAbsolute(args.projectRoot) ? args.projectRoot : resolve(args.projectRoot);
|
|
144
|
+
const docsDir = isAbsolute(args.docsDir) ? args.docsDir : resolve(root, args.docsDir);
|
|
145
|
+
|
|
146
|
+
const tempRoot = mkdtempSync(join(tmpdir(), "meta-verify-docs-"));
|
|
147
|
+
try {
|
|
148
|
+
// NO `<project-root>` POSITIONAL, deliberately. Passing one is not the same command:
|
|
149
|
+
// an explicit path PINS the source set to that directory's own sources (#327), while a
|
|
150
|
+
// bare run discovers the project by walking up. The committed pages were produced by
|
|
151
|
+
// whatever the user actually ran, and a bare run is what `meta docs` means by default —
|
|
152
|
+
// so the gate reproduces that and takes its project root from `cwd`, the way the docs
|
|
153
|
+
// command itself does. A gate that resolved a different source set would report pages
|
|
154
|
+
// as drifted that a plain regeneration would reproduce exactly.
|
|
155
|
+
//
|
|
156
|
+
// `--out` IS passed, because the whole point is to write somewhere else; everything
|
|
157
|
+
// else — layout, surfaces, api surfaces, base URL — still resolves from the project's
|
|
158
|
+
// own config.
|
|
159
|
+
const exit = await docsCommand(["--out", tempRoot], args.cwd, { silent: true });
|
|
160
|
+
if (exit !== 0) {
|
|
161
|
+
return {
|
|
162
|
+
clean: false,
|
|
163
|
+
driftedFiles: [],
|
|
164
|
+
lines: [],
|
|
165
|
+
checked: 0,
|
|
166
|
+
error:
|
|
167
|
+
`verify --docs: 'meta docs' exited ${exit}, so the committed pages could not be ` +
|
|
168
|
+
`compared against a fresh run. Fix that first — the error is above.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const fresh = listFiles(tempRoot).sort();
|
|
173
|
+
if (fresh.length === 0) {
|
|
174
|
+
return {
|
|
175
|
+
clean: false,
|
|
176
|
+
driftedFiles: [],
|
|
177
|
+
lines: [],
|
|
178
|
+
checked: 0,
|
|
179
|
+
error:
|
|
180
|
+
"verify --docs: a fresh 'meta docs' produced no pages, so there is nothing to " +
|
|
181
|
+
"compare. Check that this project declares metadata the docs surfaces cover.",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const driftedFiles: string[] = [];
|
|
186
|
+
const lines: string[] = [];
|
|
187
|
+
for (const rel of fresh) {
|
|
188
|
+
const committedPath = join(docsDir, rel);
|
|
189
|
+
if (!existsSync(committedPath)) {
|
|
190
|
+
driftedFiles.push(rel);
|
|
191
|
+
lines.push(`+ ${rel} (a fresh 'meta docs' emits it; not committed)`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const a = readFileSync(committedPath, "utf8");
|
|
195
|
+
const b = readFileSync(join(tempRoot, rel), "utf8");
|
|
196
|
+
if (a !== b) {
|
|
197
|
+
driftedFiles.push(rel);
|
|
198
|
+
lines.push(`~ ${rel} (committed content differs from a fresh 'meta docs')`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Committed-but-not-regenerated, inside the directories this command owns. Counted
|
|
202
|
+
// into `checked` as well as `driftedFiles` so the failing line and the passing line
|
|
203
|
+
// keep dividing by the same set — the `verify --templates` mistake, where a red run
|
|
204
|
+
// and a green run reported different denominators for the same project.
|
|
205
|
+
const freshSet = new Set(fresh);
|
|
206
|
+
const orphans = listFiles(docsDir)
|
|
207
|
+
.filter((rel) => !freshSet.has(rel) && isOurs(docsDir, rel))
|
|
208
|
+
.sort();
|
|
209
|
+
for (const rel of orphans) {
|
|
210
|
+
driftedFiles.push(rel);
|
|
211
|
+
lines.push(`- ${rel} (committed; a fresh 'meta docs' no longer emits it)`);
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
clean: driftedFiles.length === 0,
|
|
215
|
+
driftedFiles: driftedFiles.sort(),
|
|
216
|
+
lines,
|
|
217
|
+
checked: fresh.length + orphans.length,
|
|
218
|
+
};
|
|
219
|
+
} finally {
|
|
220
|
+
rmSync(tempRoot, { recursive: true, force: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/lib/format.ts
CHANGED
|
@@ -21,3 +21,17 @@ export function resolveFormat(flag: string | undefined, isTTY: boolean): OutputF
|
|
|
21
21
|
export function toonEncode(value: unknown): string {
|
|
22
22
|
return encode(value);
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Put ONE machine-readable document on stdout in the active structured format.
|
|
27
|
+
*
|
|
28
|
+
* Text format writes nothing here — its human rendering is the caller's job, and a
|
|
29
|
+
* command in text mode has already printed it. Callers in a structured format must
|
|
30
|
+
* keep every prose line off stdout (route narration to stderr): a document with a
|
|
31
|
+
* sentence in front of it breaks `| jq` outright. `meta migrate`'s
|
|
32
|
+
* `emitStructuredError` is the same split, made command-locally before this existed.
|
|
33
|
+
*/
|
|
34
|
+
export function emitStructured(payload: unknown, fmt: OutputFormat): void {
|
|
35
|
+
if (fmt === "json") console.log(JSON.stringify(payload, null, 2));
|
|
36
|
+
else if (fmt === "toon") console.log(toonEncode(payload));
|
|
37
|
+
}
|
package/src/lib/output.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Dialect } from "./kysely.js";
|
|
7
7
|
import { toonEncode } from "./format.js";
|
|
8
|
+
import { skippedSection, type AdvisoryFindingRow, type AdvisorySection } from "./advisory.js";
|
|
8
9
|
|
|
9
10
|
export interface FormatOptions {
|
|
10
11
|
isTTY: boolean;
|
|
@@ -29,6 +30,17 @@ export interface GenResultShape {
|
|
|
29
30
|
dialect: Dialect | undefined;
|
|
30
31
|
dryRun: boolean;
|
|
31
32
|
warnings: string[];
|
|
33
|
+
/**
|
|
34
|
+
* The advisory anti-pattern pass, in full. Optional on the TYPE only so the
|
|
35
|
+
* pure formatter tests can build a result without one; `genResultToData` turns
|
|
36
|
+
* an absent section into an explicit "skipped" with a reason, because a payload
|
|
37
|
+
* that omits the advisory is precisely the defect this field closes — a run
|
|
38
|
+
* carrying hundreds of findings read as "all green" to an agent parsing stdout.
|
|
39
|
+
*
|
|
40
|
+
* NEVER capped here. The text renderer truncates for a human's terminal; the
|
|
41
|
+
* structured payload carries every finding.
|
|
42
|
+
*/
|
|
43
|
+
antiPatterns?: AdvisorySection<AdvisoryFindingRow>;
|
|
32
44
|
}
|
|
33
45
|
|
|
34
46
|
const GEN_GLYPHS: Record<GenFileStatus, string> = {
|
|
@@ -179,7 +191,10 @@ export function formatMigrateResult(result: MigrateResultShape, _opts: FormatOpt
|
|
|
179
191
|
// ---------------------------------------------------------------------------
|
|
180
192
|
|
|
181
193
|
export function genResultToData(result: GenResultShape): {
|
|
182
|
-
gen: { file: string; status: GenFileStatus }[];
|
|
194
|
+
gen: { file: string; status: GenFileStatus }[];
|
|
195
|
+
summary: string;
|
|
196
|
+
help: string[];
|
|
197
|
+
antiPatterns: AdvisorySection<AdvisoryFindingRow>;
|
|
183
198
|
} {
|
|
184
199
|
const counts = result.files.reduce<Record<GenFileStatus, number>>(
|
|
185
200
|
(a, f) => ((a[f.status] = (a[f.status] ?? 0) + 1), a),
|
|
@@ -198,7 +213,23 @@ export function genResultToData(result: GenResultShape): {
|
|
|
198
213
|
const help = result.files.length === 0
|
|
199
214
|
? ["author entities in this project's metadata sources then re-run `meta gen`"]
|
|
200
215
|
: ["typecheck the generated code with `npx tsc`", "create your database tables with `meta migrate --from-db --db <url> --dialect <sqlite|postgres> --slug init --apply`"];
|
|
201
|
-
|
|
216
|
+
// An absent section is STATED, never omitted: a reader must be able to tell
|
|
217
|
+
// "the scan found nothing" from "the scan never ran".
|
|
218
|
+
const antiPatterns = result.antiPatterns
|
|
219
|
+
?? skippedSection<AdvisoryFindingRow>("the advisory anti-pattern pass did not run for this result");
|
|
220
|
+
// One more next step when there is something to act on — the whole reason the
|
|
221
|
+
// findings are in the payload is that somebody can now act on all of them.
|
|
222
|
+
if (antiPatterns.total > 0) {
|
|
223
|
+
help.push(
|
|
224
|
+
`${antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
gen: result.files.map((f) => ({ file: f.path, status: f.status })),
|
|
229
|
+
summary,
|
|
230
|
+
help,
|
|
231
|
+
antiPatterns,
|
|
232
|
+
};
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
export function formatGenResultToon(result: GenResultShape): string {
|