@metaobjectsdev/cli 0.24.5 → 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +309 -54
- 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/load-metaobjects-config.d.ts +1 -0
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +47 -0
- package/dist/src/lib/load-metaobjects-config.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 +11 -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 +368 -50
- 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/load-metaobjects-config.ts +51 -0
- package/src/lib/output.ts +33 -2
|
@@ -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;
|
|
@@ -491,9 +611,19 @@ wranglerRunner) {
|
|
|
491
611
|
// "11 drift error(s) across 29 template(s)" while failing and "22 template(s)
|
|
492
612
|
// clean" once fixed: seven templates apparently vanishing on the way to green,
|
|
493
613
|
// and a failure line claiming a denominator of work that had not been done.
|
|
494
|
-
// `checkedTemplates` is
|
|
495
|
-
//
|
|
496
|
-
// counts once; the line says "template(s)", so it counts
|
|
614
|
+
// `checkedTemplates` is the single unit — templates this check ENGAGED with: one
|
|
615
|
+
// whose body verified, or one that produced an error. (An @kind=email template has
|
|
616
|
+
// up to three bodies and still counts once; the line says "template(s)", so it counts
|
|
617
|
+
// templates.)
|
|
618
|
+
//
|
|
619
|
+
// "Engaged with" rather than "verified" because an error is not always reached
|
|
620
|
+
// THROUGH a verified body: an unresolvable @payloadRef, or a body ref that resolves
|
|
621
|
+
// to nothing, errors and verifies nothing. Counting only verified bodies made the
|
|
622
|
+
// failure line divide by the population that SUCCEEDED, so a run where every template
|
|
623
|
+
// failed to resolve its body read "22 drift error(s) across 0 template(s)" — an
|
|
624
|
+
// errors-with-no-population line that says nothing was checked while naming 22
|
|
625
|
+
// failures. Found running this gate against a real adopter estate. The clean line is
|
|
626
|
+
// unaffected: on a passing run no template errors, so engaged == verified.
|
|
497
627
|
let checkedTemplates = 0;
|
|
498
628
|
for (const tmpl of templates) {
|
|
499
629
|
// ADR-0039: effective attrs — @payloadRef may be inherited via an abstract template.
|
|
@@ -510,6 +640,7 @@ wranglerRunner) {
|
|
|
510
640
|
log.error(`[${tmpl.name}] (${tmpl.subType}) ${ERR_PARTIAL_UNRESOLVED}: ` +
|
|
511
641
|
`@payloadRef "${payloadRef}" did not resolve to a loaded object.value`);
|
|
512
642
|
errorCount++;
|
|
643
|
+
checkedTemplates++;
|
|
513
644
|
continue;
|
|
514
645
|
}
|
|
515
646
|
// Collect every renderable mustache ref for this template + whether the
|
|
@@ -561,12 +692,14 @@ wranglerRunner) {
|
|
|
561
692
|
const requiredSlots = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_SLOTS)) : [];
|
|
562
693
|
const requiredTags = promptRules ? attrAsStringArray(tmpl.attr(TEMPLATE_ATTR_REQUIRED_TAGS)) : [];
|
|
563
694
|
let anyBodyChecked = false;
|
|
695
|
+
let anyErrorHere = false;
|
|
564
696
|
for (const { label, ref } of refs) {
|
|
565
697
|
// Render-engine drift check: mustache variables ↔ payload field names.
|
|
566
698
|
const text = provider.resolve(ref);
|
|
567
699
|
if (text === undefined) {
|
|
568
700
|
log.error(`[${tmpl.name}] (${label}) ${ERR_PARTIAL_UNRESOLVED}: ref "${ref}" did not resolve under ${promptsDir}`);
|
|
569
701
|
errorCount++;
|
|
702
|
+
anyErrorHere = true;
|
|
570
703
|
continue;
|
|
571
704
|
}
|
|
572
705
|
const drift = verify(text, fieldTree, { provider, requiredSlots, requiredTags });
|
|
@@ -579,17 +712,18 @@ wranglerRunner) {
|
|
|
579
712
|
else {
|
|
580
713
|
log.error(`[${tmpl.name}] (${label}) ${e.code}: ${e.path}`);
|
|
581
714
|
errorCount++;
|
|
715
|
+
anyErrorHere = true;
|
|
582
716
|
}
|
|
583
717
|
}
|
|
584
718
|
}
|
|
585
|
-
if (anyBodyChecked)
|
|
719
|
+
if (anyBodyChecked || anyErrorHere)
|
|
586
720
|
checkedTemplates++;
|
|
587
721
|
}
|
|
588
722
|
if (errorCount > 0) {
|
|
589
723
|
log.error(`meta verify — ${errorCount} drift error(s) across ${checkedTemplates} template(s).`);
|
|
590
724
|
return 1;
|
|
591
725
|
}
|
|
592
|
-
|
|
726
|
+
say(`meta verify — ${checkedTemplates} template(s) clean${warnCount > 0 ? ` (${warnCount} warning(s))` : ""}.`);
|
|
593
727
|
return 0;
|
|
594
728
|
}
|
|
595
729
|
// -- schema drift (live DB) ------------------------------------------------
|
|
@@ -852,17 +986,17 @@ wranglerRunner) {
|
|
|
852
986
|
// annotate them as external (declared) rather than let them vanish silently.
|
|
853
987
|
const externalDeclared = collectUnmanagedNames(root);
|
|
854
988
|
if (externalDeclared.length > 0) {
|
|
855
|
-
|
|
989
|
+
say(`meta verify — ${externalDeclared.length} object(s) external (declared @unmanaged, managed elsewhere): ${externalDeclared.join(", ")}`);
|
|
856
990
|
}
|
|
857
991
|
// Same reasoning for the per-command scope: an object `migrate.scope` excluded
|
|
858
992
|
// was NOT checked, and silence would misreport it as checked-and-clean. Shared
|
|
859
993
|
// wording with `meta migrate` — one declaration, one sentence about it.
|
|
860
994
|
if (driftResult.outOfScope.length > 0) {
|
|
861
|
-
|
|
995
|
+
say(outOfScopeNote("verify", driftResult.outOfScope));
|
|
862
996
|
}
|
|
863
997
|
const changes = driftResult.changes;
|
|
864
998
|
if (changes.length === 0 && ledgerDrift.length === 0) {
|
|
865
|
-
|
|
999
|
+
say(`meta verify — schema in sync with ${displayUrl}.`);
|
|
866
1000
|
return 0;
|
|
867
1001
|
}
|
|
868
1002
|
// The header is conditional: #292's snapshot findings arrive through `ledgerDrift`
|
|
@@ -914,6 +1048,11 @@ wranglerRunner) {
|
|
|
914
1048
|
}
|
|
915
1049
|
catch (err) {
|
|
916
1050
|
log.error(`verify --codegen: failed to load this package's metadata: ${err.message}`);
|
|
1051
|
+
// The second door onto the same strict load. It carried no remedy at all, which
|
|
1052
|
+
// meant a retirement diagnosed here told the author what broke and nothing about
|
|
1053
|
+
// how to fix it — the half-true rule this file's sibling comment warns about.
|
|
1054
|
+
for (const s of err.suggestions ?? [])
|
|
1055
|
+
log.error(` ${s}`);
|
|
917
1056
|
return 2;
|
|
918
1057
|
}
|
|
919
1058
|
}
|
|
@@ -930,7 +1069,7 @@ wranglerRunner) {
|
|
|
930
1069
|
return 2;
|
|
931
1070
|
}
|
|
932
1071
|
if (result.clean) {
|
|
933
|
-
|
|
1072
|
+
say("meta verify — generated output is in sync with the metadata (no codegen drift).");
|
|
934
1073
|
return 0;
|
|
935
1074
|
}
|
|
936
1075
|
log.error(`meta verify — codegen drift (${result.driftedFiles.length} file(s) differ from a fresh regen):`);
|
|
@@ -939,6 +1078,57 @@ wranglerRunner) {
|
|
|
939
1078
|
log.error("Run 'meta gen' to regenerate, then commit the result.");
|
|
940
1079
|
return 1;
|
|
941
1080
|
}
|
|
1081
|
+
// -- docs drift -------------------------------------------------------------
|
|
1082
|
+
// Gated on --docs. Runs `meta docs` into a temp dir and diffs the committed docs
|
|
1083
|
+
// tree. See lib/docs-drift.ts for the two deliberate differences from --codegen
|
|
1084
|
+
// (a byte difference IS drift here, and a committed file a regen would not emit is
|
|
1085
|
+
// never reported — `docs.outDir` is full of files MetaObjects did not write).
|
|
1086
|
+
//
|
|
1087
|
+
// It needs a config for the same reason `--codegen` does: `docs.outDir` says which
|
|
1088
|
+
// tree to compare against, and the `agent` surface will not even materialise
|
|
1089
|
+
// without one.
|
|
1090
|
+
async function runDocsVerify() {
|
|
1091
|
+
if (forgeConfig === undefined) {
|
|
1092
|
+
log.error("verify --docs: no metaobjects.config.ts found (or it is invalid) — " +
|
|
1093
|
+
"cannot locate the committed docs tree to diff against. " +
|
|
1094
|
+
"Run 'meta init' to scaffold one, or run without --docs.");
|
|
1095
|
+
return 2;
|
|
1096
|
+
}
|
|
1097
|
+
// Resolved through the SAME resolver `meta docs` uses, so a project that moved its
|
|
1098
|
+
// docs elsewhere is checked where its docs actually are. `outputLayout` is the
|
|
1099
|
+
// documented fallback for `docs.layout`.
|
|
1100
|
+
const docsCfg = resolveDocsConfig(forgeConfig.docs, {}, forgeConfig.outputLayout ?? "flat");
|
|
1101
|
+
let result;
|
|
1102
|
+
try {
|
|
1103
|
+
result = await computeDocsDrift({
|
|
1104
|
+
projectRoot: genConfigDir,
|
|
1105
|
+
docsDir: resolvePath(genConfigDir, docsCfg.outDir),
|
|
1106
|
+
cwd: genConfigDir,
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
catch (err) {
|
|
1110
|
+
log.error(`verify --docs: regeneration failed: ${err.message}`);
|
|
1111
|
+
return 1;
|
|
1112
|
+
}
|
|
1113
|
+
if (result.error !== undefined) {
|
|
1114
|
+
log.error(result.error);
|
|
1115
|
+
return 2;
|
|
1116
|
+
}
|
|
1117
|
+
if (result.clean) {
|
|
1118
|
+
// The denominator is what was actually COMPARED, so the passing line and the
|
|
1119
|
+
// failing line below count the same set. A gate whose two halves divide by
|
|
1120
|
+
// different numbers is how `verify --templates` came to report seven templates
|
|
1121
|
+
// vanishing between a red run and a green one.
|
|
1122
|
+
say(`meta verify — ${result.checked} docs page(s) match a fresh 'meta docs' (no docs drift).`);
|
|
1123
|
+
return 0;
|
|
1124
|
+
}
|
|
1125
|
+
log.error(`meta verify — docs drift (${result.driftedFiles.length} of ${result.checked} page(s) ` +
|
|
1126
|
+
`differ from a fresh 'meta docs'):`);
|
|
1127
|
+
for (const line of result.lines)
|
|
1128
|
+
log.error(` ${line}`);
|
|
1129
|
+
log.error("Run 'meta docs' to regenerate, then commit the result.");
|
|
1130
|
+
return 1;
|
|
1131
|
+
}
|
|
942
1132
|
}
|
|
943
1133
|
/**
|
|
944
1134
|
* Migration-history ledger reconciliation hook (Unit 3 — not yet built).
|
|
@@ -982,4 +1172,69 @@ function summarizeDrift(changes) {
|
|
|
982
1172
|
return `${p.glyph} ${p.noun} ${describeChange(c)}`;
|
|
983
1173
|
});
|
|
984
1174
|
}
|
|
1175
|
+
/** Project a requirement diagnostic into a payload row. */
|
|
1176
|
+
function toDiagnosticRow(d, source) {
|
|
1177
|
+
return {
|
|
1178
|
+
code: d.code,
|
|
1179
|
+
// "" rather than omitted: a tabular encoder (TOON) needs every row to carry the
|
|
1180
|
+
// same keys, and an absent column would silently shift the rest.
|
|
1181
|
+
path: d.path ?? "",
|
|
1182
|
+
severity: d.severity,
|
|
1183
|
+
source,
|
|
1184
|
+
message: d.message,
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* What `--format json|toon` puts on stdout.
|
|
1189
|
+
*
|
|
1190
|
+
* The rule this shape is built to keep: **anything not represented here is
|
|
1191
|
+
* STATED, not silently dropped.** A half-payload that looks complete is the very
|
|
1192
|
+
* defect being fixed — an agent read a run carrying 233 advisory findings as "all
|
|
1193
|
+
* green across the board", because the findings were on stderr as text and the
|
|
1194
|
+
* payload mentioned none of them.
|
|
1195
|
+
*
|
|
1196
|
+
* So: every gate's verdict, every advisory finding (uncapped), and an explicit
|
|
1197
|
+
* `notRepresented` list naming what stays on stderr as text.
|
|
1198
|
+
*/
|
|
1199
|
+
function buildVerifyPayload(input) {
|
|
1200
|
+
const ran = input.gates.filter((g) => g.ran);
|
|
1201
|
+
const failed = ran.filter((g) => !g.ok);
|
|
1202
|
+
const parts = [
|
|
1203
|
+
failed.length === 0
|
|
1204
|
+
? `${ran.length} gate(s) ran, all clean`
|
|
1205
|
+
: `${failed.length} of ${ran.length} gate(s) failed (${failed.map((g) => g.gate).join(", ")})`,
|
|
1206
|
+
];
|
|
1207
|
+
if (input.antiPatterns.status === "ran" && input.antiPatterns.total > 0) {
|
|
1208
|
+
parts.push(`${input.antiPatterns.total} advisory anti-pattern finding(s)`);
|
|
1209
|
+
}
|
|
1210
|
+
if (input.requirements.total > 0) {
|
|
1211
|
+
parts.push(`${input.requirements.total} requirement diagnostic(s)`);
|
|
1212
|
+
}
|
|
1213
|
+
const help = [];
|
|
1214
|
+
if (failed.length > 0) {
|
|
1215
|
+
help.push(`the failing gate's drift DETAIL is printed as text on stderr — this payload carries the verdict only`);
|
|
1216
|
+
}
|
|
1217
|
+
if (input.antiPatterns.total > 0) {
|
|
1218
|
+
help.push(`${input.antiPatterns.total} authored site(s) hand-roll what MetaObjects can model — see antiPatterns.rows[] and run \`meta types <construct>\``);
|
|
1219
|
+
}
|
|
1220
|
+
if (failed.length === 0 && input.antiPatterns.total === 0 && input.requirements.total === 0) {
|
|
1221
|
+
help.push("no drift and nothing advisory to answer — nothing to do");
|
|
1222
|
+
}
|
|
1223
|
+
return {
|
|
1224
|
+
verify: input.gates,
|
|
1225
|
+
exitCode: input.exitCode,
|
|
1226
|
+
summary: parts.join("; "),
|
|
1227
|
+
help,
|
|
1228
|
+
antiPatterns: input.antiPatterns,
|
|
1229
|
+
requirements: input.requirements,
|
|
1230
|
+
...(input.requirementCounts !== undefined ? { requirementCounts: input.requirementCounts } : {}),
|
|
1231
|
+
// The honest boundary. Everything named here is REACHABLE — it is printed as
|
|
1232
|
+
// text on stderr — but it is not in this document, and a reader must not have
|
|
1233
|
+
// to discover that by its absence.
|
|
1234
|
+
notRepresented: [
|
|
1235
|
+
"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",
|
|
1236
|
+
"the loader's own warnings and the agent-context/manifest advisories — printed as text on stderr",
|
|
1237
|
+
],
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
985
1240
|
//# sourceMappingURL=verify.js.map
|