@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/commands/verify.ts
CHANGED
|
@@ -10,6 +10,11 @@
|
|
|
10
10
|
import { join, resolve as resolvePath } from "node:path";
|
|
11
11
|
import { parseVerifyArgs, type MigrateFlags } from "../lib/args.js";
|
|
12
12
|
import { log } from "../lib/log.js";
|
|
13
|
+
import { emitStructured, type OutputFormat } from "../lib/format.js";
|
|
14
|
+
import {
|
|
15
|
+
antiPatternRows, ranSection, skippedSection, warnCapped,
|
|
16
|
+
type AdvisoryDiagnosticRow, type AdvisoryFindingRow, type AdvisorySection,
|
|
17
|
+
} from "../lib/advisory.js";
|
|
13
18
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
14
19
|
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
15
20
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
@@ -17,6 +22,7 @@ import { FileProvider } from "../lib/file-provider.js";
|
|
|
17
22
|
import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
18
23
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
19
24
|
import { computeCodegenDrift } from "../lib/codegen-drift.js";
|
|
25
|
+
import { computeDocsDrift } from "../lib/docs-drift.js";
|
|
20
26
|
import {
|
|
21
27
|
checkRequirements, summariseRequirements, scanRequirements, type Diagnostic,
|
|
22
28
|
} from "../lib/requirement-check.js";
|
|
@@ -29,7 +35,7 @@ import {
|
|
|
29
35
|
type WranglerRunner,
|
|
30
36
|
} from "../lib/wrangler.js";
|
|
31
37
|
import type { MetaobjectsGenConfig } from "@metaobjectsdev/codegen-ts";
|
|
32
|
-
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
38
|
+
import { buildProjectionViews, resolveDocsConfig } from "@metaobjectsdev/codegen-ts";
|
|
33
39
|
import { buildKyselyFromUrl, inferDialect, type Dialect } from "../lib/kysely.js";
|
|
34
40
|
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
35
41
|
import {
|
|
@@ -117,13 +123,29 @@ export async function verifyCommand(
|
|
|
117
123
|
cwd: string,
|
|
118
124
|
/** Injectable wrangler runner (D1 path only) — tests pass a mock; production uses the default. */
|
|
119
125
|
wranglerRunner?: WranglerRunner,
|
|
126
|
+
/**
|
|
127
|
+
* Output format, threaded from the global `--format` flag (4th position, matching
|
|
128
|
+
* `migrateCommand`). It used to be passed to `gen` and `migrate` ONLY, so
|
|
129
|
+
* `meta verify --format json` was accepted, validated, exited 0 — and printed
|
|
130
|
+
* human text. An adopter who guessed the right flag lost silently.
|
|
131
|
+
*/
|
|
132
|
+
fmt: OutputFormat = "text",
|
|
120
133
|
): Promise<number> {
|
|
121
134
|
const activeWranglerRunner = wranglerRunner ?? defaultWranglerRunner;
|
|
135
|
+
// In a structured format stdout carries exactly ONE document, so every narration
|
|
136
|
+
// line moves to stderr (`say` below) instead of being printed into the payload or
|
|
137
|
+
// dropped. Same split `meta migrate` makes for its out-of-scope note.
|
|
138
|
+
const structured = fmt !== "text";
|
|
139
|
+
const say = (msg: string): void => { if (structured) log.warn(msg); else log.info(msg); };
|
|
122
140
|
let flags: ReturnType<typeof parseVerifyArgs>;
|
|
123
141
|
try {
|
|
124
142
|
flags = parseVerifyArgs(args);
|
|
125
143
|
} catch (err) {
|
|
126
|
-
|
|
144
|
+
const msg = (err as Error).message;
|
|
145
|
+
log.error(msg);
|
|
146
|
+
// A structured caller gets a structured refusal. Exiting 2 with an EMPTY stdout
|
|
147
|
+
// is the same silence this command is being fixed for, one level up.
|
|
148
|
+
emitStructured({ error: msg, hint: "run `meta verify --help` for the accepted flags" }, fmt);
|
|
127
149
|
return 2;
|
|
128
150
|
}
|
|
129
151
|
|
|
@@ -136,10 +158,12 @@ export async function verifyCommand(
|
|
|
136
158
|
// The schema gate is selected by the presence of --db, or --dialect d1 (#225 —
|
|
137
159
|
// D1 has no URL connection); that check lives inside runSchemaVerify.
|
|
138
160
|
const runCodegen = flags.codegen;
|
|
161
|
+
const runDocs = flags.docs;
|
|
139
162
|
if (!flags.anyExplicit) {
|
|
140
|
-
|
|
163
|
+
say(
|
|
141
164
|
"meta verify — running --templates (default). Explicit subverbs: " +
|
|
142
165
|
"--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
|
|
166
|
+
"--docs (docs drift), " +
|
|
143
167
|
"--replay/--replay-snapshot (the committed migration chain replays from empty).",
|
|
144
168
|
);
|
|
145
169
|
}
|
|
@@ -157,7 +181,9 @@ export async function verifyCommand(
|
|
|
157
181
|
try {
|
|
158
182
|
collection = await resolveCollection(cwd);
|
|
159
183
|
} catch (err) {
|
|
160
|
-
|
|
184
|
+
const msg = (err as Error).message;
|
|
185
|
+
log.error(msg);
|
|
186
|
+
emitStructured({ error: msg, hint: "declare metadata `sources` in .metaobjects/config.json, or run `meta init`" }, fmt);
|
|
161
187
|
return 2;
|
|
162
188
|
}
|
|
163
189
|
|
|
@@ -225,17 +251,39 @@ export async function verifyCommand(
|
|
|
225
251
|
} catch (err) {
|
|
226
252
|
const msg = (err as Error).message;
|
|
227
253
|
log.error(`failed to load metadata: ${msg}`);
|
|
228
|
-
// Strict-load rejection (ADR-0023)
|
|
229
|
-
//
|
|
254
|
+
// Strict-load rejection (ADR-0023). Two different failures reach here and they need
|
|
255
|
+
// different advice:
|
|
256
|
+
//
|
|
257
|
+
// a TYPO'd or genuinely undeclared attr — the three exits below are right;
|
|
258
|
+
// a RETIRED attr — the loader already knows the exits and attaches them as
|
|
259
|
+
// ADR-0009 `suggestions[]`, so we print those and say nothing of our own.
|
|
260
|
+
//
|
|
261
|
+
// The distinction is not cosmetic. The middle generic exit — the `attr.properties`
|
|
262
|
+
// bag — is EXEMPT from the strict-attr check by subtype, so it loads. Offer it for a
|
|
263
|
+
// retired attribute and the author gets a green `meta verify` over a value that now
|
|
264
|
+
// reaches nothing: a correct, loud failure converted into a quiet, wrong pass. That is
|
|
265
|
+
// strictly worse than the error it replaced.
|
|
230
266
|
const code = (err as { code?: string }).code;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
267
|
+
const suggestions = (err as { suggestions?: string[] }).suggestions;
|
|
268
|
+
const strictHint =
|
|
269
|
+
"meta verify is strict (ADR-0023): every authored @attr must be declared. " +
|
|
270
|
+
"Fix: register the attr on a metadata provider, OR move arbitrary " +
|
|
271
|
+
"author-supplied properties into an `attr.properties` bag, OR re-run " +
|
|
272
|
+
"with `meta verify --lax` to keep the legacy open-attr load.";
|
|
273
|
+
const isStrictAttr = code === ERR_UNKNOWN_ATTR || msg.includes("Unknown attribute");
|
|
274
|
+
if (suggestions !== undefined && suggestions.length > 0) {
|
|
275
|
+
for (const s of suggestions) log.error(` ${s}`);
|
|
276
|
+
} else if (isStrictAttr) {
|
|
277
|
+
log.error(strictHint);
|
|
238
278
|
}
|
|
279
|
+
// The same refusal, machine-readable. No gate ran, so there is no verdict to
|
|
280
|
+
// report — only why there is none.
|
|
281
|
+
emitStructured({
|
|
282
|
+
error: `failed to load metadata: ${msg}`,
|
|
283
|
+
hint: suggestions !== undefined && suggestions.length > 0
|
|
284
|
+
? suggestions.join(" ")
|
|
285
|
+
: isStrictAttr ? strictHint : "fix the metadata error above and re-run",
|
|
286
|
+
}, fmt);
|
|
239
287
|
return 1;
|
|
240
288
|
}
|
|
241
289
|
|
|
@@ -248,12 +296,33 @@ export async function verifyCommand(
|
|
|
248
296
|
const promptsDir = join(projectRoot, flags.prompts ?? DEFAULT_PROMPTS_DIR);
|
|
249
297
|
const provider = new FileProvider(promptsDir);
|
|
250
298
|
|
|
299
|
+
// The two advisory sections, captured as the gates run so the structured payload
|
|
300
|
+
// can carry them IN FULL. They were previously formatted straight to stderr and
|
|
301
|
+
// existed nowhere else, which is why 96% of a 239-finding report was unreachable
|
|
302
|
+
// by any flag, env var or format.
|
|
303
|
+
let requirementSection: AdvisorySection<AdvisoryDiagnosticRow> =
|
|
304
|
+
skippedSection("the requirement pass did not run");
|
|
305
|
+
let antiPatternSection: AdvisorySection<AdvisoryFindingRow> =
|
|
306
|
+
skippedSection("the advisory anti-pattern pass did not run");
|
|
307
|
+
// The ledger counts `meta verify` prints on every run. Undefined for a project
|
|
308
|
+
// declaring no requirement.* node at all (opt-in by declaration) — the payload
|
|
309
|
+
// then omits the block rather than reporting zeroes that would read as an empty
|
|
310
|
+
// ledger instead of no ledger.
|
|
311
|
+
let requirementCounts: RequirementCounts | undefined;
|
|
312
|
+
|
|
313
|
+
// Whether the schema gate is selected. `runSchemaVerify` makes this decision
|
|
314
|
+
// internally (it is selected by --db or --dialect d1, not by a subverb flag);
|
|
315
|
+
// naming it here lets the structured payload report "ran" from the SAME
|
|
316
|
+
// expression rather than a second guess at it.
|
|
317
|
+
const ranSchemaGate = (flags.db !== undefined || flags.dialect === "d1") && !flags.skipSchema;
|
|
318
|
+
|
|
251
319
|
// Exit-code composition: the overall result is the MAX across every selected
|
|
252
320
|
// subverb so ANY kind of drift fails CI. Each gate only runs when its mode is
|
|
253
321
|
// selected; an unselected gate contributes 0.
|
|
254
322
|
const templateExit = runTemplates ? runTemplateVerify() : 0;
|
|
255
323
|
const schemaExit = await runSchemaVerify();
|
|
256
324
|
const codegenExit = runCodegen ? await runCodegenVerify() : 0;
|
|
325
|
+
const docsExit = runDocs ? await runDocsVerify() : 0;
|
|
257
326
|
// Requirements have no subverb: `requirement.*` nodes are metadata, so they
|
|
258
327
|
// are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
|
|
259
328
|
// requirement nodes is silent, not in drift.
|
|
@@ -268,9 +337,42 @@ export async function verifyCommand(
|
|
|
268
337
|
// model. Warnings ONLY — never changes the exit code (bias to under-flagging).
|
|
269
338
|
// Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
|
|
270
339
|
// noisy project (both opt-outs work on `meta verify` and `meta gen`).
|
|
271
|
-
|
|
340
|
+
runAntiPatternAdvisory();
|
|
341
|
+
|
|
342
|
+
const exitCode = Math.max(
|
|
343
|
+
templateExit,
|
|
344
|
+
schemaExit,
|
|
345
|
+
codegenExit,
|
|
346
|
+
docsExit,
|
|
347
|
+
requirementExit,
|
|
348
|
+
replayExit,
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
if (structured) {
|
|
352
|
+
emitStructured(
|
|
353
|
+
buildVerifyPayload({
|
|
354
|
+
gates: [
|
|
355
|
+
{ gate: "templates", ran: runTemplates, ok: templateExit === 0 },
|
|
356
|
+
// The schema gate decides internally whether it is selected (--db, or
|
|
357
|
+
// --dialect d1), so "ran" is read off the same signals rather than
|
|
358
|
+
// restated: a payload that claims a gate ran when it did not is the
|
|
359
|
+
// failure mode this whole change exists to remove.
|
|
360
|
+
{ gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
|
|
361
|
+
{ gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
|
|
362
|
+
{ gate: "docs", ran: runDocs, ok: docsExit === 0 },
|
|
363
|
+
{ gate: "requirements", ran: true, ok: requirementExit === 0 },
|
|
364
|
+
{ gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
|
|
365
|
+
],
|
|
366
|
+
exitCode,
|
|
367
|
+
requirements: requirementSection,
|
|
368
|
+
requirementCounts,
|
|
369
|
+
antiPatterns: antiPatternSection,
|
|
370
|
+
}),
|
|
371
|
+
fmt,
|
|
372
|
+
);
|
|
373
|
+
}
|
|
272
374
|
|
|
273
|
-
return
|
|
375
|
+
return exitCode;
|
|
274
376
|
|
|
275
377
|
// -- replay (#313) ---------------------------------------------------------
|
|
276
378
|
/**
|
|
@@ -359,10 +461,10 @@ export async function verifyCommand(
|
|
|
359
461
|
// empty replay is still compared against a snapshot that may record dozens of
|
|
360
462
|
// tables, rather than reporting success having compared nothing.
|
|
361
463
|
if (applied.pending.length === 0) {
|
|
362
|
-
|
|
464
|
+
say(`meta verify --replay: no committed migrations — nothing to replay`);
|
|
363
465
|
if (!flags.replaySnapshot) return 0;
|
|
364
466
|
} else {
|
|
365
|
-
|
|
467
|
+
say(
|
|
366
468
|
`meta verify --replay — the committed chain applies to an empty ${dialect} database ` +
|
|
367
469
|
`(${applied.applied.length} migration(s)).`,
|
|
368
470
|
);
|
|
@@ -402,11 +504,11 @@ export async function verifyCommand(
|
|
|
402
504
|
try {
|
|
403
505
|
snapshot = await readSnapshot(snapshotPath(dir, dialect));
|
|
404
506
|
} catch {
|
|
405
|
-
|
|
507
|
+
say(`meta verify --replay-snapshot: the committed snapshot could not be read — nothing to compare`);
|
|
406
508
|
return 0;
|
|
407
509
|
}
|
|
408
510
|
if (snapshot === null) {
|
|
409
|
-
|
|
511
|
+
say(`meta verify --replay-snapshot: no committed snapshot — nothing to compare`);
|
|
410
512
|
return 0;
|
|
411
513
|
}
|
|
412
514
|
|
|
@@ -440,7 +542,7 @@ export async function verifyCommand(
|
|
|
440
542
|
...(governed !== undefined ? { governed } : {}),
|
|
441
543
|
});
|
|
442
544
|
if (result.ok) {
|
|
443
|
-
|
|
545
|
+
say(`meta verify --replay-snapshot — the replayed chain reproduces the committed snapshot.`);
|
|
444
546
|
return 0;
|
|
445
547
|
}
|
|
446
548
|
|
|
@@ -486,34 +588,50 @@ export async function verifyCommand(
|
|
|
486
588
|
// all, which is why nothing had ever flagged the templates living in them. No
|
|
487
589
|
// check can see a tree it was never pointed at, so the honest fix is to publish
|
|
488
590
|
// what the count was taken over and let a wrong number be noticeable.
|
|
489
|
-
|
|
591
|
+
say(
|
|
490
592
|
`meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
|
|
491
593
|
`${s.architectural} architectural) — ${parts.join(", ")}; ` +
|
|
492
594
|
`${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
|
|
493
595
|
`counted over ${collection.files.length} metadata file(s).`,
|
|
494
596
|
);
|
|
495
597
|
if (s.undecided > 0) {
|
|
496
|
-
|
|
598
|
+
say(
|
|
497
599
|
`meta verify — requirements: ${s.undecided} recorded gap(s) with no @disposition. ` +
|
|
498
600
|
`These are known problems nobody has ruled on — set 'accepted' or 'deferred' to close the question.`,
|
|
499
601
|
);
|
|
500
602
|
}
|
|
603
|
+
// The same counts, machine-readable. `metadataFiles` travels with them for the
|
|
604
|
+
// reason the text line carries it: the claimed/total ratio is only ever taken
|
|
605
|
+
// over what actually loaded, so a reader needs the denominator's provenance to
|
|
606
|
+
// notice a spine that covers half an estate.
|
|
607
|
+
requirementCounts = {
|
|
608
|
+
total: s.total,
|
|
609
|
+
functional: s.functional,
|
|
610
|
+
architectural: s.architectural,
|
|
611
|
+
byStatus: order
|
|
612
|
+
.filter((k) => (s.byStatus[k] ?? 0) > 0)
|
|
613
|
+
.map((k) => ({ status: k, count: s.byStatus[k] ?? 0 })),
|
|
614
|
+
undecided: s.undecided,
|
|
615
|
+
entitiesClaimed: s.entitiesClaimed,
|
|
616
|
+
entitiesTotal: s.entitiesTotal,
|
|
617
|
+
metadataFiles: collection.files.length,
|
|
618
|
+
};
|
|
501
619
|
}
|
|
502
620
|
|
|
503
621
|
const errors = diags.filter((d) => d.severity === "error");
|
|
504
622
|
const warns = diags.filter((d) => d.severity === "warn");
|
|
505
|
-
|
|
506
|
-
|
|
623
|
+
// Named `fmtDiag`, not `fmt`: `fmt` is this command's OUTPUT FORMAT parameter,
|
|
624
|
+
// and a shadow of it inside the one function that must not confuse the two is
|
|
625
|
+
// how a structured run quietly reverts to text.
|
|
626
|
+
const fmtDiag = (d: Diagnostic): string =>
|
|
507
627
|
` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
};
|
|
515
|
-
for (const d of errors) log.error(fmt(d));
|
|
516
|
-
warnCapped(warns);
|
|
628
|
+
// Capped per SECTION, never across them: a ledger of a few hundred entries can
|
|
629
|
+
// produce hundreds of prose findings, and a shared budget would let the advisory
|
|
630
|
+
// lint push every gate warning off the end. The cap VALUE is now one shared
|
|
631
|
+
// constant (`--limit`), so raising it cannot miss a section — errors stay
|
|
632
|
+
// uncapped, as they always were.
|
|
633
|
+
for (const d of errors) log.error(fmtDiag(d));
|
|
634
|
+
warnCapped(warns.map(fmtDiag), flags.limit, { structured });
|
|
517
635
|
|
|
518
636
|
// -- the authoring lint: its own section, its own cap ----------------------
|
|
519
637
|
// Separate from the gate above because it makes a different claim. The gate
|
|
@@ -542,9 +660,17 @@ export async function verifyCommand(
|
|
|
542
660
|
`meta verify — requirements: ${lint.length} authoring warning(s) ` +
|
|
543
661
|
`(advisory — does not fail the build):`,
|
|
544
662
|
);
|
|
545
|
-
warnCapped(lint);
|
|
663
|
+
warnCapped(lint.map(fmtDiag), flags.limit, { structured });
|
|
546
664
|
}
|
|
547
665
|
|
|
666
|
+
// Everything this pass found, uncapped, for the structured payload — gate
|
|
667
|
+
// diagnostics and lint findings in one list, each row saying which it is, so a
|
|
668
|
+
// reader can tell what can fail a build from what never can.
|
|
669
|
+
requirementSection = ranSection<AdvisoryDiagnosticRow>([
|
|
670
|
+
...diags.map((d) => toDiagnosticRow(d, "gate")),
|
|
671
|
+
...lint.map((d) => toDiagnosticRow(d, "lint")),
|
|
672
|
+
]);
|
|
673
|
+
|
|
548
674
|
if (errors.length > 0) {
|
|
549
675
|
log.error(`meta verify — requirements: ${errors.length} error(s).`);
|
|
550
676
|
return 1;
|
|
@@ -553,21 +679,34 @@ export async function verifyCommand(
|
|
|
553
679
|
}
|
|
554
680
|
|
|
555
681
|
// -- verify-as-teacher (advisory) ------------------------------------------
|
|
682
|
+
// Records its result EITHER WAY — a skip carries its reason rather than looking
|
|
683
|
+
// like a clean scan. Warnings only; nothing here reaches the exit code (the
|
|
684
|
+
// scanner's own header: bias to under-flagging, never a non-zero exit).
|
|
556
685
|
function runAntiPatternAdvisory(): void {
|
|
686
|
+
if (flags.noAntipatterns) {
|
|
687
|
+
antiPatternSection = skippedSection("suppressed by --no-antipatterns");
|
|
688
|
+
return;
|
|
689
|
+
}
|
|
690
|
+
if (process.env.META_NO_ANTIPATTERNS === "1") {
|
|
691
|
+
antiPatternSection = skippedSection("suppressed by META_NO_ANTIPATTERNS=1");
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
557
694
|
let findings;
|
|
558
695
|
try {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
696
|
+
const ignore = forgeConfig?.verify?.antiPatternIgnore;
|
|
697
|
+
findings = scanSourceForAntiPatterns(projectRoot, ignore !== undefined ? { ignore } : undefined);
|
|
698
|
+
} catch (err) {
|
|
699
|
+
// Never let an advisory scan break verify — and never report it as clean.
|
|
700
|
+
antiPatternSection = skippedSection(`the scan failed: ${(err as Error).message}`);
|
|
701
|
+
return;
|
|
562
702
|
}
|
|
703
|
+
antiPatternSection = ranSection(antiPatternRows(findings));
|
|
563
704
|
if (findings.length === 0) return;
|
|
564
|
-
const CAP = 10;
|
|
565
705
|
log.warn(
|
|
566
706
|
`meta verify — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
|
|
567
707
|
`(advisory — does not fail the build):`,
|
|
568
708
|
);
|
|
569
|
-
|
|
570
|
-
if (findings.length > CAP) log.warn(` …and ${findings.length - CAP} more.`);
|
|
709
|
+
warnCapped(findings.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
571
710
|
}
|
|
572
711
|
|
|
573
712
|
// -- template (prompt / output) drift --------------------------------------
|
|
@@ -575,7 +714,7 @@ export async function verifyCommand(
|
|
|
575
714
|
// ADR-0039: effective children — resolve rather than rely on root being unextended.
|
|
576
715
|
const templates = root.children().filter((c) => c.type === TYPE_TEMPLATE);
|
|
577
716
|
if (templates.length === 0) {
|
|
578
|
-
|
|
717
|
+
say("meta verify — no template.* nodes found; nothing to check.");
|
|
579
718
|
return 0;
|
|
580
719
|
}
|
|
581
720
|
|
|
@@ -688,7 +827,7 @@ export async function verifyCommand(
|
|
|
688
827
|
);
|
|
689
828
|
return 1;
|
|
690
829
|
}
|
|
691
|
-
|
|
830
|
+
say(
|
|
692
831
|
`meta verify — ${checkedTemplates} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`,
|
|
693
832
|
);
|
|
694
833
|
return 0;
|
|
@@ -969,7 +1108,7 @@ export async function verifyCommand(
|
|
|
969
1108
|
// annotate them as external (declared) rather than let them vanish silently.
|
|
970
1109
|
const externalDeclared = collectUnmanagedNames(root);
|
|
971
1110
|
if (externalDeclared.length > 0) {
|
|
972
|
-
|
|
1111
|
+
say(
|
|
973
1112
|
`meta verify — ${externalDeclared.length} object(s) external (declared @unmanaged, managed elsewhere): ${externalDeclared.join(", ")}`,
|
|
974
1113
|
);
|
|
975
1114
|
}
|
|
@@ -978,12 +1117,12 @@ export async function verifyCommand(
|
|
|
978
1117
|
// was NOT checked, and silence would misreport it as checked-and-clean. Shared
|
|
979
1118
|
// wording with `meta migrate` — one declaration, one sentence about it.
|
|
980
1119
|
if (driftResult.outOfScope.length > 0) {
|
|
981
|
-
|
|
1120
|
+
say(outOfScopeNote("verify", driftResult.outOfScope));
|
|
982
1121
|
}
|
|
983
1122
|
|
|
984
1123
|
const changes = driftResult.changes;
|
|
985
1124
|
if (changes.length === 0 && ledgerDrift.length === 0) {
|
|
986
|
-
|
|
1125
|
+
say(`meta verify — schema in sync with ${displayUrl}.`);
|
|
987
1126
|
return 0;
|
|
988
1127
|
}
|
|
989
1128
|
|
|
@@ -1037,6 +1176,10 @@ export async function verifyCommand(
|
|
|
1037
1176
|
});
|
|
1038
1177
|
} catch (err) {
|
|
1039
1178
|
log.error(`verify --codegen: failed to load this package's metadata: ${(err as Error).message}`);
|
|
1179
|
+
// The second door onto the same strict load. It carried no remedy at all, which
|
|
1180
|
+
// meant a retirement diagnosed here told the author what broke and nothing about
|
|
1181
|
+
// how to fix it — the half-true rule this file's sibling comment warns about.
|
|
1182
|
+
for (const s of (err as { suggestions?: string[] }).suggestions ?? []) log.error(` ${s}`);
|
|
1040
1183
|
return 2;
|
|
1041
1184
|
}
|
|
1042
1185
|
}
|
|
@@ -1055,7 +1198,7 @@ export async function verifyCommand(
|
|
|
1055
1198
|
}
|
|
1056
1199
|
|
|
1057
1200
|
if (result.clean) {
|
|
1058
|
-
|
|
1201
|
+
say("meta verify — generated output is in sync with the metadata (no codegen drift).");
|
|
1059
1202
|
return 0;
|
|
1060
1203
|
}
|
|
1061
1204
|
|
|
@@ -1066,6 +1209,64 @@ export async function verifyCommand(
|
|
|
1066
1209
|
log.error("Run 'meta gen' to regenerate, then commit the result.");
|
|
1067
1210
|
return 1;
|
|
1068
1211
|
}
|
|
1212
|
+
|
|
1213
|
+
// -- docs drift -------------------------------------------------------------
|
|
1214
|
+
// Gated on --docs. Runs `meta docs` into a temp dir and diffs the committed docs
|
|
1215
|
+
// tree. See lib/docs-drift.ts for the two deliberate differences from --codegen
|
|
1216
|
+
// (a byte difference IS drift here, and a committed file a regen would not emit is
|
|
1217
|
+
// never reported — `docs.outDir` is full of files MetaObjects did not write).
|
|
1218
|
+
//
|
|
1219
|
+
// It needs a config for the same reason `--codegen` does: `docs.outDir` says which
|
|
1220
|
+
// tree to compare against, and the `agent` surface will not even materialise
|
|
1221
|
+
// without one.
|
|
1222
|
+
async function runDocsVerify(): Promise<number> {
|
|
1223
|
+
if (forgeConfig === undefined) {
|
|
1224
|
+
log.error(
|
|
1225
|
+
"verify --docs: no metaobjects.config.ts found (or it is invalid) — " +
|
|
1226
|
+
"cannot locate the committed docs tree to diff against. " +
|
|
1227
|
+
"Run 'meta init' to scaffold one, or run without --docs.",
|
|
1228
|
+
);
|
|
1229
|
+
return 2;
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// Resolved through the SAME resolver `meta docs` uses, so a project that moved its
|
|
1233
|
+
// docs elsewhere is checked where its docs actually are. `outputLayout` is the
|
|
1234
|
+
// documented fallback for `docs.layout`.
|
|
1235
|
+
const docsCfg = resolveDocsConfig(forgeConfig.docs, {}, forgeConfig.outputLayout ?? "flat");
|
|
1236
|
+
let result;
|
|
1237
|
+
try {
|
|
1238
|
+
result = await computeDocsDrift({
|
|
1239
|
+
projectRoot: genConfigDir,
|
|
1240
|
+
docsDir: resolvePath(genConfigDir, docsCfg.outDir),
|
|
1241
|
+
cwd: genConfigDir,
|
|
1242
|
+
});
|
|
1243
|
+
} catch (err) {
|
|
1244
|
+
log.error(`verify --docs: regeneration failed: ${(err as Error).message}`);
|
|
1245
|
+
return 1;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (result.error !== undefined) {
|
|
1249
|
+
log.error(result.error);
|
|
1250
|
+
return 2;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
if (result.clean) {
|
|
1254
|
+
// The denominator is what was actually COMPARED, so the passing line and the
|
|
1255
|
+
// failing line below count the same set. A gate whose two halves divide by
|
|
1256
|
+
// different numbers is how `verify --templates` came to report seven templates
|
|
1257
|
+
// vanishing between a red run and a green one.
|
|
1258
|
+
say(`meta verify — ${result.checked} docs page(s) match a fresh 'meta docs' (no docs drift).`);
|
|
1259
|
+
return 0;
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
log.error(
|
|
1263
|
+
`meta verify — docs drift (${result.driftedFiles.length} of ${result.checked} page(s) ` +
|
|
1264
|
+
`differ from a fresh 'meta docs'):`,
|
|
1265
|
+
);
|
|
1266
|
+
for (const line of result.lines) log.error(` ${line}`);
|
|
1267
|
+
log.error("Run 'meta docs' to regenerate, then commit the result.");
|
|
1268
|
+
return 1;
|
|
1269
|
+
}
|
|
1069
1270
|
}
|
|
1070
1271
|
|
|
1071
1272
|
/**
|
|
@@ -1111,3 +1312,106 @@ function summarizeDrift(changes: Change[]): string[] {
|
|
|
1111
1312
|
return `${p.glyph} ${p.noun} ${describeChange(c)}`;
|
|
1112
1313
|
});
|
|
1113
1314
|
}
|
|
1315
|
+
|
|
1316
|
+
// ---------------------------------------------------------------------------
|
|
1317
|
+
// structured output (--format toon|json)
|
|
1318
|
+
// ---------------------------------------------------------------------------
|
|
1319
|
+
|
|
1320
|
+
/** The ledger counts `meta verify` prints on every run, machine-readable. */
|
|
1321
|
+
interface RequirementCounts {
|
|
1322
|
+
total: number;
|
|
1323
|
+
functional: number;
|
|
1324
|
+
architectural: number;
|
|
1325
|
+
byStatus: { status: string; count: number }[];
|
|
1326
|
+
undecided: number;
|
|
1327
|
+
entitiesClaimed: number;
|
|
1328
|
+
entitiesTotal: number;
|
|
1329
|
+
/** How many metadata files the two entity counts were taken over. */
|
|
1330
|
+
metadataFiles: number;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/** One gate's verdict. `ran: false` is NOT a pass — it is "not selected". */
|
|
1334
|
+
interface VerifyGateRow {
|
|
1335
|
+
gate: string;
|
|
1336
|
+
ran: boolean;
|
|
1337
|
+
ok: boolean;
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
/** Project a requirement diagnostic into a payload row. */
|
|
1341
|
+
function toDiagnosticRow(d: Diagnostic, source: "gate" | "lint"): AdvisoryDiagnosticRow {
|
|
1342
|
+
return {
|
|
1343
|
+
code: d.code,
|
|
1344
|
+
// "" rather than omitted: a tabular encoder (TOON) needs every row to carry the
|
|
1345
|
+
// same keys, and an absent column would silently shift the rest.
|
|
1346
|
+
path: d.path ?? "",
|
|
1347
|
+
severity: d.severity,
|
|
1348
|
+
source,
|
|
1349
|
+
message: d.message,
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
/**
|
|
1354
|
+
* What `--format json|toon` puts on stdout.
|
|
1355
|
+
*
|
|
1356
|
+
* The rule this shape is built to keep: **anything not represented here is
|
|
1357
|
+
* STATED, not silently dropped.** A half-payload that looks complete is the very
|
|
1358
|
+
* defect being fixed — an agent read a run carrying 233 advisory findings as "all
|
|
1359
|
+
* green across the board", because the findings were on stderr as text and the
|
|
1360
|
+
* payload mentioned none of them.
|
|
1361
|
+
*
|
|
1362
|
+
* So: every gate's verdict, every advisory finding (uncapped), and an explicit
|
|
1363
|
+
* `notRepresented` list naming what stays on stderr as text.
|
|
1364
|
+
*/
|
|
1365
|
+
function buildVerifyPayload(input: {
|
|
1366
|
+
gates: VerifyGateRow[];
|
|
1367
|
+
exitCode: number;
|
|
1368
|
+
requirements: AdvisorySection<AdvisoryDiagnosticRow>;
|
|
1369
|
+
requirementCounts: RequirementCounts | undefined;
|
|
1370
|
+
antiPatterns: AdvisorySection<AdvisoryFindingRow>;
|
|
1371
|
+
}): Record<string, unknown> {
|
|
1372
|
+
const ran = input.gates.filter((g) => g.ran);
|
|
1373
|
+
const failed = ran.filter((g) => !g.ok);
|
|
1374
|
+
const parts: string[] = [
|
|
1375
|
+
failed.length === 0
|
|
1376
|
+
? `${ran.length} gate(s) ran, all clean`
|
|
1377
|
+
: `${failed.length} of ${ran.length} gate(s) failed (${failed.map((g) => g.gate).join(", ")})`,
|
|
1378
|
+
];
|
|
1379
|
+
if (input.antiPatterns.status === "ran" && input.antiPatterns.total > 0) {
|
|
1380
|
+
parts.push(`${input.antiPatterns.total} advisory anti-pattern finding(s)`);
|
|
1381
|
+
}
|
|
1382
|
+
if (input.requirements.total > 0) {
|
|
1383
|
+
parts.push(`${input.requirements.total} requirement diagnostic(s)`);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
const help: string[] = [];
|
|
1387
|
+
if (failed.length > 0) {
|
|
1388
|
+
help.push(
|
|
1389
|
+
`the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only`,
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
if (input.antiPatterns.total > 0) {
|
|
1393
|
+
help.push(
|
|
1394
|
+
`${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``,
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
if (failed.length === 0 && input.antiPatterns.total === 0 && input.requirements.total === 0) {
|
|
1398
|
+
help.push("no drift and nothing advisory to answer — nothing to do");
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
return {
|
|
1402
|
+
verify: input.gates,
|
|
1403
|
+
exitCode: input.exitCode,
|
|
1404
|
+
summary: parts.join("; "),
|
|
1405
|
+
help,
|
|
1406
|
+
antiPatterns: input.antiPatterns,
|
|
1407
|
+
requirements: input.requirements,
|
|
1408
|
+
...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
|
|
1409
|
+
// The honest boundary. Everything named here is REACHABLE — it is printed as
|
|
1410
|
+
// text on stderr — but it is not in this document, and a reader must not have
|
|
1411
|
+
// to discover that by its absence.
|
|
1412
|
+
notRepresented: [
|
|
1413
|
+
"per-gate drift detail (which template variable drifted, which schema change, which generated file differs, which migration failed to replay) — printed as text on stderr; this payload carries each gate's pass/fail verdict",
|
|
1414
|
+
"the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr",
|
|
1415
|
+
],
|
|
1416
|
+
};
|
|
1417
|
+
}
|