@metaobjectsdev/cli 0.24.4 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +38 -0
- package/dist/src/commands/eject.d.ts.map +1 -0
- package/dist/src/commands/eject.js +233 -0
- package/dist/src/commands/eject.js.map +1 -0
- 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 +15 -1
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +208 -20
- 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 +299 -51
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +105 -4
- 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 +35 -1
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +32 -1
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +4 -13
- package/dist/src/lib/detect-stack.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/dist/src/lib/package-manifest.d.ts +27 -0
- package/dist/src/lib/package-manifest.d.ts.map +1 -0
- package/dist/src/lib/package-manifest.js +52 -0
- package/dist/src/lib/package-manifest.js.map +1 -0
- package/package.json +11 -11
- package/src/commands/docs.ts +194 -8
- package/src/commands/eject.ts +282 -0
- package/src/commands/gen.ts +54 -19
- package/src/commands/init.ts +217 -19
- package/src/commands/types.ts +185 -34
- package/src/commands/verify.ts +358 -47
- package/src/index.ts +114 -5
- package/src/lib/advisory.ts +150 -0
- package/src/lib/anti-patterns.ts +163 -8
- package/src/lib/args.ts +74 -2
- package/src/lib/detect-stack.ts +4 -11
- 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/package-manifest.ts +58 -0
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { join, resolve as resolvePath } from "node:path";
|
|
10
10
|
import { parseVerifyArgs } from "../lib/args.js";
|
|
11
11
|
import { log } from "../lib/log.js";
|
|
12
|
+
import { emitStructured } from "../lib/format.js";
|
|
13
|
+
import { antiPatternRows, ranSection, skippedSection, warnCapped, } from "../lib/advisory.js";
|
|
12
14
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
13
15
|
import { warnIfManifestIgnored } from "../lib/manifest-ignored-check.js";
|
|
14
16
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
@@ -16,11 +18,12 @@ import { FileProvider } from "../lib/file-provider.js";
|
|
|
16
18
|
import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
17
19
|
import { loadMemoryOptionsFrom, loadMetaobjectsConfig, resolveGenCollection, resolveGenConfigDir } from "../lib/load-metaobjects-config.js";
|
|
18
20
|
import { computeCodegenDrift } from "../lib/codegen-drift.js";
|
|
21
|
+
import { computeDocsDrift } from "../lib/docs-drift.js";
|
|
19
22
|
import { checkRequirements, summariseRequirements, scanRequirements, } from "../lib/requirement-check.js";
|
|
20
23
|
import { lintRequirements } from "../lib/requirement-lint.js";
|
|
21
24
|
import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
|
|
22
25
|
import { buildWranglerExecuteArgs, defaultWranglerRunner, isWranglerLocalD1StatePath, } from "../lib/wrangler.js";
|
|
23
|
-
import { buildProjectionViews } from "@metaobjectsdev/codegen-ts";
|
|
26
|
+
import { buildProjectionViews, resolveDocsConfig } from "@metaobjectsdev/codegen-ts";
|
|
24
27
|
import { buildKyselyFromUrl, inferDialect } from "../lib/kysely.js";
|
|
25
28
|
import { tokensToAllowOptions, describeChange } from "../lib/allow.js";
|
|
26
29
|
import { computeDrift, computeDriftFromActual, collectUnmanagedNames, excludeFromSnapshot, scopedDiffInputs, scopeExpectedSchema, buildExpectedSchemaWithProvenance, applyPending, openReplayEngine, verifyReplay, introspect, diff, readSnapshot, snapshotPath, introspectD1, findWranglerConfig, parseWranglerConfig, resolveD1Binding, } from "@metaobjectsdev/migrate-ts";
|
|
@@ -60,14 +63,33 @@ function attrAsStringArray(attr) {
|
|
|
60
63
|
}
|
|
61
64
|
export async function verifyCommand(args, cwd,
|
|
62
65
|
/** Injectable wrangler runner (D1 path only) — tests pass a mock; production uses the default. */
|
|
63
|
-
wranglerRunner
|
|
66
|
+
wranglerRunner,
|
|
67
|
+
/**
|
|
68
|
+
* Output format, threaded from the global `--format` flag (4th position, matching
|
|
69
|
+
* `migrateCommand`). It used to be passed to `gen` and `migrate` ONLY, so
|
|
70
|
+
* `meta verify --format json` was accepted, validated, exited 0 — and printed
|
|
71
|
+
* human text. An adopter who guessed the right flag lost silently.
|
|
72
|
+
*/
|
|
73
|
+
fmt = "text") {
|
|
64
74
|
const activeWranglerRunner = wranglerRunner ?? defaultWranglerRunner;
|
|
75
|
+
// In a structured format stdout carries exactly ONE document, so every narration
|
|
76
|
+
// line moves to stderr (`say` below) instead of being printed into the payload or
|
|
77
|
+
// dropped. Same split `meta migrate` makes for its out-of-scope note.
|
|
78
|
+
const structured = fmt !== "text";
|
|
79
|
+
const say = (msg) => { if (structured)
|
|
80
|
+
log.warn(msg);
|
|
81
|
+
else
|
|
82
|
+
log.info(msg); };
|
|
65
83
|
let flags;
|
|
66
84
|
try {
|
|
67
85
|
flags = parseVerifyArgs(args);
|
|
68
86
|
}
|
|
69
87
|
catch (err) {
|
|
70
|
-
|
|
88
|
+
const msg = err.message;
|
|
89
|
+
log.error(msg);
|
|
90
|
+
// A structured caller gets a structured refusal. Exiting 2 with an EMPTY stdout
|
|
91
|
+
// is the same silence this command is being fixed for, one level up.
|
|
92
|
+
emitStructured({ error: msg, hint: "run `meta verify --help` for the accepted flags" }, fmt);
|
|
71
93
|
return 2;
|
|
72
94
|
}
|
|
73
95
|
// ADR-0021 D2 — explicit verify subverbs. Each flag selects one drift mode;
|
|
@@ -79,9 +101,11 @@ wranglerRunner) {
|
|
|
79
101
|
// The schema gate is selected by the presence of --db, or --dialect d1 (#225 —
|
|
80
102
|
// D1 has no URL connection); that check lives inside runSchemaVerify.
|
|
81
103
|
const runCodegen = flags.codegen;
|
|
104
|
+
const runDocs = flags.docs;
|
|
82
105
|
if (!flags.anyExplicit) {
|
|
83
|
-
|
|
106
|
+
say("meta verify — running --templates (default). Explicit subverbs: " +
|
|
84
107
|
"--templates (prompt drift), --db/--dialect d1 (schema drift), --codegen (codegen drift), " +
|
|
108
|
+
"--docs (docs drift), " +
|
|
85
109
|
"--replay/--replay-snapshot (the committed migration chain replays from empty).");
|
|
86
110
|
}
|
|
87
111
|
// Where the metadata lives is `resolveCollection`'s decision, not a hardcoded
|
|
@@ -98,7 +122,9 @@ wranglerRunner) {
|
|
|
98
122
|
collection = await resolveCollection(cwd);
|
|
99
123
|
}
|
|
100
124
|
catch (err) {
|
|
101
|
-
|
|
125
|
+
const msg = err.message;
|
|
126
|
+
log.error(msg);
|
|
127
|
+
emitStructured({ error: msg, hint: "declare metadata `sources` in .metaobjects/config.json, or run `meta init`" }, fmt);
|
|
102
128
|
return 2;
|
|
103
129
|
}
|
|
104
130
|
// The project root is whichever directory `resolveCollection` decided the
|
|
@@ -163,15 +189,40 @@ wranglerRunner) {
|
|
|
163
189
|
catch (err) {
|
|
164
190
|
const msg = err.message;
|
|
165
191
|
log.error(`failed to load metadata: ${msg}`);
|
|
166
|
-
// Strict-load rejection (ADR-0023)
|
|
167
|
-
//
|
|
192
|
+
// Strict-load rejection (ADR-0023). Two different failures reach here and they need
|
|
193
|
+
// different advice:
|
|
194
|
+
//
|
|
195
|
+
// a TYPO'd or genuinely undeclared attr — the three exits below are right;
|
|
196
|
+
// a RETIRED attr — the loader already knows the exits and attaches them as
|
|
197
|
+
// ADR-0009 `suggestions[]`, so we print those and say nothing of our own.
|
|
198
|
+
//
|
|
199
|
+
// The distinction is not cosmetic. The middle generic exit — the `attr.properties`
|
|
200
|
+
// bag — is EXEMPT from the strict-attr check by subtype, so it loads. Offer it for a
|
|
201
|
+
// retired attribute and the author gets a green `meta verify` over a value that now
|
|
202
|
+
// reaches nothing: a correct, loud failure converted into a quiet, wrong pass. That is
|
|
203
|
+
// strictly worse than the error it replaced.
|
|
168
204
|
const code = err.code;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
205
|
+
const suggestions = err.suggestions;
|
|
206
|
+
const strictHint = "meta verify is strict (ADR-0023): every authored @attr must be declared. " +
|
|
207
|
+
"Fix: register the attr on a metadata provider, OR move arbitrary " +
|
|
208
|
+
"author-supplied properties into an `attr.properties` bag, OR re-run " +
|
|
209
|
+
"with `meta verify --lax` to keep the legacy open-attr load.";
|
|
210
|
+
const isStrictAttr = code === ERR_UNKNOWN_ATTR || msg.includes("Unknown attribute");
|
|
211
|
+
if (suggestions !== undefined && suggestions.length > 0) {
|
|
212
|
+
for (const s of suggestions)
|
|
213
|
+
log.error(` ${s}`);
|
|
214
|
+
}
|
|
215
|
+
else if (isStrictAttr) {
|
|
216
|
+
log.error(strictHint);
|
|
174
217
|
}
|
|
218
|
+
// The same refusal, machine-readable. No gate ran, so there is no verdict to
|
|
219
|
+
// report — only why there is none.
|
|
220
|
+
emitStructured({
|
|
221
|
+
error: `failed to load metadata: ${msg}`,
|
|
222
|
+
hint: suggestions !== undefined && suggestions.length > 0
|
|
223
|
+
? suggestions.join(" ")
|
|
224
|
+
: isStrictAttr ? strictHint : "fix the metadata error above and re-run",
|
|
225
|
+
}, fmt);
|
|
175
226
|
return 1;
|
|
176
227
|
}
|
|
177
228
|
// The schema gate governs exactly the objects `meta migrate` governs — ONE
|
|
@@ -181,12 +232,29 @@ wranglerRunner) {
|
|
|
181
232
|
const schemaScope = collection.inMigrateScope;
|
|
182
233
|
const promptsDir = join(projectRoot, flags.prompts ?? DEFAULT_PROMPTS_DIR);
|
|
183
234
|
const provider = new FileProvider(promptsDir);
|
|
235
|
+
// The two advisory sections, captured as the gates run so the structured payload
|
|
236
|
+
// can carry them IN FULL. They were previously formatted straight to stderr and
|
|
237
|
+
// existed nowhere else, which is why 96% of a 239-finding report was unreachable
|
|
238
|
+
// by any flag, env var or format.
|
|
239
|
+
let requirementSection = skippedSection("the requirement pass did not run");
|
|
240
|
+
let antiPatternSection = skippedSection("the advisory anti-pattern pass did not run");
|
|
241
|
+
// The ledger counts `meta verify` prints on every run. Undefined for a project
|
|
242
|
+
// declaring no requirement.* node at all (opt-in by declaration) — the payload
|
|
243
|
+
// then omits the block rather than reporting zeroes that would read as an empty
|
|
244
|
+
// ledger instead of no ledger.
|
|
245
|
+
let requirementCounts;
|
|
246
|
+
// Whether the schema gate is selected. `runSchemaVerify` makes this decision
|
|
247
|
+
// internally (it is selected by --db or --dialect d1, not by a subverb flag);
|
|
248
|
+
// naming it here lets the structured payload report "ran" from the SAME
|
|
249
|
+
// expression rather than a second guess at it.
|
|
250
|
+
const ranSchemaGate = (flags.db !== undefined || flags.dialect === "d1") && !flags.skipSchema;
|
|
184
251
|
// Exit-code composition: the overall result is the MAX across every selected
|
|
185
252
|
// subverb so ANY kind of drift fails CI. Each gate only runs when its mode is
|
|
186
253
|
// selected; an unselected gate contributes 0.
|
|
187
254
|
const templateExit = runTemplates ? runTemplateVerify() : 0;
|
|
188
255
|
const schemaExit = await runSchemaVerify();
|
|
189
256
|
const codegenExit = runCodegen ? await runCodegenVerify() : 0;
|
|
257
|
+
const docsExit = runDocs ? await runDocsVerify() : 0;
|
|
190
258
|
// Requirements have no subverb: `requirement.*` nodes are metadata, so they
|
|
191
259
|
// are checked on every `meta verify`. Opt-in by DECLARATION — a model with no
|
|
192
260
|
// requirement nodes is silent, not in drift.
|
|
@@ -200,9 +268,29 @@ wranglerRunner) {
|
|
|
200
268
|
// model. Warnings ONLY — never changes the exit code (bias to under-flagging).
|
|
201
269
|
// Suppressed with --no-antipatterns or META_NO_ANTIPATTERNS=1 for the rare
|
|
202
270
|
// noisy project (both opt-outs work on `meta verify` and `meta gen`).
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
271
|
+
runAntiPatternAdvisory();
|
|
272
|
+
const exitCode = Math.max(templateExit, schemaExit, codegenExit, docsExit, requirementExit, replayExit);
|
|
273
|
+
if (structured) {
|
|
274
|
+
emitStructured(buildVerifyPayload({
|
|
275
|
+
gates: [
|
|
276
|
+
{ gate: "templates", ran: runTemplates, ok: templateExit === 0 },
|
|
277
|
+
// The schema gate decides internally whether it is selected (--db, or
|
|
278
|
+
// --dialect d1), so "ran" is read off the same signals rather than
|
|
279
|
+
// restated: a payload that claims a gate ran when it did not is the
|
|
280
|
+
// failure mode this whole change exists to remove.
|
|
281
|
+
{ gate: "schema", ran: ranSchemaGate, ok: schemaExit === 0 },
|
|
282
|
+
{ gate: "codegen", ran: runCodegen, ok: codegenExit === 0 },
|
|
283
|
+
{ gate: "docs", ran: runDocs, ok: docsExit === 0 },
|
|
284
|
+
{ gate: "requirements", ran: true, ok: requirementExit === 0 },
|
|
285
|
+
{ gate: "replay", ran: flags.replay || flags.replaySnapshot, ok: replayExit === 0 },
|
|
286
|
+
],
|
|
287
|
+
exitCode,
|
|
288
|
+
requirements: requirementSection,
|
|
289
|
+
requirementCounts,
|
|
290
|
+
antiPatterns: antiPatternSection,
|
|
291
|
+
}), fmt);
|
|
292
|
+
}
|
|
293
|
+
return exitCode;
|
|
206
294
|
// -- replay (#313) ---------------------------------------------------------
|
|
207
295
|
/**
|
|
208
296
|
* Replay the committed migration chain into an EMPTY throwaway database and assert
|
|
@@ -278,12 +366,12 @@ wranglerRunner) {
|
|
|
278
366
|
// empty replay is still compared against a snapshot that may record dozens of
|
|
279
367
|
// tables, rather than reporting success having compared nothing.
|
|
280
368
|
if (applied.pending.length === 0) {
|
|
281
|
-
|
|
369
|
+
say(`meta verify --replay: no committed migrations — nothing to replay`);
|
|
282
370
|
if (!flags.replaySnapshot)
|
|
283
371
|
return 0;
|
|
284
372
|
}
|
|
285
373
|
else {
|
|
286
|
-
|
|
374
|
+
say(`meta verify --replay — the committed chain applies to an empty ${dialect} database ` +
|
|
287
375
|
`(${applied.applied.length} migration(s)).`);
|
|
288
376
|
}
|
|
289
377
|
if (!flags.replaySnapshot)
|
|
@@ -318,11 +406,11 @@ wranglerRunner) {
|
|
|
318
406
|
snapshot = await readSnapshot(snapshotPath(dir, dialect));
|
|
319
407
|
}
|
|
320
408
|
catch {
|
|
321
|
-
|
|
409
|
+
say(`meta verify --replay-snapshot: the committed snapshot could not be read — nothing to compare`);
|
|
322
410
|
return 0;
|
|
323
411
|
}
|
|
324
412
|
if (snapshot === null) {
|
|
325
|
-
|
|
413
|
+
say(`meta verify --replay-snapshot: no committed snapshot — nothing to compare`);
|
|
326
414
|
return 0;
|
|
327
415
|
}
|
|
328
416
|
// A scoped project carries the OTHER owner's tables into its snapshot on purpose
|
|
@@ -354,7 +442,7 @@ wranglerRunner) {
|
|
|
354
442
|
...(governed !== undefined ? { governed } : {}),
|
|
355
443
|
});
|
|
356
444
|
if (result.ok) {
|
|
357
|
-
|
|
445
|
+
say(`meta verify --replay-snapshot — the replayed chain reproduces the committed snapshot.`);
|
|
358
446
|
return 0;
|
|
359
447
|
}
|
|
360
448
|
log.error(`meta verify --replay-snapshot: the replayed chain does not reproduce the committed snapshot. ` +
|
|
@@ -396,31 +484,45 @@ wranglerRunner) {
|
|
|
396
484
|
// all, which is why nothing had ever flagged the templates living in them. No
|
|
397
485
|
// check can see a tree it was never pointed at, so the honest fix is to publish
|
|
398
486
|
// what the count was taken over and let a wrong number be noticeable.
|
|
399
|
-
|
|
487
|
+
say(`meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
|
|
400
488
|
`${s.architectural} architectural) — ${parts.join(", ")}; ` +
|
|
401
489
|
`${s.entitiesClaimed}/${s.entitiesTotal} entities claimed, ` +
|
|
402
490
|
`counted over ${collection.files.length} metadata file(s).`);
|
|
403
491
|
if (s.undecided > 0) {
|
|
404
|
-
|
|
492
|
+
say(`meta verify — requirements: ${s.undecided} recorded gap(s) with no @disposition. ` +
|
|
405
493
|
`These are known problems nobody has ruled on — set 'accepted' or 'deferred' to close the question.`);
|
|
406
494
|
}
|
|
495
|
+
// The same counts, machine-readable. `metadataFiles` travels with them for the
|
|
496
|
+
// reason the text line carries it: the claimed/total ratio is only ever taken
|
|
497
|
+
// over what actually loaded, so a reader needs the denominator's provenance to
|
|
498
|
+
// notice a spine that covers half an estate.
|
|
499
|
+
requirementCounts = {
|
|
500
|
+
total: s.total,
|
|
501
|
+
functional: s.functional,
|
|
502
|
+
architectural: s.architectural,
|
|
503
|
+
byStatus: order
|
|
504
|
+
.filter((k) => (s.byStatus[k] ?? 0) > 0)
|
|
505
|
+
.map((k) => ({ status: k, count: s.byStatus[k] ?? 0 })),
|
|
506
|
+
undecided: s.undecided,
|
|
507
|
+
entitiesClaimed: s.entitiesClaimed,
|
|
508
|
+
entitiesTotal: s.entitiesTotal,
|
|
509
|
+
metadataFiles: collection.files.length,
|
|
510
|
+
};
|
|
407
511
|
}
|
|
408
512
|
const errors = diags.filter((d) => d.severity === "error");
|
|
409
513
|
const warns = diags.filter((d) => d.severity === "warn");
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
log.warn(` …and ${ds.length - CAP} more.`);
|
|
420
|
-
};
|
|
514
|
+
// Named `fmtDiag`, not `fmt`: `fmt` is this command's OUTPUT FORMAT parameter,
|
|
515
|
+
// and a shadow of it inside the one function that must not confuse the two is
|
|
516
|
+
// how a structured run quietly reverts to text.
|
|
517
|
+
const fmtDiag = (d) => ` ${d.code}${d.path !== undefined ? ` [${d.path}]` : ""}: ${d.message}`;
|
|
518
|
+
// Capped per SECTION, never across them: a ledger of a few hundred entries can
|
|
519
|
+
// produce hundreds of prose findings, and a shared budget would let the advisory
|
|
520
|
+
// lint push every gate warning off the end. The cap VALUE is now one shared
|
|
521
|
+
// constant (`--limit`), so raising it cannot miss a section — errors stay
|
|
522
|
+
// uncapped, as they always were.
|
|
421
523
|
for (const d of errors)
|
|
422
|
-
log.error(
|
|
423
|
-
warnCapped(warns);
|
|
524
|
+
log.error(fmtDiag(d));
|
|
525
|
+
warnCapped(warns.map(fmtDiag), flags.limit, { structured });
|
|
424
526
|
// -- the authoring lint: its own section, its own cap ----------------------
|
|
425
527
|
// Separate from the gate above because it makes a different claim. The gate
|
|
426
528
|
// says the ledger DISAGREES WITH THE MODEL; the lint says it agrees but
|
|
@@ -446,8 +548,15 @@ wranglerRunner) {
|
|
|
446
548
|
if (lint.length > 0) {
|
|
447
549
|
log.warn(`meta verify — requirements: ${lint.length} authoring warning(s) ` +
|
|
448
550
|
`(advisory — does not fail the build):`);
|
|
449
|
-
warnCapped(lint);
|
|
551
|
+
warnCapped(lint.map(fmtDiag), flags.limit, { structured });
|
|
450
552
|
}
|
|
553
|
+
// Everything this pass found, uncapped, for the structured payload — gate
|
|
554
|
+
// diagnostics and lint findings in one list, each row saying which it is, so a
|
|
555
|
+
// reader can tell what can fail a build from what never can.
|
|
556
|
+
requirementSection = ranSection([
|
|
557
|
+
...diags.map((d) => toDiagnosticRow(d, "gate")),
|
|
558
|
+
...lint.map((d) => toDiagnosticRow(d, "lint")),
|
|
559
|
+
]);
|
|
451
560
|
if (errors.length > 0) {
|
|
452
561
|
log.error(`meta verify — requirements: ${errors.length} error(s).`);
|
|
453
562
|
return 1;
|
|
@@ -455,30 +564,41 @@ wranglerRunner) {
|
|
|
455
564
|
return 0;
|
|
456
565
|
}
|
|
457
566
|
// -- verify-as-teacher (advisory) ------------------------------------------
|
|
567
|
+
// Records its result EITHER WAY — a skip carries its reason rather than looking
|
|
568
|
+
// like a clean scan. Warnings only; nothing here reaches the exit code (the
|
|
569
|
+
// scanner's own header: bias to under-flagging, never a non-zero exit).
|
|
458
570
|
function runAntiPatternAdvisory() {
|
|
571
|
+
if (flags.noAntipatterns) {
|
|
572
|
+
antiPatternSection = skippedSection("suppressed by --no-antipatterns");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (process.env.META_NO_ANTIPATTERNS === "1") {
|
|
576
|
+
antiPatternSection = skippedSection("suppressed by META_NO_ANTIPATTERNS=1");
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
459
579
|
let findings;
|
|
460
580
|
try {
|
|
461
|
-
|
|
581
|
+
const ignore = forgeConfig?.verify?.antiPatternIgnore;
|
|
582
|
+
findings = scanSourceForAntiPatterns(projectRoot, ignore !== undefined ? { ignore } : undefined);
|
|
462
583
|
}
|
|
463
|
-
catch {
|
|
464
|
-
|
|
584
|
+
catch (err) {
|
|
585
|
+
// Never let an advisory scan break verify — and never report it as clean.
|
|
586
|
+
antiPatternSection = skippedSection(`the scan failed: ${err.message}`);
|
|
587
|
+
return;
|
|
465
588
|
}
|
|
589
|
+
antiPatternSection = ranSection(antiPatternRows(findings));
|
|
466
590
|
if (findings.length === 0)
|
|
467
591
|
return;
|
|
468
|
-
const CAP = 10;
|
|
469
592
|
log.warn(`meta verify — ${findings.length} place(s) hand-roll what MetaObjects can model ` +
|
|
470
593
|
`(advisory — does not fail the build):`);
|
|
471
|
-
|
|
472
|
-
log.warn(` ${f.message}`);
|
|
473
|
-
if (findings.length > CAP)
|
|
474
|
-
log.warn(` …and ${findings.length - CAP} more.`);
|
|
594
|
+
warnCapped(findings.map((f) => ` ${f.message}`), flags.limit, { structured });
|
|
475
595
|
}
|
|
476
596
|
// -- template (prompt / output) drift --------------------------------------
|
|
477
597
|
function runTemplateVerify() {
|
|
478
598
|
// ADR-0039: effective children — resolve rather than rely on root being unextended.
|
|
479
599
|
const templates = root.children().filter((c) => c.type === TYPE_TEMPLATE);
|
|
480
600
|
if (templates.length === 0) {
|
|
481
|
-
|
|
601
|
+
say("meta verify — no template.* nodes found; nothing to check.");
|
|
482
602
|
return 0;
|
|
483
603
|
}
|
|
484
604
|
let errorCount = 0;
|
|
@@ -589,7 +709,7 @@ wranglerRunner) {
|
|
|
589
709
|
log.error(`meta verify — ${errorCount} drift error(s) across ${checkedTemplates} template(s).`);
|
|
590
710
|
return 1;
|
|
591
711
|
}
|
|
592
|
-
|
|
712
|
+
say(`meta verify — ${checkedTemplates} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`);
|
|
593
713
|
return 0;
|
|
594
714
|
}
|
|
595
715
|
// -- schema drift (live DB) ------------------------------------------------
|
|
@@ -833,9 +953,16 @@ wranglerRunner) {
|
|
|
833
953
|
if (result.changes.length === 0)
|
|
834
954
|
return [];
|
|
835
955
|
return [
|
|
956
|
+
// `meta migrate --from-db` is NOT the repair: it writes a snapshot only when it has
|
|
957
|
+
// changes to EMIT, so on a database that already matches the metadata it reports
|
|
958
|
+
// "no schema changes / nothing to do", writes nothing, and leaves the stale snapshot
|
|
959
|
+
// exactly as it was — so this gate fails again, identically, with the user having
|
|
960
|
+
// been told everything is in sync. `baseline --from-db` rewrites it unconditionally,
|
|
961
|
+
// which is the whole point of the subcommand.
|
|
836
962
|
`the committed schema snapshot disagrees with ${displayUrl} ` +
|
|
837
963
|
`(${result.changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
|
|
838
|
-
`and fail at apply. Re-derive it with
|
|
964
|
+
`and fail at apply. Re-derive it with ` +
|
|
965
|
+
`'meta migrate baseline --from-db --db <url> --dialect ${dialect}'.`,
|
|
839
966
|
...summarizeDrift(result.changes),
|
|
840
967
|
];
|
|
841
968
|
}
|
|
@@ -845,17 +972,17 @@ wranglerRunner) {
|
|
|
845
972
|
// annotate them as external (declared) rather than let them vanish silently.
|
|
846
973
|
const externalDeclared = collectUnmanagedNames(root);
|
|
847
974
|
if (externalDeclared.length > 0) {
|
|
848
|
-
|
|
975
|
+
say(`meta verify — ${externalDeclared.length} object(s) external (declared @unmanaged, managed elsewhere): ${externalDeclared.join(", ")}`);
|
|
849
976
|
}
|
|
850
977
|
// Same reasoning for the per-command scope: an object `migrate.scope` excluded
|
|
851
978
|
// was NOT checked, and silence would misreport it as checked-and-clean. Shared
|
|
852
979
|
// wording with `meta migrate` — one declaration, one sentence about it.
|
|
853
980
|
if (driftResult.outOfScope.length > 0) {
|
|
854
|
-
|
|
981
|
+
say(outOfScopeNote("verify", driftResult.outOfScope));
|
|
855
982
|
}
|
|
856
983
|
const changes = driftResult.changes;
|
|
857
984
|
if (changes.length === 0 && ledgerDrift.length === 0) {
|
|
858
|
-
|
|
985
|
+
say(`meta verify — schema in sync with ${displayUrl}.`);
|
|
859
986
|
return 0;
|
|
860
987
|
}
|
|
861
988
|
// The header is conditional: #292's snapshot findings arrive through `ledgerDrift`
|
|
@@ -907,6 +1034,11 @@ wranglerRunner) {
|
|
|
907
1034
|
}
|
|
908
1035
|
catch (err) {
|
|
909
1036
|
log.error(`verify --codegen: failed to load this package's metadata: ${err.message}`);
|
|
1037
|
+
// The second door onto the same strict load. It carried no remedy at all, which
|
|
1038
|
+
// meant a retirement diagnosed here told the author what broke and nothing about
|
|
1039
|
+
// how to fix it — the half-true rule this file's sibling comment warns about.
|
|
1040
|
+
for (const s of err.suggestions ?? [])
|
|
1041
|
+
log.error(` ${s}`);
|
|
910
1042
|
return 2;
|
|
911
1043
|
}
|
|
912
1044
|
}
|
|
@@ -923,7 +1055,7 @@ wranglerRunner) {
|
|
|
923
1055
|
return 2;
|
|
924
1056
|
}
|
|
925
1057
|
if (result.clean) {
|
|
926
|
-
|
|
1058
|
+
say("meta verify — generated output is in sync with the metadata (no codegen drift).");
|
|
927
1059
|
return 0;
|
|
928
1060
|
}
|
|
929
1061
|
log.error(`meta verify — codegen drift (${result.driftedFiles.length} file(s) differ from a fresh regen):`);
|
|
@@ -932,6 +1064,57 @@ wranglerRunner) {
|
|
|
932
1064
|
log.error("Run 'meta gen' to regenerate, then commit the result.");
|
|
933
1065
|
return 1;
|
|
934
1066
|
}
|
|
1067
|
+
// -- docs drift -------------------------------------------------------------
|
|
1068
|
+
// Gated on --docs. Runs `meta docs` into a temp dir and diffs the committed docs
|
|
1069
|
+
// tree. See lib/docs-drift.ts for the two deliberate differences from --codegen
|
|
1070
|
+
// (a byte difference IS drift here, and a committed file a regen would not emit is
|
|
1071
|
+
// never reported — `docs.outDir` is full of files MetaObjects did not write).
|
|
1072
|
+
//
|
|
1073
|
+
// It needs a config for the same reason `--codegen` does: `docs.outDir` says which
|
|
1074
|
+
// tree to compare against, and the `agent` surface will not even materialise
|
|
1075
|
+
// without one.
|
|
1076
|
+
async function runDocsVerify() {
|
|
1077
|
+
if (forgeConfig === undefined) {
|
|
1078
|
+
log.error("verify --docs: no metaobjects.config.ts found (or it is invalid) — " +
|
|
1079
|
+
"cannot locate the committed docs tree to diff against. " +
|
|
1080
|
+
"Run 'meta init' to scaffold one, or run without --docs.");
|
|
1081
|
+
return 2;
|
|
1082
|
+
}
|
|
1083
|
+
// Resolved through the SAME resolver `meta docs` uses, so a project that moved its
|
|
1084
|
+
// docs elsewhere is checked where its docs actually are. `outputLayout` is the
|
|
1085
|
+
// documented fallback for `docs.layout`.
|
|
1086
|
+
const docsCfg = resolveDocsConfig(forgeConfig.docs, {}, forgeConfig.outputLayout ?? "flat");
|
|
1087
|
+
let result;
|
|
1088
|
+
try {
|
|
1089
|
+
result = await computeDocsDrift({
|
|
1090
|
+
projectRoot: genConfigDir,
|
|
1091
|
+
docsDir: resolvePath(genConfigDir, docsCfg.outDir),
|
|
1092
|
+
cwd: genConfigDir,
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
catch (err) {
|
|
1096
|
+
log.error(`verify --docs: regeneration failed: ${err.message}`);
|
|
1097
|
+
return 1;
|
|
1098
|
+
}
|
|
1099
|
+
if (result.error !== undefined) {
|
|
1100
|
+
log.error(result.error);
|
|
1101
|
+
return 2;
|
|
1102
|
+
}
|
|
1103
|
+
if (result.clean) {
|
|
1104
|
+
// The denominator is what was actually COMPARED, so the passing line and the
|
|
1105
|
+
// failing line below count the same set. A gate whose two halves divide by
|
|
1106
|
+
// different numbers is how `verify --templates` came to report seven templates
|
|
1107
|
+
// vanishing between a red run and a green one.
|
|
1108
|
+
say(`meta verify — ${result.checked} docs page(s) match a fresh 'meta docs' (no docs drift).`);
|
|
1109
|
+
return 0;
|
|
1110
|
+
}
|
|
1111
|
+
log.error(`meta verify — docs drift (${result.driftedFiles.length} of ${result.checked} page(s) ` +
|
|
1112
|
+
`differ from a fresh 'meta docs'):`);
|
|
1113
|
+
for (const line of result.lines)
|
|
1114
|
+
log.error(` ${line}`);
|
|
1115
|
+
log.error("Run 'meta docs' to regenerate, then commit the result.");
|
|
1116
|
+
return 1;
|
|
1117
|
+
}
|
|
935
1118
|
}
|
|
936
1119
|
/**
|
|
937
1120
|
* Migration-history ledger reconciliation hook (Unit 3 — not yet built).
|
|
@@ -975,4 +1158,69 @@ function summarizeDrift(changes) {
|
|
|
975
1158
|
return `${p.glyph} ${p.noun} ${describeChange(c)}`;
|
|
976
1159
|
});
|
|
977
1160
|
}
|
|
1161
|
+
/** Project a requirement diagnostic into a payload row. */
|
|
1162
|
+
function toDiagnosticRow(d, source) {
|
|
1163
|
+
return {
|
|
1164
|
+
code: d.code,
|
|
1165
|
+
// "" rather than omitted: a tabular encoder (TOON) needs every row to carry the
|
|
1166
|
+
// same keys, and an absent column would silently shift the rest.
|
|
1167
|
+
path: d.path ?? "",
|
|
1168
|
+
severity: d.severity,
|
|
1169
|
+
source,
|
|
1170
|
+
message: d.message,
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
/**
|
|
1174
|
+
* What `--format json|toon` puts on stdout.
|
|
1175
|
+
*
|
|
1176
|
+
* The rule this shape is built to keep: **anything not represented here is
|
|
1177
|
+
* STATED, not silently dropped.** A half-payload that looks complete is the very
|
|
1178
|
+
* defect being fixed — an agent read a run carrying 233 advisory findings as "all
|
|
1179
|
+
* green across the board", because the findings were on stderr as text and the
|
|
1180
|
+
* payload mentioned none of them.
|
|
1181
|
+
*
|
|
1182
|
+
* So: every gate's verdict, every advisory finding (uncapped), and an explicit
|
|
1183
|
+
* `notRepresented` list naming what stays on stderr as text.
|
|
1184
|
+
*/
|
|
1185
|
+
function buildVerifyPayload(input) {
|
|
1186
|
+
const ran = input.gates.filter((g) => g.ran);
|
|
1187
|
+
const failed = ran.filter((g) => !g.ok);
|
|
1188
|
+
const parts = [
|
|
1189
|
+
failed.length === 0
|
|
1190
|
+
? `${ran.length} gate(s) ran, all clean`
|
|
1191
|
+
: `${failed.length} of ${ran.length} gate(s) failed (${failed.map((g) => g.gate).join(", ")})`,
|
|
1192
|
+
];
|
|
1193
|
+
if (input.antiPatterns.status === "ran" && input.antiPatterns.total > 0) {
|
|
1194
|
+
parts.push(`${input.antiPatterns.total} advisory anti-pattern finding(s)`);
|
|
1195
|
+
}
|
|
1196
|
+
if (input.requirements.total > 0) {
|
|
1197
|
+
parts.push(`${input.requirements.total} requirement diagnostic(s)`);
|
|
1198
|
+
}
|
|
1199
|
+
const help = [];
|
|
1200
|
+
if (failed.length > 0) {
|
|
1201
|
+
help.push(`the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only`);
|
|
1202
|
+
}
|
|
1203
|
+
if (input.antiPatterns.total > 0) {
|
|
1204
|
+
help.push(`${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``);
|
|
1205
|
+
}
|
|
1206
|
+
if (failed.length === 0 && input.antiPatterns.total === 0 && input.requirements.total === 0) {
|
|
1207
|
+
help.push("no drift and nothing advisory to answer — nothing to do");
|
|
1208
|
+
}
|
|
1209
|
+
return {
|
|
1210
|
+
verify: input.gates,
|
|
1211
|
+
exitCode: input.exitCode,
|
|
1212
|
+
summary: parts.join("; "),
|
|
1213
|
+
help,
|
|
1214
|
+
antiPatterns: input.antiPatterns,
|
|
1215
|
+
requirements: input.requirements,
|
|
1216
|
+
...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
|
|
1217
|
+
// The honest boundary. Everything named here is REACHABLE — it is printed as
|
|
1218
|
+
// text on stderr — but it is not in this document, and a reader must not have
|
|
1219
|
+
// to discover that by its absence.
|
|
1220
|
+
notRepresented: [
|
|
1221
|
+
"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",
|
|
1222
|
+
"the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr",
|
|
1223
|
+
],
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
978
1226
|
//# sourceMappingURL=verify.js.map
|