@metaobjectsdev/cli 0.22.1 → 0.23.1-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +117 -9
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts +30 -0
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +259 -13
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/dist/src/lib/verified-by-scan.d.ts +3 -1
- package/dist/src/lib/verified-by-scan.d.ts.map +1 -1
- package/dist/src/lib/verified-by-scan.js +192 -16
- package/dist/src/lib/verified-by-scan.js.map +1 -1
- package/package.json +10 -10
- package/src/commands/verify.ts +133 -8
- package/src/lib/requirement-check.ts +275 -12
- package/src/lib/verified-by-scan.ts +202 -17
|
@@ -21,25 +21,94 @@
|
|
|
21
21
|
// says NOTHING rather than reporting every name missing. Absence of evidence is
|
|
22
22
|
// not evidence of absence, and a monorepo whose tests live outside `--cwd` must
|
|
23
23
|
// not be told its requirements are unverified.
|
|
24
|
+
//
|
|
25
|
+
// WHAT COUNTS AS A TEST FILE IS THE PROJECT'S CALL, NOT OURS. The built-in patterns
|
|
26
|
+
// below are a convenience for the ecosystems this repo ports to, and they are a GUESS
|
|
27
|
+
// about someone else's repository. They were wrong on a mainstream case from the day
|
|
28
|
+
// they shipped: Maven Failsafe names integration tests `FooIT.java`, which matched
|
|
29
|
+
// nothing, so a JVM project naming a real integration test got a confident
|
|
30
|
+
// "the claim was never true".
|
|
31
|
+
//
|
|
32
|
+
// Two consequences, both deliberate:
|
|
33
|
+
// - `testFiles` (config: `verify.testFiles`) lets a project declare its own
|
|
34
|
+
// conventions, unioned with the built-ins. Nothing here can be authoritative
|
|
35
|
+
// about a convention we have never seen.
|
|
36
|
+
// - the fail-open above is extended from "no test files at all" to the case that
|
|
37
|
+
// actually bites: a name we cannot find in the corpus, which IS present in a file
|
|
38
|
+
// the corpus definition did not classify. That is our ignorance, not a broken
|
|
39
|
+
// claim, and it is reported as such (WARN_REQUIREMENT_TEST_UNCLASSIFIED) rather
|
|
40
|
+
// than as an error. The error is reserved for a name that appears NOWHERE.
|
|
24
41
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
25
42
|
import { join, relative, sep } from "node:path";
|
|
26
43
|
import { TYPE_REQUIREMENT, } from "@metaobjectsdev/metadata";
|
|
27
44
|
export const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
|
|
28
45
|
export const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
|
|
46
|
+
export const WARN_REQUIREMENT_TEST_COMMENT_ONLY = "WARN_REQUIREMENT_TEST_COMMENT_ONLY";
|
|
47
|
+
export const WARN_REQUIREMENT_TEST_UNCLASSIFIED = "WARN_REQUIREMENT_TEST_UNCLASSIFIED";
|
|
29
48
|
const IGNORE_SEGMENTS = new Set([
|
|
30
49
|
"node_modules", ".git", "dist", "build", "out", ".next", "coverage",
|
|
31
50
|
".metaobjects", "generated", "target", "bin", "obj", "__pycache__", ".venv", "venv",
|
|
32
51
|
]);
|
|
33
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Test files across the five ecosystems this project ports to — a CONVENIENCE DEFAULT,
|
|
54
|
+
* never an authority. A project whose conventions differ declares them via
|
|
55
|
+
* `verify.testFiles`; see the module header.
|
|
56
|
+
*
|
|
57
|
+
* The `IT` entries are Maven Failsafe's own defaults (`IT*`, `*IT`, `*ITCase`), which
|
|
58
|
+
* is how every JVM project in the wild names an integration test. Their absence is the
|
|
59
|
+
* bug that motivated making this list extensible in the first place.
|
|
60
|
+
*/
|
|
34
61
|
const TEST_FILE = new RegExp([
|
|
35
62
|
"\\.(?:test|spec)\\.[cm]?[jt]sx?$", // bun / jest / vitest / mocha
|
|
36
63
|
"(?:^|[./_-])[Tt]est[^/]*\\.java$", // JUnit — TestFoo.java
|
|
37
64
|
"[A-Za-z0-9]Test(?:s)?\\.java$", // JUnit — FooTest.java / FooTests.java
|
|
65
|
+
"[A-Za-z0-9]IT(?:Case)?\\.java$", // Failsafe — FooIT.java / FooITCase.java
|
|
66
|
+
"^IT[A-Za-z0-9][^/]*\\.java$", // Failsafe — ITFoo.java
|
|
38
67
|
"[A-Za-z0-9]Tests?\\.cs$", // xUnit / NUnit
|
|
39
68
|
"^test_[^/]*\\.py$", // pytest
|
|
40
69
|
"[^/]*_test\\.py$", // pytest, trailing convention
|
|
41
70
|
"[A-Za-z0-9]Test(?:s)?\\.kt$", // Kotlin
|
|
71
|
+
"[A-Za-z0-9]IT(?:Case)?\\.kt$", // Failsafe under Kotlin — FooIT.kt
|
|
42
72
|
].join("|"));
|
|
73
|
+
/** Files worth searching when a name is missing from the corpus, to tell "nowhere" from
|
|
74
|
+
* "somewhere I did not classify". Source-ish only; a match in a lockfile proves nothing. */
|
|
75
|
+
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|java|kt|kts|cs|py|rb|go|rs|scala|groovy|feature)$/;
|
|
76
|
+
/**
|
|
77
|
+
* A glob as permissive as the ones adopters actually write (`**/*IT.kt`, `*.feature`),
|
|
78
|
+
* anchored at the project root and matched against forward-slash relative paths.
|
|
79
|
+
*
|
|
80
|
+
* Deliberately small: `**` spans separators, `*` does not, `?` is one non-separator
|
|
81
|
+
* character. Anything richer belongs to a glob library, and pulling one in for a config
|
|
82
|
+
* knob this narrow is not worth the dependency.
|
|
83
|
+
*/
|
|
84
|
+
function globToRegExp(glob) {
|
|
85
|
+
let out = "";
|
|
86
|
+
for (let i = 0; i < glob.length; i++) {
|
|
87
|
+
const c = glob[i];
|
|
88
|
+
if (c === "*") {
|
|
89
|
+
if (glob[i + 1] === "*") {
|
|
90
|
+
// `**/` may match zero segments, so `**/*.feature` matches a root-level file.
|
|
91
|
+
if (glob[i + 2] === "/") {
|
|
92
|
+
out += "(?:.*/)?";
|
|
93
|
+
i += 2;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
out += ".*";
|
|
97
|
+
i += 1;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
out += "[^/]*";
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (c === "?") {
|
|
105
|
+
out += "[^/]";
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
109
|
+
}
|
|
110
|
+
return new RegExp(`^${out}$`);
|
|
111
|
+
}
|
|
43
112
|
/** Markers that a test exists but is disabled, across the same ecosystems. */
|
|
44
113
|
const SKIP_MARKER = new RegExp([
|
|
45
114
|
"\\b(?:it|test|describe)\\.(?:skip|todo)\\b", // jest/vitest/bun
|
|
@@ -50,7 +119,7 @@ const SKIP_MARKER = new RegExp([
|
|
|
50
119
|
"\\[Ignore[\\](]", // MSTest / NUnit
|
|
51
120
|
"\\bSkip\\s*=", // xUnit [Fact(Skip = "...")]
|
|
52
121
|
].join("|"));
|
|
53
|
-
function walk(dir, root, acc, depth = 0) {
|
|
122
|
+
function walk(dir, root, acc, isTestFile, depth = 0) {
|
|
54
123
|
if (depth > 12)
|
|
55
124
|
return; // pathological trees; the scan is advisory, not exhaustive
|
|
56
125
|
let entries;
|
|
@@ -64,16 +133,22 @@ function walk(dir, root, acc, depth = 0) {
|
|
|
64
133
|
if (e.isDirectory()) {
|
|
65
134
|
if (IGNORE_SEGMENTS.has(e.name) || e.name.startsWith("."))
|
|
66
135
|
continue;
|
|
67
|
-
walk(join(dir, e.name), root, acc, depth + 1);
|
|
136
|
+
walk(join(dir, e.name), root, acc, isTestFile, depth + 1);
|
|
68
137
|
continue;
|
|
69
138
|
}
|
|
70
|
-
if (!e.isFile()
|
|
139
|
+
if (!e.isFile())
|
|
71
140
|
continue;
|
|
72
141
|
const abs = join(dir, e.name);
|
|
142
|
+
const rel = relative(root, abs).split(sep).join("/");
|
|
143
|
+
if (!isTestFile(rel, e.name)) {
|
|
144
|
+
if (SOURCE_FILE.test(e.name) && acc.unclassified.length < 20_000)
|
|
145
|
+
acc.unclassified.push(rel);
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
73
148
|
try {
|
|
74
149
|
if (statSync(abs).size > 512 * 1024)
|
|
75
150
|
continue;
|
|
76
|
-
acc.byFile.set(
|
|
151
|
+
acc.byFile.set(rel, readFileSync(abs, "utf8").split("\n"));
|
|
77
152
|
acc.files++;
|
|
78
153
|
}
|
|
79
154
|
catch {
|
|
@@ -81,6 +156,45 @@ function walk(dir, root, acc, depth = 0) {
|
|
|
81
156
|
}
|
|
82
157
|
}
|
|
83
158
|
}
|
|
159
|
+
/** Does this path or body look like a test the corpus definition simply did not match?
|
|
160
|
+
* Deliberately narrow: living under a test directory, or containing an assertion/test
|
|
161
|
+
* declaration. Without this, a name occurring anywhere in PRODUCTION source downgrades a
|
|
162
|
+
* genuinely broken claim to a warning — the exact failure the comment-only check exists
|
|
163
|
+
* to catch. */
|
|
164
|
+
const TESTISH_PATH = /(^|\/)(tests?|spec|__tests__|src\/test)(\/|$)/i;
|
|
165
|
+
const TESTISH_BODY = /\b(assert\w*|expect|should|@Test|def test_|it\(|test\(|describe\()/;
|
|
166
|
+
/** Test by LOCATION or by CONTENT — either is enough. Production source with a matching
|
|
167
|
+
* name satisfies neither, which is the case that must stay a hard error. */
|
|
168
|
+
function looksLikeTest(rel, lines) {
|
|
169
|
+
return TESTISH_PATH.test(rel) || lines.some((l) => TESTISH_BODY.test(l));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Where does this name live, if not in the test corpus?
|
|
173
|
+
*
|
|
174
|
+
* Only ever called on the miss path, so the cost is paid per BROKEN claim rather than
|
|
175
|
+
* per run. Returns the first unclassified source file containing the name, which is
|
|
176
|
+
* enough to tell the author which pattern they are missing.
|
|
177
|
+
*/
|
|
178
|
+
function findOutsideCorpus(name, root, files) {
|
|
179
|
+
const rx = wordRx(name);
|
|
180
|
+
for (const rel of files) {
|
|
181
|
+
try {
|
|
182
|
+
const abs = join(root, ...rel.split("/"));
|
|
183
|
+
if (statSync(abs).size > 512 * 1024)
|
|
184
|
+
continue;
|
|
185
|
+
const lines = readFileSync(abs, "utf8").split("\n");
|
|
186
|
+
for (let i = 0; i < lines.length; i++) {
|
|
187
|
+
const line = lines[i] ?? "";
|
|
188
|
+
if (rx.test(line) && !isCommentLine(line, rel) && looksLikeTest(rel, lines))
|
|
189
|
+
return rel;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
/* unreadable file is not a finding */
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
}
|
|
84
198
|
/** Every `requirement.*` node in the tree, at any nesting depth. */
|
|
85
199
|
function collect(root) {
|
|
86
200
|
const out = [];
|
|
@@ -102,6 +216,23 @@ function collect(root) {
|
|
|
102
216
|
* emit the confident false error this scan is built to avoid. Camel-case boundaries
|
|
103
217
|
* stay strict, which is what actually prevents a short name matching a longer one.
|
|
104
218
|
*/
|
|
219
|
+
/**
|
|
220
|
+
* Is this whole line a comment?
|
|
221
|
+
*
|
|
222
|
+
* WHOLE-LINE ONLY, deliberately. Stripping from the first `//` would truncate a code
|
|
223
|
+
* line containing one inside a string — a test titled with a URL is the obvious case —
|
|
224
|
+
* and turn a real match into a confident false error, which is the failure this scan is
|
|
225
|
+
* built to avoid. A trailing comment after code therefore still counts as code; that
|
|
226
|
+
* under-flags, which is the repo's standing bias for drift checks.
|
|
227
|
+
*
|
|
228
|
+
* `#` is Python-only: in TypeScript it opens a private field, not a comment.
|
|
229
|
+
*/
|
|
230
|
+
function isCommentLine(line, file) {
|
|
231
|
+
const t = line.trimStart();
|
|
232
|
+
if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*"))
|
|
233
|
+
return true;
|
|
234
|
+
return file.endsWith(".py") && t.startsWith("#");
|
|
235
|
+
}
|
|
105
236
|
function wordRx(name) {
|
|
106
237
|
return new RegExp(`(?:^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9])`);
|
|
107
238
|
}
|
|
@@ -112,12 +243,17 @@ function wordRx(name) {
|
|
|
112
243
|
* and silent on `abandoned`/`superseded`, because a retired requirement naming a
|
|
113
244
|
* deleted test is the entry doing its job, not drift.
|
|
114
245
|
*/
|
|
115
|
-
export function checkVerifiedBy(root, cwd) {
|
|
246
|
+
export function checkVerifiedBy(root, cwd, testFiles) {
|
|
116
247
|
const reqs = collect(root).filter((r) => r.verifiedBy().length > 0);
|
|
117
248
|
if (reqs.length === 0)
|
|
118
249
|
return []; // opt-in by declaration
|
|
119
|
-
|
|
120
|
-
|
|
250
|
+
// Project-declared conventions ADD to the built-ins: the failure being fixed is
|
|
251
|
+
// under-matching, and a project that names an extra convention is telling us
|
|
252
|
+
// something we did not know — not asking us to forget what we did.
|
|
253
|
+
const declared = (testFiles ?? []).map(globToRegExp);
|
|
254
|
+
const isTestFile = (rel, base) => TEST_FILE.test(base) || declared.some((rx) => rx.test(rel));
|
|
255
|
+
const corpus = { files: 0, byFile: new Map(), unclassified: [] };
|
|
256
|
+
walk(cwd, cwd, corpus, isTestFile);
|
|
121
257
|
if (corpus.files === 0)
|
|
122
258
|
return []; // fail open: nothing to judge against
|
|
123
259
|
const out = [];
|
|
@@ -126,10 +262,20 @@ export function checkVerifiedBy(root, cwd) {
|
|
|
126
262
|
const rx = wordRx(test);
|
|
127
263
|
let foundIn;
|
|
128
264
|
let skippedAt;
|
|
265
|
+
// #293-adjacent (the `verifiedBy` audit): a name that occurs ONLY in comments
|
|
266
|
+
// satisfied this scan, because the match is line-agnostic. That is how a claim
|
|
267
|
+
// came to name `mountCrudRoutes`, whose single occurrence in an entire corpus was
|
|
268
|
+
// inside a `// via mountCrudRoutes(...)` note. Tracked separately so the name can
|
|
269
|
+
// still be reported as found (it is) while saying what it was found in.
|
|
270
|
+
let commentOnlyAt;
|
|
129
271
|
for (const [file, lines] of corpus.byFile) {
|
|
130
272
|
for (let i = 0; i < lines.length; i++) {
|
|
131
273
|
if (!rx.test(lines[i] ?? ""))
|
|
132
274
|
continue;
|
|
275
|
+
if (isCommentLine(lines[i] ?? "", file)) {
|
|
276
|
+
commentOnlyAt ??= `${file}:${i + 1}`;
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
133
279
|
foundIn ??= file;
|
|
134
280
|
// a decorator/annotation sits above the declaration it disables
|
|
135
281
|
const window = lines.slice(Math.max(0, i - 3), i + 1).join("\n");
|
|
@@ -139,16 +285,46 @@ export function checkVerifiedBy(root, cwd) {
|
|
|
139
285
|
if (foundIn !== undefined && skippedAt !== undefined)
|
|
140
286
|
break;
|
|
141
287
|
}
|
|
288
|
+
// Found, but only ever in prose. Not an error — the scan's job is to catch a name
|
|
289
|
+
// that has gone missing, and this one has not — but a comment proves nothing, so
|
|
290
|
+
// the claim is reported rather than silently accepted.
|
|
291
|
+
if (foundIn === undefined && commentOnlyAt !== undefined) {
|
|
292
|
+
out.push({
|
|
293
|
+
severity: "warn",
|
|
294
|
+
code: WARN_REQUIREMENT_TEST_COMMENT_ONLY,
|
|
295
|
+
name: req.name,
|
|
296
|
+
message: `'verifiedBy' names '${test}', which appears only in a comment (${commentOnlyAt}) ` +
|
|
297
|
+
`and in no test declaration. A comment proves nothing — name the test that asserts it.`,
|
|
298
|
+
});
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
142
301
|
if (foundIn === undefined) {
|
|
143
302
|
if (req.requiresLiveNodes()) {
|
|
144
|
-
out
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
303
|
+
// Before calling a claim broken, rule out the likelier explanation: that this
|
|
304
|
+
// project names its tests in a way the corpus definition does not know. A name
|
|
305
|
+
// sitting in an unclassified source file is OUR ignorance, and saying "the claim
|
|
306
|
+
// was never true" about it is the tool being confidently wrong.
|
|
307
|
+
const elsewhere = findOutsideCorpus(test, cwd, corpus.unclassified);
|
|
308
|
+
out.push(elsewhere !== undefined
|
|
309
|
+
? {
|
|
310
|
+
severity: "warn",
|
|
311
|
+
code: WARN_REQUIREMENT_TEST_UNCLASSIFIED,
|
|
312
|
+
name: req.name,
|
|
313
|
+
message: `'verifiedBy' names '${test}', which is not in any of the ${corpus.files} ` +
|
|
314
|
+
`file(s) recognised as tests, but DOES appear in ${elsewhere}. That file is ` +
|
|
315
|
+
`probably a test this scan does not know how to recognise — declare the ` +
|
|
316
|
+
`convention in metaobjects.config.ts (verify.testFiles, e.g. ` +
|
|
317
|
+
`["**/*IT.kt"]) and this becomes a real check instead of a guess.`,
|
|
318
|
+
}
|
|
319
|
+
: {
|
|
320
|
+
severity: "error",
|
|
321
|
+
code: ERR_REQUIREMENT_TEST_MISSING,
|
|
322
|
+
name: req.name,
|
|
323
|
+
message: `'verifiedBy' names '${test}', which appears in none of the ` +
|
|
324
|
+
`${corpus.files} test file(s) found under this project, and in no other ` +
|
|
325
|
+
`source file either. Either the test was renamed or removed, or the ` +
|
|
326
|
+
`claim was never true.`,
|
|
327
|
+
});
|
|
152
328
|
}
|
|
153
329
|
continue;
|
|
154
330
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verified-by-scan.js","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,iFAAiF;AACjF,kFAAkF;AAClF,iFAAiF;AACjF,6CAA6C;AAC7C,EAAE;AACF,kFAAkF;AAClF,mFAAmF;AACnF,gFAAgF;AAChF,4CAA4C;AAC5C,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,kFAAkF;AAClF,kFAAkF;AAClF,mFAAmF;AACnF,oCAAoC;AACpC,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,+CAA+C;
|
|
1
|
+
{"version":3,"file":"verified-by-scan.js","sourceRoot":"","sources":["../../../src/lib/verified-by-scan.ts"],"names":[],"mappings":"AAAA,2CAA2C;AAC3C,EAAE;AACF,iFAAiF;AACjF,kFAAkF;AAClF,iFAAiF;AACjF,6CAA6C;AAC7C,EAAE;AACF,kFAAkF;AAClF,mFAAmF;AACnF,gFAAgF;AAChF,4CAA4C;AAC5C,EAAE;AACF,+EAA+E;AAC/E,2EAA2E;AAC3E,kFAAkF;AAClF,kFAAkF;AAClF,mFAAmF;AACnF,oCAAoC;AACpC,EAAE;AACF,iFAAiF;AACjF,gFAAgF;AAChF,gFAAgF;AAChF,+CAA+C;AAC/C,EAAE;AACF,oFAAoF;AACpF,sFAAsF;AACtF,qFAAqF;AACrF,mFAAmF;AACnF,2EAA2E;AAC3E,8BAA8B;AAC9B,EAAE;AACF,qCAAqC;AACrC,8EAA8E;AAC9E,iFAAiF;AACjF,6CAA6C;AAC7C,mFAAmF;AACnF,sFAAsF;AACtF,kFAAkF;AAClF,oFAAoF;AACpF,+EAA+E;AAE/E,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAC9D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AAChD,OAAO,EACL,gBAAgB,GAGjB,MAAM,0BAA0B,CAAC;AAElC,MAAM,CAAC,MAAM,4BAA4B,GAAG,8BAA8B,CAAC;AAC3E,MAAM,CAAC,MAAM,6BAA6B,GAAG,+BAA+B,CAAC;AAC7E,MAAM,CAAC,MAAM,kCAAkC,GAAG,oCAAoC,CAAC;AACvF,MAAM,CAAC,MAAM,kCAAkC,GAAG,oCAAoC,CAAC;AASvF,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC;IAC9B,cAAc,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU;IACnE,cAAc,EAAE,WAAW,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM;CACpF,CAAC,CAAC;AAEH;;;;;;;;GAQG;AACH,MAAM,SAAS,GAAG,IAAI,MAAM,CAC1B;IACE,kCAAkC,EAAE,8BAA8B;IAClE,kCAAkC,EAAE,uBAAuB;IAC3D,+BAA+B,EAAE,2CAA2C;IAC5E,gCAAgC,EAAE,4CAA4C;IAC9E,6BAA6B,EAAE,8BAA8B;IAC7D,yBAAyB,EAAE,0BAA0B;IACrD,mBAAmB,EAAE,yBAAyB;IAC9C,kBAAkB,EAAE,+CAA+C;IACnE,6BAA6B,EAAE,eAAe;IAC9C,8BAA8B,EAAE,wCAAwC;CACzE,CAAC,IAAI,CAAC,GAAG,CAAC,CACZ,CAAC;AAEF;6FAC6F;AAC7F,MAAM,WAAW,GAAG,qEAAqE,CAAC;AAE1F;;;;;;;GAOG;AACH,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,CAAE,CAAC;QACnB,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YACd,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;gBACxB,8EAA8E;gBAC9E,IAAI,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;oBAAC,GAAG,IAAI,UAAU,CAAC;oBAAC,CAAC,IAAI,CAAC,CAAC;gBAAC,CAAC;qBAAM,CAAC;oBAAC,GAAG,IAAI,IAAI,CAAC;oBAAC,CAAC,IAAI,CAAC,CAAC;gBAAC,CAAC;YACvF,CAAC;;gBAAM,GAAG,IAAI,OAAO,CAAC;YACtB,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;YAAC,GAAG,IAAI,MAAM,CAAC;YAAC,SAAS;QAAC,CAAC;QAC3C,GAAG,IAAI,CAAC,CAAC,OAAO,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,MAAM,CAAC,IAAI,GAAG,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,MAAM,WAAW,GAAG,IAAI,MAAM,CAC5B;IACE,4CAA4C,EAAE,kBAAkB;IAChE,6BAA6B,EAAE,8BAA8B;IAC7D,cAAc,EAAE,uCAAuC;IACvD,YAAY,EAAE,kDAAkD;IAChE,uBAAuB,EAAE,6BAA6B;IACtD,iBAAiB,EAAE,2CAA2C;IAC9D,cAAc,EAAE,2DAA2D;CAC5E,CAAC,IAAI,CAAC,GAAG,CAAC,CACZ,CAAC;AAWF,SAAS,IAAI,CACX,GAAW,EACX,IAAY,EACZ,GAAe,EACf,UAAkD,EAClD,KAAK,GAAG,CAAC;IAET,IAAI,KAAK,GAAG,EAAE;QAAE,OAAO,CAAC,2DAA2D;IACnF,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC;YACpB,IAAI,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;gBAAE,SAAS;YACpE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;YAC1D,SAAS;QACX,CAAC;QACD,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE;YAAE,SAAS;QAC1B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;QAC9B,MAAM,GAAG,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrD,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,YAAY,CAAC,MAAM,GAAG,MAAM;gBAAE,GAAG,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC7F,SAAS;QACX,CAAC;QACD,IAAI,CAAC;YACH,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI;gBAAE,SAAS;YAC9C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;YAC3D,GAAG,CAAC,KAAK,EAAE,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC;AACH,CAAC;AAED;;;;gBAIgB;AAChB,MAAM,YAAY,GAAG,gDAAgD,CAAC;AACtE,MAAM,YAAY,GAAG,oEAAoE,CAAC;AAE1F;6EAC6E;AAC7E,SAAS,aAAa,CAAC,GAAW,EAAE,KAAe;IACjD,OAAO,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3E,CAAC;AAED;;;;;;GAMG;AACH,SAAS,iBAAiB,CAAC,IAAY,EAAE,IAAY,EAAE,KAAe;IACpE,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACxB,KAAK,MAAM,GAAG,IAAI,KAAK,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;YAC1C,IAAI,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,GAAG,GAAG,IAAI;gBAAE,SAAS;YAC9C,MAAM,KAAK,GAAG,YAAY,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YACpD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtC,MAAM,IAAI,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5B,IAAI,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,IAAI,EAAE,GAAG,CAAC,IAAI,aAAa,CAAC,GAAG,EAAE,KAAK,CAAC;oBAAE,OAAO,GAAG,CAAC;YAC1F,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,sCAAsC;QACxC,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,oEAAoE;AACpE,SAAS,OAAO,CAAC,IAAc;IAC7B,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,MAAM,GAAG,GAAG,CAAC,CAAW,EAAQ,EAAE;QAChC,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC7B,IAAI,CAAC,CAAC,IAAI,KAAK,gBAAgB;gBAAE,GAAG,CAAC,IAAI,CAAC,CAAoB,CAAC,CAAC;YAChE,GAAG,CAAC,CAAC,CAAC,CAAC;QACT,CAAC;IACH,CAAC,CAAC;IACF,GAAG,CAAC,IAAI,CAAC,CAAC;IACV,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;;GAOG;AACH;;;;;;;;;;GAUG;AACH,SAAS,aAAa,CAAC,IAAY,EAAE,IAAY;IAC/C,MAAM,CAAC,GAAG,IAAI,CAAC,SAAS,EAAE,CAAC;IAC3B,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC/E,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,MAAM,CAAC,IAAY;IAC1B,OAAO,IAAI,MAAM,CAAC,qBAAqB,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;AACvG,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAC7B,IAAc,EACd,GAAW,EACX,SAAoB;IAEpB,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACpE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,CAAC,wBAAwB;IAE1D,gFAAgF;IAChF,6EAA6E;IAC7E,mEAAmE;IACnE,MAAM,QAAQ,GAAG,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;IACrD,MAAM,UAAU,GAAG,CAAC,GAAW,EAAE,IAAY,EAAW,EAAE,CACxD,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;IAE9D,MAAM,MAAM,GAAe,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,IAAI,GAAG,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE,CAAC;IAC7E,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,UAAU,CAAC,CAAC;IACnC,IAAI,MAAM,CAAC,KAAK,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC,CAAC,sCAAsC;IAEzE,MAAM,GAAG,GAA2B,EAAE,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,KAAK,MAAM,IAAI,IAAI,GAAG,CAAC,UAAU,EAAE,EAAE,CAAC;YACpC,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,OAA2B,CAAC;YAChC,IAAI,SAA6B,CAAC;YAClC,8EAA8E;YAC9E,+EAA+E;YAC/E,kFAAkF;YAClF,kFAAkF;YAClF,wEAAwE;YACxE,IAAI,aAAiC,CAAC;YACtC,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,MAAM,EAAE,CAAC;gBAC1C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;oBACtC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;wBAAE,SAAS;oBACvC,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,IAAI,CAAC,EAAE,CAAC;wBACxC,aAAa,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;wBACrC,SAAS;oBACX,CAAC;oBACD,OAAO,KAAK,IAAI,CAAC;oBACjB,gEAAgE;oBAChE,MAAM,MAAM,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBACjE,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC;wBAAE,SAAS,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjE,CAAC;gBACD,IAAI,OAAO,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;oBAAE,MAAM;YAC9D,CAAC;YAED,kFAAkF;YAClF,iFAAiF;YACjF,uDAAuD;YACvD,IAAI,OAAO,KAAK,SAAS,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;gBACzD,GAAG,CAAC,IAAI,CAAC;oBACP,QAAQ,EAAE,MAAM;oBAChB,IAAI,EAAE,kCAAkC;oBACxC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,OAAO,EACL,uBAAuB,IAAI,uCAAuC,aAAa,IAAI;wBACnF,uFAAuF;iBAC1F,CAAC,CAAC;gBACH,SAAS;YACX,CAAC;YAED,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,IAAI,GAAG,CAAC,iBAAiB,EAAE,EAAE,CAAC;oBAC5B,8EAA8E;oBAC9E,+EAA+E;oBAC/E,iFAAiF;oBACjF,gEAAgE;oBAChE,MAAM,SAAS,GAAG,iBAAiB,CAAC,IAAI,EAAE,GAAG,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;oBACpE,GAAG,CAAC,IAAI,CACN,SAAS,KAAK,SAAS;wBACrB,CAAC,CAAC;4BACE,QAAQ,EAAE,MAAM;4BAChB,IAAI,EAAE,kCAAkC;4BACxC,IAAI,EAAE,GAAG,CAAC,IAAI;4BACd,OAAO,EACL,uBAAuB,IAAI,iCAAiC,MAAM,CAAC,KAAK,GAAG;gCAC3E,mDAAmD,SAAS,iBAAiB;gCAC7E,yEAAyE;gCACzE,8DAA8D;gCAC9D,kEAAkE;yBACrE;wBACH,CAAC,CAAC;4BACE,QAAQ,EAAE,OAAO;4BACjB,IAAI,EAAE,4BAA4B;4BAClC,IAAI,EAAE,GAAG,CAAC,IAAI;4BACd,OAAO,EACL,uBAAuB,IAAI,kCAAkC;gCAC7D,GAAG,MAAM,CAAC,KAAK,0DAA0D;gCACzE,qEAAqE;gCACrE,uBAAuB;yBAC1B,CACN,CAAC;gBACJ,CAAC;gBACD,SAAS;YACX,CAAC;YACD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;gBAC5B,GAAG,CAAC,IAAI,CAAC;oBACP,QAAQ,EAAE,MAAM;oBAChB,IAAI,EAAE,6BAA6B;oBACnC,IAAI,EAAE,GAAG,CAAC,IAAI;oBACd,OAAO,EACL,uBAAuB,IAAI,4BAA4B,SAAS,IAAI;wBACpE,+EAA+E;iBAClF,CAAC,CAAC;YACL,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metaobjectsdev/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.1-rc.1",
|
|
4
4
|
"description": "CLI for MetaObjects: scaffold, codegen, migrate, and drift-detection commands.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -49,15 +49,15 @@
|
|
|
49
49
|
],
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@libsql/kysely-libsql": "^0.4.0",
|
|
52
|
-
"@metaobjectsdev/codegen-ts": "0.
|
|
53
|
-
"@metaobjectsdev/codegen-ts-react": "0.
|
|
54
|
-
"@metaobjectsdev/codegen-ts-tanstack": "0.
|
|
55
|
-
"@metaobjectsdev/docs-site": "0.
|
|
56
|
-
"@metaobjectsdev/metadata": "0.
|
|
57
|
-
"@metaobjectsdev/migrate-ts": "0.
|
|
58
|
-
"@metaobjectsdev/render": "0.
|
|
59
|
-
"@metaobjectsdev/runtime-ts": "0.
|
|
60
|
-
"@metaobjectsdev/sdk": "0.
|
|
52
|
+
"@metaobjectsdev/codegen-ts": "0.23.1-rc.1",
|
|
53
|
+
"@metaobjectsdev/codegen-ts-react": "0.23.1-rc.1",
|
|
54
|
+
"@metaobjectsdev/codegen-ts-tanstack": "0.23.1-rc.1",
|
|
55
|
+
"@metaobjectsdev/docs-site": "0.23.1-rc.1",
|
|
56
|
+
"@metaobjectsdev/metadata": "0.23.1-rc.1",
|
|
57
|
+
"@metaobjectsdev/migrate-ts": "0.23.1-rc.1",
|
|
58
|
+
"@metaobjectsdev/render": "0.23.1-rc.1",
|
|
59
|
+
"@metaobjectsdev/runtime-ts": "0.23.1-rc.1",
|
|
60
|
+
"@metaobjectsdev/sdk": "0.23.1-rc.1",
|
|
61
61
|
"@toon-format/toon": "^2.3.0",
|
|
62
62
|
"jiti": "^2.4.0"
|
|
63
63
|
},
|
package/src/commands/verify.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// the text ships. Required-slot misses are warnings (don't fail the build).
|
|
9
9
|
|
|
10
10
|
import { join, resolve as resolvePath } from "node:path";
|
|
11
|
-
import { parseVerifyArgs } from "../lib/args.js";
|
|
11
|
+
import { parseVerifyArgs, type MigrateFlags } from "../lib/args.js";
|
|
12
12
|
import { log } from "../lib/log.js";
|
|
13
13
|
import { warnIfAgentContextStale } from "../lib/agent-context-staleness.js";
|
|
14
14
|
import { scanSourceForAntiPatterns } from "../lib/anti-patterns.js";
|
|
@@ -16,9 +16,9 @@ import { FileProvider } from "../lib/file-provider.js";
|
|
|
16
16
|
import { derivePayloadFieldTree } from "../lib/payload-field-tree.js";
|
|
17
17
|
import { loadMetaobjectsConfig } from "../lib/load-metaobjects-config.js";
|
|
18
18
|
import { computeCodegenDrift } from "../lib/codegen-drift.js";
|
|
19
|
-
import { checkRequirements } from "../lib/requirement-check.js";
|
|
19
|
+
import { checkRequirements, summariseRequirements } from "../lib/requirement-check.js";
|
|
20
20
|
import { checkVerifiedBy } from "../lib/verified-by-scan.js";
|
|
21
|
-
import { resolveD1Config } from "../lib/config.js";
|
|
21
|
+
import { resolveD1Config, resolveMigrateConfig } from "../lib/config.js";
|
|
22
22
|
import {
|
|
23
23
|
buildWranglerExecuteArgs,
|
|
24
24
|
defaultWranglerRunner,
|
|
@@ -33,6 +33,11 @@ import {
|
|
|
33
33
|
computeDrift,
|
|
34
34
|
computeDriftFromActual,
|
|
35
35
|
collectUnmanagedNames,
|
|
36
|
+
introspect,
|
|
37
|
+
diff,
|
|
38
|
+
readSnapshot,
|
|
39
|
+
snapshotPath,
|
|
40
|
+
type SchemaSnapshot,
|
|
36
41
|
introspectD1,
|
|
37
42
|
findWranglerConfig,
|
|
38
43
|
parseWranglerConfig,
|
|
@@ -67,6 +72,19 @@ const DEFAULT_PROMPTS_DIR = "prompts";
|
|
|
67
72
|
// exported by the package, so named here to avoid an inline literal at the use site.
|
|
68
73
|
const ERR_UNKNOWN_ATTR = "ERR_UNKNOWN_ATTR";
|
|
69
74
|
|
|
75
|
+
/**
|
|
76
|
+
* A no-flags MigrateFlags, so `resolveMigrateConfig` yields exactly what `meta migrate`
|
|
77
|
+
* would use with nothing passed on the command line — config value, else default. verify
|
|
78
|
+
* consumes only `outDir` from the result (#292); the other fields exist to satisfy the
|
|
79
|
+
* shared shape, and reading any of them here would be reaching into migrate's decisions.
|
|
80
|
+
*/
|
|
81
|
+
const EMPTY_MIGRATE_FLAGS = {
|
|
82
|
+
db: undefined, dialect: undefined, format: undefined, outDir: undefined, slug: undefined,
|
|
83
|
+
allow: [], onAmbiguous: undefined, dryRun: false, d1Binding: undefined, remote: false,
|
|
84
|
+
apply: false, rollback: undefined, yes: false, fromDb: false, baseline: false,
|
|
85
|
+
applyPending: false,
|
|
86
|
+
} as const satisfies MigrateFlags;
|
|
87
|
+
|
|
70
88
|
/** Coerce a string-array attr (array, or a single string) into a string[]. */
|
|
71
89
|
function attrAsStringArray(attr: unknown): string[] {
|
|
72
90
|
if (Array.isArray(attr)) return attr.filter((s): s is string => typeof s === "string");
|
|
@@ -177,7 +195,36 @@ export async function verifyCommand(
|
|
|
177
195
|
function runRequirementVerify(): number {
|
|
178
196
|
// `@verifiedBy` resolution needs the project on disk, so it is a separate
|
|
179
197
|
// scan; its diagnostics carry the same severities and share this reporter.
|
|
180
|
-
|
|
198
|
+
// `verify.testFiles` lets a project name its own test-file conventions. What counts
|
|
199
|
+
// as a test is project-specific, and the built-in patterns are a convenience, not an
|
|
200
|
+
// authority — see the verified-by-scan header.
|
|
201
|
+
const diags = [
|
|
202
|
+
...checkRequirements(root),
|
|
203
|
+
...checkVerifiedBy(root, cwd, forgeConfig?.verify?.testFiles),
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
// Printed on EVERY run, clean or not — a gate that says nothing when it
|
|
207
|
+
// passes cannot be told apart from a gate that checked nothing, and the
|
|
208
|
+
// recorded-gap counts are the whole reason to keep a ledger.
|
|
209
|
+
const s = summariseRequirements(root);
|
|
210
|
+
if (s !== undefined) {
|
|
211
|
+
const order = ["planned", "live", "partial", "abandoned", "superseded"];
|
|
212
|
+
const parts = order
|
|
213
|
+
.filter((k) => (s.byStatus[k] ?? 0) > 0)
|
|
214
|
+
.map((k) => `${s.byStatus[k]} ${k}`);
|
|
215
|
+
log.info(
|
|
216
|
+
`meta verify — requirements: ${s.total} entries (${s.functional} functional, ` +
|
|
217
|
+
`${s.architectural} architectural) — ${parts.join(", ")}; ` +
|
|
218
|
+
`${s.entitiesClaimed}/${s.entitiesTotal} entities claimed.`,
|
|
219
|
+
);
|
|
220
|
+
if (s.undecided > 0) {
|
|
221
|
+
log.info(
|
|
222
|
+
`meta verify — requirements: ${s.undecided} recorded gap(s) with no @disposition. ` +
|
|
223
|
+
`These are known problems nobody has ruled on — set 'accepted' or 'deferred' to close the question.`,
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
181
228
|
if (diags.length === 0) return 0;
|
|
182
229
|
const errors = diags.filter((d) => d.severity === "error");
|
|
183
230
|
const warns = diags.filter((d) => d.severity === "warn");
|
|
@@ -375,14 +422,24 @@ export async function verifyCommand(
|
|
|
375
422
|
const viewStrategy = forgeConfig?.columnNamingStrategy ?? "snake_case";
|
|
376
423
|
const expectedViews = buildProjectionViews(root, { dialect: kysely.dialect, columnNamingStrategy: viewStrategy });
|
|
377
424
|
let driftResult;
|
|
425
|
+
let actual: SchemaSnapshot;
|
|
378
426
|
try {
|
|
379
|
-
|
|
427
|
+
// Introspect once and keep the result: #292's snapshot check needs the same
|
|
428
|
+
// `actual` this drift comparison uses, and re-introspecting for it would both
|
|
429
|
+
// cost a second round trip and open a window where the two could disagree.
|
|
430
|
+
actual = await introspect(kysely.db, kysely.dialect);
|
|
431
|
+
driftResult = await computeDriftFromActual(actual, kysely.dialect, root, { allow, views: expectedViews });
|
|
380
432
|
} catch (err) {
|
|
381
433
|
log.error(`verify: failed to introspect ${kysely.displayUrl}: ${(err as Error).message}`);
|
|
382
434
|
return 1;
|
|
383
435
|
}
|
|
384
436
|
|
|
385
|
-
|
|
437
|
+
const snapshotDrift =
|
|
438
|
+
driftResult.changes.length === 0
|
|
439
|
+
? await checkCommittedSnapshot(actual, kysely.dialect, kysely.displayUrl)
|
|
440
|
+
: [];
|
|
441
|
+
|
|
442
|
+
return reportSchemaDrift(driftResult, [...ledgerDrift, ...snapshotDrift], kysely.displayUrl);
|
|
386
443
|
} finally {
|
|
387
444
|
try {
|
|
388
445
|
await kysely.close();
|
|
@@ -466,6 +523,69 @@ export async function verifyCommand(
|
|
|
466
523
|
// Shared drift-reporting + exit-code logic for BOTH schema-drift paths (sqlite/
|
|
467
524
|
// postgres via computeDrift, D1 via computeDriftFromActual) — #225 requires the
|
|
468
525
|
// D1 path to feed the SAME reporting, not a forked copy.
|
|
526
|
+
// #292 — the committed reference snapshot is itself checked, and this is the only
|
|
527
|
+
// place in the toolchain that can do it.
|
|
528
|
+
//
|
|
529
|
+
// `meta migrate` diffs metadata against `.metaobjects/migrations/.schema.<dialect>.json`
|
|
530
|
+
// by default (`--from-db` is the documented opt-out), so that file decides what DDL the
|
|
531
|
+
// next migration contains. Nothing verified it. A snapshot gone stale — an interrupted
|
|
532
|
+
// migrate, a rollback, a bad merge resolution — passed `verify` clean and then made the
|
|
533
|
+
// next `migrate --slug` emit DDL that fails at apply (`column ... already exists`), which
|
|
534
|
+
// surfaces as a migration that cannot be applied and a history that cannot be reproduced.
|
|
535
|
+
//
|
|
536
|
+
// THE GATE IS CONDITIONED ON metadata==DB, deliberately, and that is what makes it
|
|
537
|
+
// false-positive-free. The snapshot means "the schema the COMMITTED MIGRATIONS land you
|
|
538
|
+
// in" and it advances at GENERATION time, so between `migrate --slug` and applying that
|
|
539
|
+
// migration the snapshot legitimately leads the database. In exactly that window the
|
|
540
|
+
// metadata↔DB drift is non-empty and this check stays silent; when metadata and the DB
|
|
541
|
+
// agree there is no pending work left to explain a difference, so a snapshot that
|
|
542
|
+
// disagrees is stale, full stop.
|
|
543
|
+
//
|
|
544
|
+
// Keying on the drift result rather than on the migration ledger is the load-bearing
|
|
545
|
+
// choice. A ledger-based "are there unapplied migrations?" test looks equivalent and is
|
|
546
|
+
// not: a project that applies its migrations out of band — psql, a CI step, another
|
|
547
|
+
// tool — has no ledger rows at all, so every migration reads as pending and the gate
|
|
548
|
+
// would silently never fire. That is the same class of defect as the one being fixed.
|
|
549
|
+
//
|
|
550
|
+
// Fails OPEN when there is no snapshot on disk (a project that has never generated one
|
|
551
|
+
// offline is not in an error state) and when the file cannot be read or parsed (that is
|
|
552
|
+
// migrate's error to raise, with its own message, not a drift verdict).
|
|
553
|
+
async function checkCommittedSnapshot(
|
|
554
|
+
actual: SchemaSnapshot,
|
|
555
|
+
dialect: Dialect,
|
|
556
|
+
displayUrl: string,
|
|
557
|
+
): Promise<string[]> {
|
|
558
|
+
if (dialect === "d1") return []; // d1 keeps migrations Wrangler-native; no offline snapshot
|
|
559
|
+
// Resolve the migrations dir through migrate's OWN precedence (flag > config >
|
|
560
|
+
// default) rather than re-deriving it, so verify can never look somewhere migrate
|
|
561
|
+
// does not write. Only `outDir` is consumed; the rest of the resolved config is
|
|
562
|
+
// migrate's business.
|
|
563
|
+
const migrateConfig = await resolveMigrateConfig(EMPTY_MIGRATE_FLAGS, cwd);
|
|
564
|
+
const dir = resolvePath(cwd, migrateConfig.outDir);
|
|
565
|
+
let snapshot: SchemaSnapshot | null;
|
|
566
|
+
try {
|
|
567
|
+
snapshot = await readSnapshot(snapshotPath(dir, dialect));
|
|
568
|
+
} catch {
|
|
569
|
+
return [];
|
|
570
|
+
}
|
|
571
|
+
if (snapshot === null) return [];
|
|
572
|
+
|
|
573
|
+
const result = await diff({
|
|
574
|
+
expected: snapshot,
|
|
575
|
+
actual,
|
|
576
|
+
allow: {},
|
|
577
|
+
unmanagedNames: collectUnmanagedNames(root),
|
|
578
|
+
});
|
|
579
|
+
if (result.changes.length === 0) return [];
|
|
580
|
+
|
|
581
|
+
return [
|
|
582
|
+
`the committed schema snapshot disagrees with ${displayUrl} ` +
|
|
583
|
+
`(${result.changes.length} difference(s)) — the next 'meta migrate' would emit DDL from it ` +
|
|
584
|
+
`and fail at apply. Re-derive it with 'meta migrate --from-db --db <url> --dialect ${dialect}'.`,
|
|
585
|
+
...summarizeDrift(result.changes),
|
|
586
|
+
];
|
|
587
|
+
}
|
|
588
|
+
|
|
469
589
|
function reportSchemaDrift(driftResult: DiffResult, ledgerDrift: string[], displayUrl: string): number {
|
|
470
590
|
// #208 §8 — make declared-external objects visible: they are excluded from the
|
|
471
591
|
// drift comparison (computeDrift/computeDriftFromActual thread them out), so
|
|
@@ -483,8 +603,13 @@ export async function verifyCommand(
|
|
|
483
603
|
return 0;
|
|
484
604
|
}
|
|
485
605
|
|
|
486
|
-
|
|
487
|
-
|
|
606
|
+
// The header is conditional: #292's snapshot findings arrive through `ledgerDrift`
|
|
607
|
+
// with the metadata↔DB comparison clean, and announcing "schema drift (0 change(s))"
|
|
608
|
+
// above them would contradict the very check that just passed.
|
|
609
|
+
if (changes.length > 0) {
|
|
610
|
+
log.error(`meta verify — schema drift vs ${displayUrl} (${changes.length} change(s)):`);
|
|
611
|
+
for (const line of summarizeDrift(changes)) log.error(` ${line}`);
|
|
612
|
+
}
|
|
488
613
|
for (const line of ledgerDrift) log.error(` ${line}`);
|
|
489
614
|
return 1;
|
|
490
615
|
}
|