@metaobjectsdev/cli 0.23.1 → 0.24.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 +78 -0
- package/dist/src/commands/docs.d.ts.map +1 -1
- package/dist/src/commands/docs.js +148 -50
- package/dist/src/commands/docs.js.map +1 -1
- package/dist/src/commands/export.d.ts.map +1 -1
- package/dist/src/commands/export.js +28 -6
- package/dist/src/commands/export.js.map +1 -1
- package/dist/src/commands/gen.d.ts.map +1 -1
- package/dist/src/commands/gen.js +61 -22
- package/dist/src/commands/gen.js.map +1 -1
- package/dist/src/commands/init.d.ts +8 -0
- package/dist/src/commands/init.d.ts.map +1 -1
- package/dist/src/commands/init.js +142 -52
- package/dist/src/commands/init.js.map +1 -1
- package/dist/src/commands/migrate.d.ts +9 -2
- package/dist/src/commands/migrate.d.ts.map +1 -1
- package/dist/src/commands/migrate.js +399 -73
- package/dist/src/commands/migrate.js.map +1 -1
- package/dist/src/commands/prompt-snapshot.d.ts.map +1 -1
- package/dist/src/commands/prompt-snapshot.js +40 -18
- package/dist/src/commands/prompt-snapshot.js.map +1 -1
- package/dist/src/commands/upgrade.d.ts +2 -0
- package/dist/src/commands/upgrade.d.ts.map +1 -0
- package/dist/src/commands/upgrade.js +139 -0
- package/dist/src/commands/upgrade.js.map +1 -0
- package/dist/src/commands/verify.d.ts.map +1 -1
- package/dist/src/commands/verify.js +336 -48
- package/dist/src/commands/verify.js.map +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +46 -15
- package/dist/src/index.js.map +1 -1
- package/dist/src/lib/allow.d.ts.map +1 -1
- package/dist/src/lib/allow.js +4 -0
- package/dist/src/lib/allow.js.map +1 -1
- package/dist/src/lib/args.d.ts +16 -2
- package/dist/src/lib/args.d.ts.map +1 -1
- package/dist/src/lib/args.js +19 -3
- package/dist/src/lib/args.js.map +1 -1
- package/dist/src/lib/codegen-drift.d.ts +8 -1
- package/dist/src/lib/codegen-drift.d.ts.map +1 -1
- package/dist/src/lib/codegen-drift.js +9 -1
- package/dist/src/lib/codegen-drift.js.map +1 -1
- package/dist/src/lib/detect-stack.d.ts +1 -1
- package/dist/src/lib/detect-stack.d.ts.map +1 -1
- package/dist/src/lib/detect-stack.js +22 -37
- package/dist/src/lib/detect-stack.js.map +1 -1
- package/dist/src/lib/load-metaobjects-config.d.ts +48 -1
- package/dist/src/lib/load-metaobjects-config.d.ts.map +1 -1
- package/dist/src/lib/load-metaobjects-config.js +80 -1
- package/dist/src/lib/load-metaobjects-config.js.map +1 -1
- package/dist/src/lib/manifest-ignored-check.d.ts +12 -0
- package/dist/src/lib/manifest-ignored-check.d.ts.map +1 -0
- package/dist/src/lib/manifest-ignored-check.js +74 -0
- package/dist/src/lib/manifest-ignored-check.js.map +1 -0
- package/dist/src/lib/migrate-scope.d.ts +49 -0
- package/dist/src/lib/migrate-scope.d.ts.map +1 -0
- package/dist/src/lib/migrate-scope.js +92 -0
- package/dist/src/lib/migrate-scope.js.map +1 -0
- package/dist/src/lib/output.d.ts +1 -1
- package/dist/src/lib/output.d.ts.map +1 -1
- package/dist/src/lib/output.js +9 -3
- package/dist/src/lib/output.js.map +1 -1
- package/dist/src/lib/requirement-check.d.ts.map +1 -1
- package/dist/src/lib/requirement-check.js +10 -57
- package/dist/src/lib/requirement-check.js.map +1 -1
- package/package.json +11 -11
- package/src/commands/docs.ts +158 -49
- package/src/commands/export.ts +30 -6
- package/src/commands/gen.ts +62 -22
- package/src/commands/init.ts +161 -58
- package/src/commands/migrate.ts +423 -75
- package/src/commands/prompt-snapshot.ts +43 -18
- package/src/commands/upgrade.ts +156 -0
- package/src/commands/verify.ts +373 -48
- package/src/index.ts +46 -15
- package/src/lib/allow.ts +4 -0
- package/src/lib/args.ts +35 -4
- package/src/lib/codegen-drift.ts +9 -0
- package/src/lib/detect-stack.ts +20 -33
- package/src/lib/load-metaobjects-config.ts +84 -2
- package/src/lib/manifest-ignored-check.ts +75 -0
- package/src/lib/migrate-scope.ts +102 -0
- package/src/lib/output.ts +8 -4
- package/src/lib/requirement-check.ts +10 -58
- package/dist/src/lib/verified-by-scan.d.ts +0 -20
- package/dist/src/lib/verified-by-scan.d.ts.map +0 -1
- package/dist/src/lib/verified-by-scan.js +0 -344
- package/dist/src/lib/verified-by-scan.js.map +0 -1
- package/src/lib/verified-by-scan.ts +0 -375
|
@@ -1,375 +0,0 @@
|
|
|
1
|
-
// `meta verify` — the `@verifiedBy` check.
|
|
2
|
-
//
|
|
3
|
-
// A requirement's `@verifiedBy` names the tests that prove the behaviour. verify
|
|
4
|
-
// checks each name EXISTS and is NOT SKIPPED; it never runs them. Running them is
|
|
5
|
-
// the test runner's job, and a requirement gate that shelled out to one would be
|
|
6
|
-
// slow, ecosystem-specific, and wrong in CI.
|
|
7
|
-
//
|
|
8
|
-
// Until this existed, `@verifiedBy` was registered vocabulary that nothing read —
|
|
9
|
-
// the precise pattern ADR-0007 Amendment 2 and the `@role` shrink exist to punish.
|
|
10
|
-
// An attribute the loader accepts and no consumer dispatches on teaches authors
|
|
11
|
-
// that declaring is free and means nothing.
|
|
12
|
-
//
|
|
13
|
-
// PRECISION OVER RECALL, deliberately. The scan matches a name anywhere in the
|
|
14
|
-
// test corpus, as a whole word, in any language. That is the most generous
|
|
15
|
-
// possible reading, so a "missing" verdict means the name appears in NO test file
|
|
16
|
-
// at all — which is a broken claim in any ecosystem. The repo's standing rule for
|
|
17
|
-
// drift checks is to bias toward under-flagging, and a nagging gate gets disabled,
|
|
18
|
-
// which costs more than the misses.
|
|
19
|
-
//
|
|
20
|
-
// FAIL-OPEN ON INABILITY. If the project has no test files this scan can see, it
|
|
21
|
-
// says NOTHING rather than reporting every name missing. Absence of evidence is
|
|
22
|
-
// not evidence of absence, and a monorepo whose tests live outside `--cwd` must
|
|
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.
|
|
41
|
-
|
|
42
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
43
|
-
import { join, relative, sep } from "node:path";
|
|
44
|
-
import {
|
|
45
|
-
TYPE_REQUIREMENT,
|
|
46
|
-
type MetaData,
|
|
47
|
-
type MetaRequirement,
|
|
48
|
-
} from "@metaobjectsdev/metadata";
|
|
49
|
-
|
|
50
|
-
export const ERR_REQUIREMENT_TEST_MISSING = "ERR_REQUIREMENT_TEST_MISSING";
|
|
51
|
-
export const WARN_REQUIREMENT_TEST_SKIPPED = "WARN_REQUIREMENT_TEST_SKIPPED";
|
|
52
|
-
export const WARN_REQUIREMENT_TEST_COMMENT_ONLY = "WARN_REQUIREMENT_TEST_COMMENT_ONLY";
|
|
53
|
-
export const WARN_REQUIREMENT_TEST_UNCLASSIFIED = "WARN_REQUIREMENT_TEST_UNCLASSIFIED";
|
|
54
|
-
|
|
55
|
-
export interface VerifiedByDiagnostic {
|
|
56
|
-
severity: "error" | "warn";
|
|
57
|
-
code: string;
|
|
58
|
-
name?: string;
|
|
59
|
-
message: string;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const IGNORE_SEGMENTS = new Set([
|
|
63
|
-
"node_modules", ".git", "dist", "build", "out", ".next", "coverage",
|
|
64
|
-
".metaobjects", "generated", "target", "bin", "obj", "__pycache__", ".venv", "venv",
|
|
65
|
-
]);
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Test files across the five ecosystems this project ports to — a CONVENIENCE DEFAULT,
|
|
69
|
-
* never an authority. A project whose conventions differ declares them via
|
|
70
|
-
* `verify.testFiles`; see the module header.
|
|
71
|
-
*
|
|
72
|
-
* The `IT` entries are Maven Failsafe's own defaults (`IT*`, `*IT`, `*ITCase`), which
|
|
73
|
-
* is how every JVM project in the wild names an integration test. Their absence is the
|
|
74
|
-
* bug that motivated making this list extensible in the first place.
|
|
75
|
-
*/
|
|
76
|
-
const TEST_FILE = new RegExp(
|
|
77
|
-
[
|
|
78
|
-
"\\.(?:test|spec)\\.[cm]?[jt]sx?$", // bun / jest / vitest / mocha
|
|
79
|
-
"(?:^|[./_-])[Tt]est[^/]*\\.java$", // JUnit — TestFoo.java
|
|
80
|
-
"[A-Za-z0-9]Test(?:s)?\\.java$", // JUnit — FooTest.java / FooTests.java
|
|
81
|
-
"[A-Za-z0-9]IT(?:Case)?\\.java$", // Failsafe — FooIT.java / FooITCase.java
|
|
82
|
-
"^IT[A-Za-z0-9][^/]*\\.java$", // Failsafe — ITFoo.java
|
|
83
|
-
"[A-Za-z0-9]Tests?\\.cs$", // xUnit / NUnit
|
|
84
|
-
"^test_[^/]*\\.py$", // pytest
|
|
85
|
-
"[^/]*_test\\.py$", // pytest, trailing convention
|
|
86
|
-
"[A-Za-z0-9]Test(?:s)?\\.kt$", // Kotlin
|
|
87
|
-
"[A-Za-z0-9]IT(?:Case)?\\.kt$", // Failsafe under Kotlin — FooIT.kt
|
|
88
|
-
].join("|"),
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
/** Files worth searching when a name is missing from the corpus, to tell "nowhere" from
|
|
92
|
-
* "somewhere I did not classify". Source-ish only; a match in a lockfile proves nothing. */
|
|
93
|
-
const SOURCE_FILE = /\.(?:[cm]?[jt]sx?|java|kt|kts|cs|py|rb|go|rs|scala|groovy|feature)$/;
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* A glob as permissive as the ones adopters actually write (`**/*IT.kt`, `*.feature`),
|
|
97
|
-
* anchored at the project root and matched against forward-slash relative paths.
|
|
98
|
-
*
|
|
99
|
-
* Deliberately small: `**` spans separators, `*` does not, `?` is one non-separator
|
|
100
|
-
* character. Anything richer belongs to a glob library, and pulling one in for a config
|
|
101
|
-
* knob this narrow is not worth the dependency.
|
|
102
|
-
*/
|
|
103
|
-
function globToRegExp(glob: string): RegExp {
|
|
104
|
-
let out = "";
|
|
105
|
-
for (let i = 0; i < glob.length; i++) {
|
|
106
|
-
const c = glob[i]!;
|
|
107
|
-
if (c === "*") {
|
|
108
|
-
if (glob[i + 1] === "*") {
|
|
109
|
-
// `**/` may match zero segments, so `**/*.feature` matches a root-level file.
|
|
110
|
-
if (glob[i + 2] === "/") { out += "(?:.*/)?"; i += 2; } else { out += ".*"; i += 1; }
|
|
111
|
-
} else out += "[^/]*";
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
if (c === "?") { out += "[^/]"; continue; }
|
|
115
|
-
out += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
116
|
-
}
|
|
117
|
-
return new RegExp(`^${out}$`);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Markers that a test exists but is disabled, across the same ecosystems. */
|
|
121
|
-
const SKIP_MARKER = new RegExp(
|
|
122
|
-
[
|
|
123
|
-
"\\b(?:it|test|describe)\\.(?:skip|todo)\\b", // jest/vitest/bun
|
|
124
|
-
"\\bx(?:it|test|describe)\\b", // mocha/jasmine
|
|
125
|
-
"@Disabled\\b", // JUnit 5
|
|
126
|
-
"@Ignore\\b", // JUnit 4 / Kotlin
|
|
127
|
-
"@pytest\\.mark\\.skip", // pytest
|
|
128
|
-
"\\[Ignore[\\](]", // MSTest / NUnit
|
|
129
|
-
"\\bSkip\\s*=", // xUnit [Fact(Skip = "...")]
|
|
130
|
-
].join("|"),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
interface TestCorpus {
|
|
134
|
-
files: number;
|
|
135
|
-
/** rel path -> lines, kept so a skip marker can be located near the name. */
|
|
136
|
-
byFile: Map<string, string[]>;
|
|
137
|
-
/** Source files NOT classified as tests, kept only to tell a broken claim from an
|
|
138
|
-
* unknown convention. Paths only — contents are read on demand, on the error path. */
|
|
139
|
-
unclassified: string[];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function walk(
|
|
143
|
-
dir: string,
|
|
144
|
-
root: string,
|
|
145
|
-
acc: TestCorpus,
|
|
146
|
-
isTestFile: (rel: string, base: string) => boolean,
|
|
147
|
-
depth = 0,
|
|
148
|
-
): void {
|
|
149
|
-
if (depth > 12) return; // pathological trees; the scan is advisory, not exhaustive
|
|
150
|
-
let entries;
|
|
151
|
-
try {
|
|
152
|
-
entries = readdirSync(dir, { withFileTypes: true });
|
|
153
|
-
} catch {
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
for (const e of entries) {
|
|
157
|
-
if (e.isDirectory()) {
|
|
158
|
-
if (IGNORE_SEGMENTS.has(e.name) || e.name.startsWith(".")) continue;
|
|
159
|
-
walk(join(dir, e.name), root, acc, isTestFile, depth + 1);
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
if (!e.isFile()) continue;
|
|
163
|
-
const abs = join(dir, e.name);
|
|
164
|
-
const rel = relative(root, abs).split(sep).join("/");
|
|
165
|
-
if (!isTestFile(rel, e.name)) {
|
|
166
|
-
if (SOURCE_FILE.test(e.name) && acc.unclassified.length < 20_000) acc.unclassified.push(rel);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
try {
|
|
170
|
-
if (statSync(abs).size > 512 * 1024) continue;
|
|
171
|
-
acc.byFile.set(rel, readFileSync(abs, "utf8").split("\n"));
|
|
172
|
-
acc.files++;
|
|
173
|
-
} catch {
|
|
174
|
-
/* unreadable file is not a finding */
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** Does this path or body look like a test the corpus definition simply did not match?
|
|
180
|
-
* Deliberately narrow: living under a test directory, or containing an assertion/test
|
|
181
|
-
* declaration. Without this, a name occurring anywhere in PRODUCTION source downgrades a
|
|
182
|
-
* genuinely broken claim to a warning — the exact failure the comment-only check exists
|
|
183
|
-
* to catch. */
|
|
184
|
-
const TESTISH_PATH = /(^|\/)(tests?|spec|__tests__|src\/test)(\/|$)/i;
|
|
185
|
-
const TESTISH_BODY = /\b(assert\w*|expect|should|@Test|def test_|it\(|test\(|describe\()/;
|
|
186
|
-
|
|
187
|
-
/** Test by LOCATION or by CONTENT — either is enough. Production source with a matching
|
|
188
|
-
* name satisfies neither, which is the case that must stay a hard error. */
|
|
189
|
-
function looksLikeTest(rel: string, lines: string[]): boolean {
|
|
190
|
-
return TESTISH_PATH.test(rel) || lines.some((l) => TESTISH_BODY.test(l));
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* Where does this name live, if not in the test corpus?
|
|
195
|
-
*
|
|
196
|
-
* Only ever called on the miss path, so the cost is paid per BROKEN claim rather than
|
|
197
|
-
* per run. Returns the first unclassified source file containing the name, which is
|
|
198
|
-
* enough to tell the author which pattern they are missing.
|
|
199
|
-
*/
|
|
200
|
-
function findOutsideCorpus(name: string, root: string, files: string[]): string | undefined {
|
|
201
|
-
const rx = wordRx(name);
|
|
202
|
-
for (const rel of files) {
|
|
203
|
-
try {
|
|
204
|
-
const abs = join(root, ...rel.split("/"));
|
|
205
|
-
if (statSync(abs).size > 512 * 1024) continue;
|
|
206
|
-
const lines = readFileSync(abs, "utf8").split("\n");
|
|
207
|
-
for (let i = 0; i < lines.length; i++) {
|
|
208
|
-
const line = lines[i] ?? "";
|
|
209
|
-
if (rx.test(line) && !isCommentLine(line, rel) && looksLikeTest(rel, lines)) return rel;
|
|
210
|
-
}
|
|
211
|
-
} catch {
|
|
212
|
-
/* unreadable file is not a finding */
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return undefined;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Every `requirement.*` node in the tree, at any nesting depth. */
|
|
219
|
-
function collect(root: MetaData): MetaRequirement[] {
|
|
220
|
-
const out: MetaRequirement[] = [];
|
|
221
|
-
const rec = (n: MetaData): void => {
|
|
222
|
-
for (const c of n.children()) {
|
|
223
|
-
if (c.type === TYPE_REQUIREMENT) out.push(c as MetaRequirement);
|
|
224
|
-
rec(c);
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
rec(root);
|
|
228
|
-
return out;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* A whole-word match, so `OrderServiceTest` never satisfies a claim naming `Order`.
|
|
233
|
-
*
|
|
234
|
-
* `_` counts as a SEPARATOR, not a word character: pytest's `def test_OrderServiceTest`
|
|
235
|
-
* plainly is the test a claim naming `OrderServiceTest` means, and refusing it would
|
|
236
|
-
* emit the confident false error this scan is built to avoid. Camel-case boundaries
|
|
237
|
-
* stay strict, which is what actually prevents a short name matching a longer one.
|
|
238
|
-
*/
|
|
239
|
-
/**
|
|
240
|
-
* Is this whole line a comment?
|
|
241
|
-
*
|
|
242
|
-
* WHOLE-LINE ONLY, deliberately. Stripping from the first `//` would truncate a code
|
|
243
|
-
* line containing one inside a string — a test titled with a URL is the obvious case —
|
|
244
|
-
* and turn a real match into a confident false error, which is the failure this scan is
|
|
245
|
-
* built to avoid. A trailing comment after code therefore still counts as code; that
|
|
246
|
-
* under-flags, which is the repo's standing bias for drift checks.
|
|
247
|
-
*
|
|
248
|
-
* `#` is Python-only: in TypeScript it opens a private field, not a comment.
|
|
249
|
-
*/
|
|
250
|
-
function isCommentLine(line: string, file: string): boolean {
|
|
251
|
-
const t = line.trimStart();
|
|
252
|
-
if (t.startsWith("//") || t.startsWith("/*") || t.startsWith("*")) return true;
|
|
253
|
-
return file.endsWith(".py") && t.startsWith("#");
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function wordRx(name: string): RegExp {
|
|
257
|
-
return new RegExp(`(?:^|[^A-Za-z0-9])${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?![A-Za-z0-9])`);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Check every `@verifiedBy` name against the project's test corpus.
|
|
262
|
-
*
|
|
263
|
-
* Severity mirrors `@implementedBy`: a broken claim is an ERROR on `live`/`partial`
|
|
264
|
-
* and silent on `abandoned`/`superseded`, because a retired requirement naming a
|
|
265
|
-
* deleted test is the entry doing its job, not drift.
|
|
266
|
-
*/
|
|
267
|
-
export function checkVerifiedBy(
|
|
268
|
-
root: MetaData,
|
|
269
|
-
cwd: string,
|
|
270
|
-
testFiles?: string[],
|
|
271
|
-
): VerifiedByDiagnostic[] {
|
|
272
|
-
const reqs = collect(root).filter((r) => r.verifiedBy().length > 0);
|
|
273
|
-
if (reqs.length === 0) return []; // opt-in by declaration
|
|
274
|
-
|
|
275
|
-
// Project-declared conventions ADD to the built-ins: the failure being fixed is
|
|
276
|
-
// under-matching, and a project that names an extra convention is telling us
|
|
277
|
-
// something we did not know — not asking us to forget what we did.
|
|
278
|
-
const declared = (testFiles ?? []).map(globToRegExp);
|
|
279
|
-
const isTestFile = (rel: string, base: string): boolean =>
|
|
280
|
-
TEST_FILE.test(base) || declared.some((rx) => rx.test(rel));
|
|
281
|
-
|
|
282
|
-
const corpus: TestCorpus = { files: 0, byFile: new Map(), unclassified: [] };
|
|
283
|
-
walk(cwd, cwd, corpus, isTestFile);
|
|
284
|
-
if (corpus.files === 0) return []; // fail open: nothing to judge against
|
|
285
|
-
|
|
286
|
-
const out: VerifiedByDiagnostic[] = [];
|
|
287
|
-
for (const req of reqs) {
|
|
288
|
-
for (const test of req.verifiedBy()) {
|
|
289
|
-
const rx = wordRx(test);
|
|
290
|
-
let foundIn: string | undefined;
|
|
291
|
-
let skippedAt: string | undefined;
|
|
292
|
-
// #293-adjacent (the `verifiedBy` audit): a name that occurs ONLY in comments
|
|
293
|
-
// satisfied this scan, because the match is line-agnostic. That is how a claim
|
|
294
|
-
// came to name `mountCrudRoutes`, whose single occurrence in an entire corpus was
|
|
295
|
-
// inside a `// via mountCrudRoutes(...)` note. Tracked separately so the name can
|
|
296
|
-
// still be reported as found (it is) while saying what it was found in.
|
|
297
|
-
let commentOnlyAt: string | undefined;
|
|
298
|
-
for (const [file, lines] of corpus.byFile) {
|
|
299
|
-
for (let i = 0; i < lines.length; i++) {
|
|
300
|
-
if (!rx.test(lines[i] ?? "")) continue;
|
|
301
|
-
if (isCommentLine(lines[i] ?? "", file)) {
|
|
302
|
-
commentOnlyAt ??= `${file}:${i + 1}`;
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
foundIn ??= file;
|
|
306
|
-
// a decorator/annotation sits above the declaration it disables
|
|
307
|
-
const window = lines.slice(Math.max(0, i - 3), i + 1).join("\n");
|
|
308
|
-
if (SKIP_MARKER.test(window)) skippedAt ??= `${file}:${i + 1}`;
|
|
309
|
-
}
|
|
310
|
-
if (foundIn !== undefined && skippedAt !== undefined) break;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// Found, but only ever in prose. Not an error — the scan's job is to catch a name
|
|
314
|
-
// that has gone missing, and this one has not — but a comment proves nothing, so
|
|
315
|
-
// the claim is reported rather than silently accepted.
|
|
316
|
-
if (foundIn === undefined && commentOnlyAt !== undefined) {
|
|
317
|
-
out.push({
|
|
318
|
-
severity: "warn",
|
|
319
|
-
code: WARN_REQUIREMENT_TEST_COMMENT_ONLY,
|
|
320
|
-
name: req.name,
|
|
321
|
-
message:
|
|
322
|
-
`'verifiedBy' names '${test}', which appears only in a comment (${commentOnlyAt}) ` +
|
|
323
|
-
`and in no test declaration. A comment proves nothing — name the test that asserts it.`,
|
|
324
|
-
});
|
|
325
|
-
continue;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
if (foundIn === undefined) {
|
|
329
|
-
if (req.requiresLiveNodes()) {
|
|
330
|
-
// Before calling a claim broken, rule out the likelier explanation: that this
|
|
331
|
-
// project names its tests in a way the corpus definition does not know. A name
|
|
332
|
-
// sitting in an unclassified source file is OUR ignorance, and saying "the claim
|
|
333
|
-
// was never true" about it is the tool being confidently wrong.
|
|
334
|
-
const elsewhere = findOutsideCorpus(test, cwd, corpus.unclassified);
|
|
335
|
-
out.push(
|
|
336
|
-
elsewhere !== undefined
|
|
337
|
-
? {
|
|
338
|
-
severity: "warn",
|
|
339
|
-
code: WARN_REQUIREMENT_TEST_UNCLASSIFIED,
|
|
340
|
-
name: req.name,
|
|
341
|
-
message:
|
|
342
|
-
`'verifiedBy' names '${test}', which is not in any of the ${corpus.files} ` +
|
|
343
|
-
`file(s) recognised as tests, but DOES appear in ${elsewhere}. That file is ` +
|
|
344
|
-
`probably a test this scan does not know how to recognise — declare the ` +
|
|
345
|
-
`convention in metaobjects.config.ts (verify.testFiles, e.g. ` +
|
|
346
|
-
`["**/*IT.kt"]) and this becomes a real check instead of a guess.`,
|
|
347
|
-
}
|
|
348
|
-
: {
|
|
349
|
-
severity: "error",
|
|
350
|
-
code: ERR_REQUIREMENT_TEST_MISSING,
|
|
351
|
-
name: req.name,
|
|
352
|
-
message:
|
|
353
|
-
`'verifiedBy' names '${test}', which appears in none of the ` +
|
|
354
|
-
`${corpus.files} test file(s) found under this project, and in no other ` +
|
|
355
|
-
`source file either. Either the test was renamed or removed, or the ` +
|
|
356
|
-
`claim was never true.`,
|
|
357
|
-
},
|
|
358
|
-
);
|
|
359
|
-
}
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (skippedAt !== undefined) {
|
|
363
|
-
out.push({
|
|
364
|
-
severity: "warn",
|
|
365
|
-
code: WARN_REQUIREMENT_TEST_SKIPPED,
|
|
366
|
-
name: req.name,
|
|
367
|
-
message:
|
|
368
|
-
`'verifiedBy' names '${test}', but it is disabled at ${skippedAt}. ` +
|
|
369
|
-
`A skipped test proves nothing — the requirement reads as verified and is not.`,
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return out;
|
|
375
|
-
}
|