@descent-vtt/spec-guard 0.6.0 → 0.7.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/CHANGELOG.md +75 -8
- package/README.md +86 -1
- package/dist/cli.d.ts +17 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +126 -7
- package/dist/cli.js.map +1 -1
- package/dist/glob.d.ts +17 -2
- package/dist/glob.d.ts.map +1 -1
- package/dist/glob.js +25 -9
- package/dist/glob.js.map +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +117 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +544 -0
- package/dist/mcp.js.map +1 -0
- package/dist/parser.d.ts +25 -0
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +54 -15
- package/dist/parser.js.map +1 -1
- package/dist/query.d.ts +108 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +206 -0
- package/dist/query.js.map +1 -0
- package/dist/rules.d.ts +108 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +153 -0
- package/dist/rules.js.map +1 -0
- package/dist/runner.d.ts +16 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +108 -88
- package/dist/runner.js.map +1 -1
- package/dist/specs.d.ts +44 -0
- package/dist/specs.d.ts.map +1 -0
- package/dist/specs.js +62 -0
- package/dist/specs.js.map +1 -0
- package/package.json +1 -1
package/dist/rules.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which rules govern a path, decided from the specs alone.
|
|
3
|
+
*
|
|
4
|
+
* Pure. Nothing here reads the codebase: whether an assertion's scope reaches a
|
|
5
|
+
* path is arithmetic on the assertion's own `target`, `exclude` and `glob`, the
|
|
6
|
+
* run's skipped directory names, and the path's extension. That is the whole
|
|
7
|
+
* reason a query can answer in milliseconds - and it is also a claim, that the
|
|
8
|
+
* arithmetic agrees with the walk the assertion would really make. The suite
|
|
9
|
+
* holds it to that claim by comparing the two over real trees; ADR-0012 lists
|
|
10
|
+
* the three things arithmetic cannot see.
|
|
11
|
+
*
|
|
12
|
+
* A file is governed or it is not. A directory is governed when some file under
|
|
13
|
+
* it could be, and "could" is doing work: whether `glob="*.py"` matches anything
|
|
14
|
+
* in `src/` is a question about files that are not read here. The rule is shown
|
|
15
|
+
* with its filters rather than being guessed away.
|
|
16
|
+
*/
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { createExcludeMatcher, createGlobMatcher } from './glob.js';
|
|
19
|
+
import { isGraphFile } from './graph.js';
|
|
20
|
+
import { ANALYSABLE_EXTENSIONS } from './imports.js';
|
|
21
|
+
import { layerMatcher } from './layers.js';
|
|
22
|
+
/** Whether `child` is `parent` or lies under it. */
|
|
23
|
+
export function within(child, parent) {
|
|
24
|
+
return parent === '.' || child === parent || child.startsWith(`${parent}/`);
|
|
25
|
+
}
|
|
26
|
+
/** Whether the file-reading part of an assertion can read this file at all. */
|
|
27
|
+
function readable(kind, relativePath) {
|
|
28
|
+
if (kind === 'assert-import-cycle')
|
|
29
|
+
return isGraphFile(relativePath);
|
|
30
|
+
if (kind === 'assert-import-absence' || kind === 'assert-import-count' || kind === 'assert-layers') {
|
|
31
|
+
return ANALYSABLE_EXTENSIONS.has(path.posix.extname(relativePath));
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether the walk from `target` would step into a skipped directory on its way
|
|
37
|
+
* to `relativePath`.
|
|
38
|
+
*
|
|
39
|
+
* Only the directories below the target count, which is how the walk behaves: it
|
|
40
|
+
* never checks the name of the directory it was started in, so
|
|
41
|
+
* `target="node_modules/pkg"` is searched and `target="src"` does not reach
|
|
42
|
+
* `src/node_modules`.
|
|
43
|
+
*/
|
|
44
|
+
function skippedOnTheWay(target, relativePath, options, isFile) {
|
|
45
|
+
// A target that is the path itself leaves this empty, and it splits into one
|
|
46
|
+
// empty name - which no skipped directory has.
|
|
47
|
+
const directories = path.posix.relative(target, relativePath).split('/');
|
|
48
|
+
if (isFile)
|
|
49
|
+
directories.pop();
|
|
50
|
+
return directories.some((name) => options.scope.skippedDirectories.has(name));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Whether an assertion's scope reaches a path.
|
|
54
|
+
*
|
|
55
|
+
* For a file, exactly what the walk would do: under a target, not excluded, not
|
|
56
|
+
* inside a skipped directory, matched by `glob`, not one of the spec files, and
|
|
57
|
+
* in a language the assertion reads. For a directory, whether any of that could
|
|
58
|
+
* be true of something under it.
|
|
59
|
+
*/
|
|
60
|
+
export function governs(assertion, query) {
|
|
61
|
+
if (assertion.kind === 'assert-present') {
|
|
62
|
+
return assertion.files.some((file) => (query.shape === 'file' ? file === query.path : within(file, query.path)));
|
|
63
|
+
}
|
|
64
|
+
const options = assertion.search;
|
|
65
|
+
const excluded = createExcludeMatcher(options.excludeGlobs);
|
|
66
|
+
if (query.shape === 'file') {
|
|
67
|
+
return (assertion.targets.some((target) => within(query.path, target) && !skippedOnTheWay(target, query.path, options, true)) &&
|
|
68
|
+
!excluded(query.path) &&
|
|
69
|
+
createGlobMatcher(options.globs)(query.path) &&
|
|
70
|
+
!options.excludeFiles.has(query.absolutePath) &&
|
|
71
|
+
readable(assertion.kind, query.path));
|
|
72
|
+
}
|
|
73
|
+
return assertion.targets.some((target) => {
|
|
74
|
+
// The directory is inside the target: everything under it is walked unless
|
|
75
|
+
// the directory itself is skipped or excluded - and an excluded directory
|
|
76
|
+
// excludes all of its contents, because `exclude` tests every ancestor.
|
|
77
|
+
if (within(query.path, target)) {
|
|
78
|
+
return !skippedOnTheWay(target, query.path, options, false) && !excluded(query.path);
|
|
79
|
+
}
|
|
80
|
+
// The target is inside the directory: its files are under the directory.
|
|
81
|
+
return within(target, query.path) && !excluded(target);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Where a path sits in a layer order.
|
|
86
|
+
*
|
|
87
|
+
* For a directory, the layer that matches the directory itself - which then
|
|
88
|
+
* matches every file under it, since a layer is an `exclude`-style pattern and
|
|
89
|
+
* those test every ancestor. A directory no layer matches may still hold files
|
|
90
|
+
* that one does, and is reported as belonging to none rather than guessed at.
|
|
91
|
+
*/
|
|
92
|
+
export function layerPosition(order, relativePath) {
|
|
93
|
+
const indices = layerMatcher(order)(relativePath);
|
|
94
|
+
const matches = indices.map((index) => order[index]);
|
|
95
|
+
if (indices.length !== 1)
|
|
96
|
+
return { layer: null, position: null, matches, mayImport: [], mustNotImport: [] };
|
|
97
|
+
const index = indices[0];
|
|
98
|
+
return {
|
|
99
|
+
layer: order[index],
|
|
100
|
+
position: index + 1,
|
|
101
|
+
matches,
|
|
102
|
+
mayImport: order.slice(0, index + 1),
|
|
103
|
+
mustNotImport: order.slice(index + 1),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A rule as a report shows it, optionally as it applies to one path.
|
|
108
|
+
*
|
|
109
|
+
* Every field is what the directive said, resolved - never a default invented
|
|
110
|
+
* for display. A text rule reports its `glob` even when empty, because "every
|
|
111
|
+
* file" is a statement about scope a reader should not have to infer.
|
|
112
|
+
*/
|
|
113
|
+
export function viewRule(assertion, document, query) {
|
|
114
|
+
const view = {
|
|
115
|
+
document: document.file,
|
|
116
|
+
line: assertion.location.line,
|
|
117
|
+
kind: assertion.kind,
|
|
118
|
+
description: assertion.description,
|
|
119
|
+
reason: assertion.reason ?? null,
|
|
120
|
+
inForce: document.inForce,
|
|
121
|
+
bounds: assertion.bounds,
|
|
122
|
+
};
|
|
123
|
+
if (assertion.kind === 'assert-present')
|
|
124
|
+
return { ...view, files: assertion.files };
|
|
125
|
+
const options = assertion.search;
|
|
126
|
+
const baseline = query ? assertion.baseline.filter((entry) => within(entry.path, query.path)) : assertion.baseline;
|
|
127
|
+
const scoped = { ...view, targets: assertion.targets, exclude: options.excludeGlobs };
|
|
128
|
+
if (assertion.imports === undefined) {
|
|
129
|
+
return {
|
|
130
|
+
...scoped,
|
|
131
|
+
glob: options.globs,
|
|
132
|
+
symbol: assertion.symbol,
|
|
133
|
+
regex: options.regex,
|
|
134
|
+
word: options.word,
|
|
135
|
+
ignoreCase: options.ignoreCase,
|
|
136
|
+
comments: options.ignoreComments ? 'ignore' : 'include',
|
|
137
|
+
baseline,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
const types = assertion.imports.includeTypes ? 'include' : 'ignore';
|
|
141
|
+
if (assertion.kind === 'assert-import-cycle')
|
|
142
|
+
return { ...scoped, types };
|
|
143
|
+
if (assertion.layers === undefined)
|
|
144
|
+
return { ...scoped, modules: assertion.imports.modules, types, baseline };
|
|
145
|
+
return {
|
|
146
|
+
...scoped,
|
|
147
|
+
order: assertion.layers,
|
|
148
|
+
types,
|
|
149
|
+
baseline,
|
|
150
|
+
...(query ? { position: layerPosition(assertion.layers, query.path) } : {}),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=rules.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rules.js","sourceRoot":"","sources":["../src/rules.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACpE,OAAO,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACzC,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAc3C,oDAAoD;AACpD,MAAM,UAAU,MAAM,CAAC,KAAa,EAAE,MAAc;IAClD,OAAO,MAAM,KAAK,GAAG,IAAI,KAAK,KAAK,MAAM,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,MAAM,GAAG,CAAC,CAAC;AAC9E,CAAC;AAED,+EAA+E;AAC/E,SAAS,QAAQ,CAAC,IAAmB,EAAE,YAAoB;IACzD,IAAI,IAAI,KAAK,qBAAqB;QAAE,OAAO,WAAW,CAAC,YAAY,CAAC,CAAC;IACrE,IAAI,IAAI,KAAK,uBAAuB,IAAI,IAAI,KAAK,qBAAqB,IAAI,IAAI,KAAK,eAAe,EAAE,CAAC;QACnG,OAAO,qBAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC;IACrE,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,eAAe,CAAC,MAAc,EAAE,YAAoB,EAAE,OAAsB,EAAE,MAAe;IACpG,6EAA6E;IAC7E,+CAA+C;IAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACzE,IAAI,MAAM;QAAE,WAAW,CAAC,GAAG,EAAE,CAAC;IAC9B,OAAO,WAAW,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,OAAO,CAAC,SAAoB,EAAE,KAAgB;IAC5D,IAAI,SAAS,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;QACxC,OAAO,SAAS,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACnH,CAAC;IAED,MAAM,OAAO,GAAG,SAAS,CAAC,MAAuB,CAAC;IAClD,MAAM,QAAQ,GAAG,oBAAoB,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC;IAE5D,IAAI,KAAK,CAAC,KAAK,KAAK,MAAM,EAAE,CAAC;QAC3B,OAAO,CACL,SAAS,CAAC,OAAO,CAAC,IAAI,CACpB,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,CAC9F;YACD,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC;YACrB,iBAAiB,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC;YAC5C,CAAC,OAAO,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,YAAY,CAAC;YAC7C,QAAQ,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CACrC,CAAC;IACJ,CAAC;IAED,OAAO,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;QACvC,2EAA2E;QAC3E,0EAA0E;QAC1E,wEAAwE;QACxE,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;YAC/B,OAAO,CAAC,eAAe,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACvF,CAAC;QACD,yEAAyE;QACzE,OAAO,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC,CAAC,CAAC;AACL,CAAC;AA4DD;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,KAAwB,EAAE,YAAoB;IAC1E,MAAM,OAAO,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,YAAY,CAAC,CAAC;IAClD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAW,CAAC,CAAC;IAC/D,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE,CAAC;IAC5G,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAW,CAAC;IACnC,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,KAAK,CAAW;QAC7B,QAAQ,EAAE,KAAK,GAAG,CAAC;QACnB,OAAO;QACP,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC;QACpC,aAAa,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC;KACtC,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,QAAQ,CAAC,SAAoB,EAAE,QAAsB,EAAE,KAAiB;IACtF,MAAM,IAAI,GAAa;QACrB,QAAQ,EAAE,QAAQ,CAAC,IAAI;QACvB,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,IAAI;QAC7B,IAAI,EAAE,SAAS,CAAC,IAAI;QACpB,WAAW,EAAE,SAAS,CAAC,WAAW;QAClC,MAAM,EAAE,SAAS,CAAC,MAAM,IAAI,IAAI;QAChC,OAAO,EAAE,QAAQ,CAAC,OAAO;QACzB,MAAM,EAAE,SAAS,CAAC,MAAM;KACzB,CAAC;IAEF,IAAI,SAAS,CAAC,IAAI,KAAK,gBAAgB;QAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,KAAK,EAAE,CAAC;IAEpF,MAAM,OAAO,GAAG,SAAS,CAAC,MAAuB,CAAC;IAClD,MAAM,QAAQ,GAAG,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC;IACnH,MAAM,MAAM,GAAa,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,YAAY,EAAE,CAAC;IAEhG,IAAI,SAAS,CAAC,OAAO,KAAK,SAAS,EAAE,CAAC;QACpC,OAAO;YACL,GAAG,MAAM;YACT,IAAI,EAAE,OAAO,CAAC,KAAK;YACnB,MAAM,EAAE,SAAS,CAAC,MAAgB;YAClC,KAAK,EAAE,OAAO,CAAC,KAAK;YACpB,IAAI,EAAE,OAAO,CAAC,IAAI;YAClB,UAAU,EAAE,OAAO,CAAC,UAAU;YAC9B,QAAQ,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS;YACvD,QAAQ;SACT,CAAC;IACJ,CAAC;IAED,MAAM,KAAK,GAAG,SAAS,CAAC,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;IACpE,IAAI,SAAS,CAAC,IAAI,KAAK,qBAAqB;QAAE,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAC;IAC1E,IAAI,SAAS,CAAC,MAAM,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;IAC9G,OAAO;QACL,GAAG,MAAM;QACT,KAAK,EAAE,SAAS,CAAC,MAAM;QACvB,KAAK;QACL,QAAQ;QACR,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5E,CAAC;AACJ,CAAC"}
|
package/dist/runner.d.ts
CHANGED
|
@@ -57,6 +57,15 @@ export interface RunOptions {
|
|
|
57
57
|
* default, because the point of reading the status is to honour it.
|
|
58
58
|
*/
|
|
59
59
|
ignoreStatus?: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Which resolved assertions to execute. Every one, when unset.
|
|
62
|
+
*
|
|
63
|
+
* For a caller that wants the rules governing a few paths rather than all of
|
|
64
|
+
* them - the MCP server's `check_architecture`. An assertion that is not
|
|
65
|
+
* selected is not executed and not counted; it is still resolved, so a
|
|
66
|
+
* malformed directive is an error whatever is selected.
|
|
67
|
+
*/
|
|
68
|
+
select?: (assertion: Assertion) => boolean;
|
|
60
69
|
}
|
|
61
70
|
export interface RunResult extends RunReport {
|
|
62
71
|
/** Spec files that were executed, relative to root. */
|
|
@@ -107,6 +116,13 @@ export declare function resolveDirective(directive: Directive, context: ResolveC
|
|
|
107
116
|
} | {
|
|
108
117
|
error: DirectiveError;
|
|
109
118
|
};
|
|
119
|
+
/**
|
|
120
|
+
* The spec files a search must not count, as absolute paths.
|
|
121
|
+
*
|
|
122
|
+
* A spec names the symbol it forbids, so without this every absence rule would
|
|
123
|
+
* find itself. Empty under `--include-specs`.
|
|
124
|
+
*/
|
|
125
|
+
export declare function specExclusions(specFiles: readonly string[], includeSpecs: boolean): ReadonlySet<string>;
|
|
110
126
|
export interface ExecuteOptions {
|
|
111
127
|
root: string;
|
|
112
128
|
engine: Engine;
|
package/dist/runner.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EASL,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAWrB,OAAO,EAIL,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,aAAa,EAEb,SAAS,EACT,cAAc,EAId,SAAS,
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../src/runner.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAKH,OAAO,EASL,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,aAAa,EACnB,MAAM,aAAa,CAAC;AAWrB,OAAO,EAIL,KAAK,WAAW,EAEjB,MAAM,cAAc,CAAC;AAGtB,OAAO,EAIL,KAAK,WAAW,EACjB,MAAM,YAAY,CAAC;AACpB,OAAO,KAAK,EACV,SAAS,EACT,eAAe,EACf,aAAa,EAEb,SAAS,EACT,cAAc,EAId,SAAS,EAIT,kBAAkB,EACnB,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,oBAAoB,IAAI,CAAC;AACtC,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,MAAM,WAAW,UAAU;IACzB,8DAA8D;IAC9D,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5B,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;OAKG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,+DAA+D;IAC/D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,uEAAuE;IACvE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,iCAAiC;IACjC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC;CAC5C;AAED,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,uDAAuD;IACvD,SAAS,EAAE,MAAM,EAAE,CAAC;CACrB;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAE,MAA0B,GAAG,MAAM,CAE5E;AAyND;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,SAAS,aAAa,EAAE,EACjC,UAAU,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,GACtC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,kBAAkB,EAAE,CAAC;IAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAA;CAAE,CAsBrF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAoB/E;AAiBD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAClC,oEAAoE;IACpE,KAAK,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,qEAAqE;AACrE,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,cAAc,GACtB;IAAE,SAAS,EAAE,SAAS,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,cAAc,CAAA;CAAE,CAyPtD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,EAAE,YAAY,EAAE,OAAO,GAAG,WAAW,CAAC,MAAM,CAAC,CAEvG;AAMD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,mBAAmB,EAAE,OAAO,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,OAAO,CAAC;IACzB,WAAW,EAAE,MAAM,CAAC;IACpB,sDAAsD;IACtD,OAAO,EAAE,WAAW,CAAC;IACrB,2DAA2D;IAC3D,QAAQ,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACxD;AA6iBD,4CAA4C;AAC5C,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,SAAS,EACpB,OAAO,EAAE,cAAc,GACtB,OAAO,CAAC,eAAe,CAAC,CAI1B;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,gFAAgF;AAChF,wBAAsB,YAAY,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,SAAS,CAAC,CAyK1E"}
|
package/dist/runner.js
CHANGED
|
@@ -8,11 +8,11 @@
|
|
|
8
8
|
import { promises as fs } from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { comparePaths, createCachedEngine, enumerateCandidates, passKey, resolveEngine, runSearches, ANY_FILE_PROBE, ROOT_TARGETS, } from "./engine.js";
|
|
11
|
-
import { createExcludeMatcher,
|
|
11
|
+
import { createExcludeMatcher, toPosix } from "./glob.js";
|
|
12
12
|
import { buildGraph, cyclicComponents, edgeKey, isGraphFile, witness, } from "./graph.js";
|
|
13
13
|
import { ANALYSABLE_EXTENSIONS, createImportIndex, resolveModule, } from "./imports.js";
|
|
14
14
|
import { checkLayers } from "./layers.js";
|
|
15
|
-
import {
|
|
15
|
+
import { readSpecs, specPath } from "./specs.js";
|
|
16
16
|
import { createScope, DEFAULT_SCOPE, EMPTY_LEDGER, } from "./scope.js";
|
|
17
17
|
export const DEFAULT_MAX_SNIPPETS = 5;
|
|
18
18
|
export const DEFAULT_CONCURRENCY = 8;
|
|
@@ -477,6 +477,15 @@ export function resolveDirective(directive, context) {
|
|
|
477
477
|
return fail(error instanceof Error ? error.message : String(error));
|
|
478
478
|
}
|
|
479
479
|
}
|
|
480
|
+
/**
|
|
481
|
+
* The spec files a search must not count, as absolute paths.
|
|
482
|
+
*
|
|
483
|
+
* A spec names the symbol it forbids, so without this every absence rule would
|
|
484
|
+
* find itself. Empty under `--include-specs`.
|
|
485
|
+
*/
|
|
486
|
+
export function specExclusions(specFiles, includeSpecs) {
|
|
487
|
+
return new Set(includeSpecs ? undefined : specFiles.map((file) => path.resolve(file)));
|
|
488
|
+
}
|
|
480
489
|
async function pathExists(candidate) {
|
|
481
490
|
return (await fs.stat(candidate).catch(() => null)) !== null;
|
|
482
491
|
}
|
|
@@ -551,61 +560,71 @@ async function prepareAssertion(assertion, options) {
|
|
|
551
560
|
targets: existingTargets,
|
|
552
561
|
options: assertion.search,
|
|
553
562
|
};
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
563
|
+
// Every target missing, and that tolerated, leaves nothing to search - and
|
|
564
|
+
// must not be handed to the engine as an empty target list, which the engine
|
|
565
|
+
// reads, by contract, as the root. It used to be: a rule on a deleted
|
|
566
|
+
// `src/auth` searched the whole repository, where `min="1"` could pass on a
|
|
567
|
+
// test file. Found by holding `spec-guard query` to the walk; see ADR-0012.
|
|
568
|
+
const nothingLeft = existingTargets.length === 0;
|
|
569
|
+
if (!options.allowEmptyScope && !assertion.allowEmpty && (nothingLeft || !(await options.hasFiles(request)))) {
|
|
570
|
+
return {
|
|
571
|
+
...base,
|
|
572
|
+
ok: false,
|
|
573
|
+
actual: 0,
|
|
574
|
+
message: `no files were inspected, so this assertion verified nothing (${EMPTY_SCOPE_HINT})`,
|
|
575
|
+
matches: [],
|
|
576
|
+
durationMs: elapsed(startedAt),
|
|
577
|
+
};
|
|
565
578
|
}
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
durationMs: elapsed(startedAt),
|
|
605
|
-
};
|
|
606
|
-
},
|
|
579
|
+
const finish = (search) => {
|
|
580
|
+
// A file that could not be read, or whose bytes are not text but did
|
|
581
|
+
// contain the symbol, is a hole in the answer rather than a detail of it.
|
|
582
|
+
// The count is still reported, because it is still true of everything
|
|
583
|
+
// that was read; --strict is for runs where "true of what we read" is not
|
|
584
|
+
// good enough.
|
|
585
|
+
//
|
|
586
|
+
// No filter for uncertainty here any more. The ledger only ever holds
|
|
587
|
+
// gaps: the walk drops a policy skip before recording it, and the only
|
|
588
|
+
// other reasons the scanner adds are "unreadable" and "binary". Filtering
|
|
589
|
+
// again was a second statement of an invariant that is enforced where it
|
|
590
|
+
// is created, and being a no-op, nothing could tell it from its absence.
|
|
591
|
+
const gaps = search.scope.skipped;
|
|
592
|
+
const strictFailure = options.strictTargets && gaps.length > 0;
|
|
593
|
+
const { excluded, stale, shows } = applyBaseline(assertion.baseline, search.fileCounts);
|
|
594
|
+
const staleFailure = assertion.ratchet === "two-sided" && stale.length > 0;
|
|
595
|
+
const actual = search.count - excluded;
|
|
596
|
+
return {
|
|
597
|
+
...base,
|
|
598
|
+
ok: satisfies(actual, assertion.bounds) && !strictFailure && !staleFailure,
|
|
599
|
+
actual,
|
|
600
|
+
message: describeOutcome(assertion.bounds, actual, {
|
|
601
|
+
excluded,
|
|
602
|
+
stale: staleFailure ? stale : [],
|
|
603
|
+
gaps: strictFailure ? gaps.length : 0,
|
|
604
|
+
}),
|
|
605
|
+
baselinedMatches: excluded,
|
|
606
|
+
staleBaseline: stale,
|
|
607
|
+
fileMatches: [...search.fileCounts].map(([file, count]) => ({ file, count })),
|
|
608
|
+
matches: search.matches.filter((match) => shows(match.file)).slice(0, options.maxSnippets),
|
|
609
|
+
// Carried as numbers, not prose, so the reporter can total them across a
|
|
610
|
+
// run and JSON consumers can act on them.
|
|
611
|
+
commentMatches: search.commentMatches,
|
|
612
|
+
unclassifiedFiles: search.unclassifiedFiles,
|
|
613
|
+
scope: search.scope,
|
|
614
|
+
engine: search.engine,
|
|
615
|
+
durationMs: elapsed(startedAt),
|
|
616
|
+
};
|
|
607
617
|
};
|
|
618
|
+
return nothingLeft ? finish(NOTHING_SEARCHED) : { request, finish };
|
|
608
619
|
}
|
|
620
|
+
const NOTHING_SEARCHED = {
|
|
621
|
+
count: 0,
|
|
622
|
+
commentMatches: 0,
|
|
623
|
+
unclassifiedFiles: 0,
|
|
624
|
+
matches: [],
|
|
625
|
+
fileCounts: new Map(),
|
|
626
|
+
scope: EMPTY_LEDGER,
|
|
627
|
+
};
|
|
609
628
|
/** How each kind of reference is spelled in a report snippet. */
|
|
610
629
|
const IMPORT_VERBS = {
|
|
611
630
|
import: "import",
|
|
@@ -637,11 +656,22 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
|
|
|
637
656
|
// No symbol: an import assertion never runs a text search, and the walk does
|
|
638
657
|
// not depend on one. This used to pass `symbol: ""`, an invented value that
|
|
639
658
|
// reached nothing and that no test could therefore be wrong about.
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
659
|
+
//
|
|
660
|
+
// And no walk at all when every target is missing: an empty target list is
|
|
661
|
+
// the root to the engine, as the text rules above found out.
|
|
662
|
+
const enumeration = existingTargets.length === 0
|
|
663
|
+
? { files: [] }
|
|
664
|
+
: await enumerateCandidates({
|
|
665
|
+
root: options.root,
|
|
666
|
+
targets: existingTargets,
|
|
667
|
+
options: assertion.search,
|
|
668
|
+
});
|
|
669
|
+
// Why a missing target fails the assertion, or null when it does not. Decided
|
|
670
|
+
// before scope, so a rule whose only target is gone says so rather than that
|
|
671
|
+
// its scope is empty - which is true, and not the thing to fix.
|
|
672
|
+
const missing = !options.allowMissingTargets && assertion.missingTargets.length > 0
|
|
673
|
+
? `target path${assertion.missingTargets.length === 1 ? " does" : "s do"} not exist: ${assertion.missingTargets.join(", ")}`
|
|
674
|
+
: null;
|
|
645
675
|
// A cycle needs files the resolver can name, which today means JavaScript
|
|
646
676
|
// and TypeScript; every other import rule reads any language spec-guard
|
|
647
677
|
// tokenizes. ADR-0011 has the reasons, language by language.
|
|
@@ -649,7 +679,7 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
|
|
|
649
679
|
const analysable = enumeration.files.filter((file) => cycles
|
|
650
680
|
? isGraphFile(file.relativePath)
|
|
651
681
|
: ANALYSABLE_EXTENSIONS.has(path.posix.extname(file.relativePath)));
|
|
652
|
-
if (analysable.length === 0 && !options.allowEmptyScope && !assertion.allowEmpty) {
|
|
682
|
+
if (analysable.length === 0 && missing === null && !options.allowEmptyScope && !assertion.allowEmpty) {
|
|
653
683
|
// For an import rule the bar is higher than "some file exists": a directory
|
|
654
684
|
// of YAML has nothing this can read, so a dependency claim about it is a
|
|
655
685
|
// claim about nothing.
|
|
@@ -689,9 +719,7 @@ async function executeImportAssertion(assertion, options, base, warnings, starte
|
|
|
689
719
|
walked: enumeration.files.map((file) => file.relativePath),
|
|
690
720
|
targets: existingTargets,
|
|
691
721
|
unresolved: unresolved.length,
|
|
692
|
-
missing
|
|
693
|
-
? `target path${assertion.missingTargets.length === 1 ? " does" : "s do"} not exist: ${assertion.missingTargets.join(", ")}`
|
|
694
|
-
: null,
|
|
722
|
+
missing,
|
|
695
723
|
};
|
|
696
724
|
if (cycles)
|
|
697
725
|
return finishCycles(assertion, options, base, warnings, startedAt, read);
|
|
@@ -891,50 +919,42 @@ export async function runSpecGuard(options) {
|
|
|
891
919
|
const allowMissingTargets = options.allowMissingTargets ?? false;
|
|
892
920
|
const allowEmptyScope = options.allowEmptyScope ?? false;
|
|
893
921
|
const scope = createScope(options.defaultSkips ?? true);
|
|
894
|
-
const
|
|
895
|
-
const
|
|
922
|
+
const specs = await readSpecs(options.patterns, root);
|
|
923
|
+
const specFiles = specs.files;
|
|
924
|
+
const excludeFiles = specExclusions(specFiles, options.includeSpecs ?? false);
|
|
896
925
|
const directives = [];
|
|
897
926
|
/** Parsed, validated, and then not run: see ADR-0010. */
|
|
898
927
|
const withheld = [];
|
|
899
|
-
const errors = [];
|
|
928
|
+
const errors = [...specs.errors];
|
|
900
929
|
const inactiveSpecs = [];
|
|
901
|
-
for (const
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
raw: "",
|
|
907
|
-
message: `Unable to read spec file: ${error instanceof Error ? error.message : String(error)}`,
|
|
908
|
-
});
|
|
909
|
-
return null;
|
|
910
|
-
});
|
|
911
|
-
if (source === null)
|
|
912
|
-
continue;
|
|
913
|
-
const parsed = parseDirectives(source, { file, relativeFile });
|
|
914
|
-
errors.push(...parsed.errors);
|
|
915
|
-
const status = parsed.status;
|
|
916
|
-
if (status && !status.active && !(options.ignoreStatus ?? false)) {
|
|
917
|
-
withheld.push(...parsed.directives);
|
|
930
|
+
for (const document of specs.documents) {
|
|
931
|
+
if (!document.inForce && !(options.ignoreStatus ?? false)) {
|
|
932
|
+
// A document is only ever out of force because of a status it declares.
|
|
933
|
+
const status = document.status;
|
|
934
|
+
withheld.push(...document.directives);
|
|
918
935
|
// Recorded even when it held no directives. "docs/adr/0011.md is a
|
|
919
936
|
// draft" is worth saying to someone wondering why their new rule has no
|
|
920
937
|
// effect, and a report that only mentions the documents it happened to
|
|
921
938
|
// find directives in cannot answer that.
|
|
922
939
|
inactiveSpecs.push({
|
|
923
|
-
file: relativeFile,
|
|
940
|
+
file: document.relativeFile,
|
|
924
941
|
status: status.value,
|
|
925
942
|
label: status.label,
|
|
926
|
-
directives:
|
|
943
|
+
directives: document.directives.length,
|
|
927
944
|
});
|
|
928
945
|
continue;
|
|
929
946
|
}
|
|
930
|
-
directives.push(...
|
|
947
|
+
directives.push(...document.directives);
|
|
931
948
|
}
|
|
932
949
|
const assertions = [];
|
|
933
950
|
for (const directive of directives) {
|
|
934
951
|
const resolved = resolveDirective(directive, { root, excludeFiles, scope });
|
|
935
952
|
if ("error" in resolved)
|
|
936
953
|
errors.push(resolved.error);
|
|
937
|
-
|
|
954
|
+
// Selection happens after resolution, so a directive that is not selected
|
|
955
|
+
// is still held to being well-formed - the same bargain ADR-0010 strikes
|
|
956
|
+
// for a document that is not in force.
|
|
957
|
+
else if (options.select?.(resolved.assertion) ?? true)
|
|
938
958
|
assertions.push(resolved.assertion);
|
|
939
959
|
}
|
|
940
960
|
// Not in force is not the same as not checked. A withheld directive is still
|
|
@@ -1028,7 +1048,7 @@ export async function runSpecGuard(options) {
|
|
|
1028
1048
|
errors,
|
|
1029
1049
|
warnings,
|
|
1030
1050
|
inactiveSpecs,
|
|
1031
|
-
specFiles: specFiles.map((file) =>
|
|
1051
|
+
specFiles: specFiles.map((file) => specPath(root, file)),
|
|
1032
1052
|
};
|
|
1033
1053
|
}
|
|
1034
1054
|
//# sourceMappingURL=runner.js.map
|