eval-quality 3.1.0 → 3.3.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 +1 -1
- package/dist/application/index.d.ts +6 -0
- package/dist/application/index.js +8 -0
- package/dist/gates/audit-lockfile-age.mjs +105 -8
- package/dist/gates/check-dependency-direction.js +10 -10
- package/dist/gates/check-doc-claims.js +1026 -0
- package/dist/gates/check-doc-counts.js +408 -0
- package/dist/gates/check-doc-invocations.mjs +677 -0
- package/dist/gates/check-licenses.mjs +89 -16
- package/dist/gates/consumer-pattern.js +104 -0
- package/dist/gates/dependency-direction.js +130 -0
- package/dist/gates/gate-config.js +179 -12
- package/dist/gates/gates-cli.js +216 -19
- package/dist/gates/lineage-ownership.js +20 -23
- package/dist/gates/module-value.js +187 -0
- package/dist/gates/package-boundary.js +33 -144
- package/dist/gates/scanned-paths.js +110 -0
- package/dist/gates/typescript-scanner.js +181 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1 -1
- package/dist/ports/environment-probe-port.d.ts +12 -12
- package/package.json +8 -5
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
// A published gate: everything a package ships, held against forbidden patterns
|
|
2
|
+
// the consumer declares.
|
|
3
|
+
//
|
|
4
|
+
// Both halves are the consumer's. The scanned set is a list of paths, each with
|
|
5
|
+
// a recursion flag and an extension filter, plus the manifest fields a registry
|
|
6
|
+
// publishes verbatim. What a consumer leaves out of that list is exempt, and
|
|
7
|
+
// omission is the only exemption the gate has. The rules are an ordered array of
|
|
8
|
+
// named regular expressions carrying a reason apiece.
|
|
9
|
+
//
|
|
10
|
+
// `scanPackageBoundary` is pure and synchronous over a file map, so one function
|
|
11
|
+
// backs both the real scan and a synthetic test map. It matches text line by
|
|
12
|
+
// line rather than tokenizing, because the references this class of rule forbids
|
|
13
|
+
// live in comments and in string literals, which a token scan reports by kind
|
|
14
|
+
// and not by content. Nothing here loads `typescript`.
|
|
15
|
+
//
|
|
16
|
+
// Run by `node` directly: Node's type stripping erases types only, so no
|
|
17
|
+
// TypeScript enum, namespace, parameter property, or non-type re-export may
|
|
18
|
+
// appear in this file or anything it imports.
|
|
19
|
+
import { readFile } from 'node:fs/promises';
|
|
2
20
|
import { resolve } from 'node:path';
|
|
3
21
|
import { z } from 'zod';
|
|
22
|
+
import { PatternFlags, PatternSource, refinePattern, } from './consumer-pattern.js';
|
|
23
|
+
import { discoverEntries, RelativePath, SCAN_PATH_ERROR, ScannedPathList, } from './scanned-paths.js';
|
|
4
24
|
/**
|
|
5
25
|
* The bound on a consumer-supplied regular expression, and what it does not
|
|
6
26
|
* cover.
|
|
7
27
|
*
|
|
8
28
|
* A pattern arrives as text from a file this package did not write, so the work
|
|
9
29
|
* one pattern can do is the consumer's to choose. JavaScript's `RegExp` exposes
|
|
10
|
-
* no step counter and no timeout, and `
|
|
30
|
+
* no step counter and no timeout, and `doc-claim-sources.ts` records the same
|
|
11
31
|
* limit from the other side: `REGEX_STEP_BUDGET` is a number that exists because
|
|
12
|
-
* a step count is the bound you would want and nothing offers one.
|
|
32
|
+
* a step count is the bound you would want and nothing offers one. A line
|
|
33
|
+
* citation is deliberately not used here: the doc-claims gate resolves citations
|
|
34
|
+
* on published pages, so one rotting inside `scripts/` is held by nothing.
|
|
13
35
|
*
|
|
14
36
|
* So the bound is on the two things that are measurable. The pattern side:
|
|
15
37
|
* `MAX_PATTERN_LENGTH` on the source, `MAX_PATTERNS` on the array, a flag set
|
|
16
38
|
* that excludes `g` and `y`, and a refusal of backreferences, which is the
|
|
17
|
-
* construct that turns a linear scan of one line into an exponential one.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
39
|
+
* construct that turns a linear scan of one line into an exponential one. Those
|
|
40
|
+
* four live in `consumer-pattern.ts`, shared with every gate that takes a
|
|
41
|
+
* pattern. The input side is this scanner's own: `MAX_SCANNED_LINE` bounds the
|
|
42
|
+
* text a pattern is matched against, so a minified bundle or an embedded data
|
|
43
|
+
* URI cannot hand a pattern a megabyte.
|
|
20
44
|
*
|
|
21
45
|
* 64 KiB, not the single kilobyte a first draft of this bound used. This
|
|
22
46
|
* package's own `corpus/` is a legitimate long-line source: an unminified
|
|
@@ -34,7 +58,6 @@ import { z } from 'zod';
|
|
|
34
58
|
* more than the surface is worth.
|
|
35
59
|
*/
|
|
36
60
|
export const MAX_PATTERNS = 64;
|
|
37
|
-
export const MAX_PATTERN_LENGTH = 200;
|
|
38
61
|
export const MAX_SCANNED_LINE = 65_536;
|
|
39
62
|
/**
|
|
40
63
|
* The name an over-long line is reported under. Reserved, so a consumer pattern
|
|
@@ -42,46 +65,9 @@ export const MAX_SCANNED_LINE = 65_536;
|
|
|
42
65
|
* line the gate declined to match.
|
|
43
66
|
*/
|
|
44
67
|
export const OVERLONG_LINE = 'line-exceeds-scan-bound';
|
|
45
|
-
/** A path or manifest field the configuration named and the tree does not have. */
|
|
46
|
-
export const SCAN_PATH_ERROR = 'EVAL_QUALITY_SCAN_PATH';
|
|
47
|
-
/** A tree the gate could not read to the end, so the scan proves nothing. */
|
|
48
|
-
export const SCAN_UNREADABLE = 'EVAL_QUALITY_SCAN_UNREADABLE';
|
|
49
68
|
const codedError = (code, message) => Object.assign(new Error(message), { code });
|
|
50
69
|
const detail = (error) => error instanceof Error ? error.message : String(error);
|
|
51
70
|
const NonEmpty = z.string().min(1);
|
|
52
|
-
const isSafeRelative = (value) => !value.startsWith('/') &&
|
|
53
|
-
!/^[A-Za-z]:/.test(value) &&
|
|
54
|
-
!value.includes('\\') &&
|
|
55
|
-
value.split('/').every((segment) => segment !== '' && segment !== '..');
|
|
56
|
-
const RELATIVE_PATH_MESSAGE = 'is not a repository-relative path: write it with forward slashes, no leading slash, no drive letter, and no ".." segment, so a configuration can only name files beneath itself';
|
|
57
|
-
/** Shared with `lineage-ownership.ts`; see the note on `ScannedPathList`. */
|
|
58
|
-
export const RelativePath = NonEmpty.max(400).refine(isSafeRelative, RELATIVE_PATH_MESSAGE);
|
|
59
|
-
/** The same shape with a trailing slash allowed, for a directory prefix. */
|
|
60
|
-
export const RelativePrefix = NonEmpty.max(400).refine((value) => isSafeRelative(value.endsWith('/') ? value.slice(0, -1) : value), RELATIVE_PATH_MESSAGE);
|
|
61
|
-
const ScannedPath = z.strictObject({
|
|
62
|
-
path: RelativePath.describe('A directory to walk, or a single file to read, relative to this configuration file.'),
|
|
63
|
-
recursive: z
|
|
64
|
-
.boolean()
|
|
65
|
-
.default(true)
|
|
66
|
-
.describe('Whether a directory is walked to the bottom. Ignored when the path names a file.'),
|
|
67
|
-
extensions: z
|
|
68
|
-
.array(z
|
|
69
|
-
.string()
|
|
70
|
-
.regex(/^\.[A-Za-z0-9][A-Za-z0-9.]*$/, 'is not a file extension; write it with its leading dot, as ".ts"'))
|
|
71
|
-
.min(1)
|
|
72
|
-
.optional()
|
|
73
|
-
.describe('Which files under this path are read. Leave it out and every file is read whatever its name, which is what a tree of generated data needs.'),
|
|
74
|
-
optional: z
|
|
75
|
-
.boolean()
|
|
76
|
-
.default(false)
|
|
77
|
-
.describe('Whether this path may contribute nothing. False, the default, fails when the path is absent or holds no matching file, so a generated tree nobody built cannot read as a clean scan.'),
|
|
78
|
-
});
|
|
79
|
-
/**
|
|
80
|
-
* The scanned-set declaration, shared with the field-ownership gate. It sits
|
|
81
|
-
* here because this is the gate whose headline is the scanned set; a third gate
|
|
82
|
-
* that needs it is the point at which it earns a module of its own.
|
|
83
|
-
*/
|
|
84
|
-
export const ScannedPathList = z.array(ScannedPath).min(1);
|
|
85
71
|
const ManifestField = NonEmpty.regex(/^[A-Za-z_$][A-Za-z0-9_$-]*(?:\.[A-Za-z_$][A-Za-z0-9_$-]*)*$/, 'is not a manifest field path; write one key, or several joined by dots');
|
|
86
72
|
const ManifestScan = z
|
|
87
73
|
.strictObject({
|
|
@@ -92,25 +78,11 @@ const ManifestScan = z
|
|
|
92
78
|
.describe('Which fields are scanned. A string is one entry, an array joins into one, and an object becomes one entry per key, so "scripts" covers every script by name. A field named here and absent from the manifest is refused, so a typo cannot read as a field with nothing in it.'),
|
|
93
79
|
})
|
|
94
80
|
.describe('The manifest fields scanned as synthetic entries. A JSON value has no line of its own, so every one of them reports at line 1.');
|
|
95
|
-
/**
|
|
96
|
-
* `\1` through `\9` and `\k<name>`. It over-refuses an escaped backslash
|
|
97
|
-
* followed by a digit, which is a literal backslash and not a backreference,
|
|
98
|
-
* and that spelling has no place in a boundary pattern anyway.
|
|
99
|
-
*/
|
|
100
|
-
const BACKREFERENCE = /\\[1-9]|\\k</;
|
|
101
81
|
const ForbiddenPattern = z
|
|
102
82
|
.strictObject({
|
|
103
83
|
name: NonEmpty.describe('What a violation is reported under. Unique across the array.'),
|
|
104
|
-
match:
|
|
105
|
-
|
|
106
|
-
.min(1)
|
|
107
|
-
.max(MAX_PATTERN_LENGTH)
|
|
108
|
-
.describe('The regular expression, as source text. It is matched against one logical line at a time.'),
|
|
109
|
-
flags: z
|
|
110
|
-
.string()
|
|
111
|
-
.regex(/^[imsuv]*$/, 'admits only i, m, s, u and v. A g or a y carries a match position between calls, so a pattern holding either would match every second line it should have matched')
|
|
112
|
-
.default('')
|
|
113
|
-
.describe('Regular-expression flags. Empty by default.'),
|
|
84
|
+
match: PatternSource.describe('The regular expression, as source text. It is matched against one logical line at a time.'),
|
|
85
|
+
flags: PatternFlags.describe('Regular-expression flags. Empty by default.'),
|
|
114
86
|
reason: NonEmpty.describe('Why the package may not carry it. Printed beside every violation, so the report says what to do rather than only what fired.'),
|
|
115
87
|
})
|
|
116
88
|
.superRefine((pattern, ctx) => {
|
|
@@ -121,23 +93,7 @@ const ForbiddenPattern = z
|
|
|
121
93
|
message: `is reserved: the gate reports a line past its own matching bound under "${OVERLONG_LINE}"`,
|
|
122
94
|
});
|
|
123
95
|
}
|
|
124
|
-
|
|
125
|
-
ctx.addIssue({
|
|
126
|
-
code: 'custom',
|
|
127
|
-
path: ['match'],
|
|
128
|
-
message: 'carries a backreference, which is the construct that turns a scan of one line into an exponential one; write the pattern without one',
|
|
129
|
-
});
|
|
130
|
-
}
|
|
131
|
-
try {
|
|
132
|
-
new RegExp(pattern.match, pattern.flags);
|
|
133
|
-
}
|
|
134
|
-
catch (error) {
|
|
135
|
-
ctx.addIssue({
|
|
136
|
-
code: 'custom',
|
|
137
|
-
path: ['match'],
|
|
138
|
-
message: `is not a regular expression: ${detail(error)}`,
|
|
139
|
-
});
|
|
140
|
-
}
|
|
96
|
+
refinePattern(pattern.match, pattern.flags, ctx, ['match']);
|
|
141
97
|
});
|
|
142
98
|
export const PackageBoundarySection = z
|
|
143
99
|
.strictObject({
|
|
@@ -251,73 +207,6 @@ export function scanPackageBoundary(files, patterns) {
|
|
|
251
207
|
}
|
|
252
208
|
return violations;
|
|
253
209
|
}
|
|
254
|
-
const matchesExtension = (declared, name) => declared.extensions === undefined ||
|
|
255
|
-
declared.extensions.some((extension) => name.endsWith(extension));
|
|
256
|
-
async function walk(root, posix, declared, entries) {
|
|
257
|
-
let dirents;
|
|
258
|
-
try {
|
|
259
|
-
dirents = await readdir(resolve(root, posix), { withFileTypes: true });
|
|
260
|
-
}
|
|
261
|
-
catch (error) {
|
|
262
|
-
throw codedError(SCAN_UNREADABLE, `${posix} could not be read: ${detail(error)}`);
|
|
263
|
-
}
|
|
264
|
-
let count = 0;
|
|
265
|
-
for (const entry of dirents) {
|
|
266
|
-
const child = `${posix}/${entry.name}`;
|
|
267
|
-
if (entry.isSymbolicLink()) {
|
|
268
|
-
throw codedError(SCAN_UNREADABLE, `${child} is a symbolic link; this scan does not follow links, and skipping one would leave a file unscanned while the run reported a clean tree`);
|
|
269
|
-
}
|
|
270
|
-
if (entry.isDirectory()) {
|
|
271
|
-
if (!declared.recursive)
|
|
272
|
-
continue;
|
|
273
|
-
count += await walk(root, child, declared, entries);
|
|
274
|
-
}
|
|
275
|
-
else if (entry.isFile() && matchesExtension(declared, entry.name)) {
|
|
276
|
-
entries.set(child, await readFile(resolve(root, child), 'utf8'));
|
|
277
|
-
count += 1;
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
return count;
|
|
281
|
-
}
|
|
282
|
-
/**
|
|
283
|
-
* Everything the declared paths hold, keyed by the path a report prints. Fails
|
|
284
|
-
* closed on the three ways a walk under-reports: a path that is not there, a
|
|
285
|
-
* path that matched no file, and a link whose target could sit anywhere.
|
|
286
|
-
*/
|
|
287
|
-
export async function discoverEntries(root, paths, gate) {
|
|
288
|
-
const entries = new Map();
|
|
289
|
-
const counts = [];
|
|
290
|
-
for (const declared of paths) {
|
|
291
|
-
let info;
|
|
292
|
-
try {
|
|
293
|
-
info = await lstat(resolve(root, declared.path));
|
|
294
|
-
}
|
|
295
|
-
catch (error) {
|
|
296
|
-
if (error.code !== 'ENOENT') {
|
|
297
|
-
throw codedError(SCAN_UNREADABLE, `${declared.path} could not be read: ${detail(error)}`);
|
|
298
|
-
}
|
|
299
|
-
if (declared.optional) {
|
|
300
|
-
counts.push({ path: declared.path, files: 0 });
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
throw codedError(SCAN_PATH_ERROR, `${declared.path} does not exist, and the "${gate}" section names it under paths; mark it optional if it may be absent`);
|
|
304
|
-
}
|
|
305
|
-
if (info.isSymbolicLink()) {
|
|
306
|
-
throw codedError(SCAN_UNREADABLE, `${declared.path} is a symbolic link, and this scan does not follow links`);
|
|
307
|
-
}
|
|
308
|
-
if (info.isFile()) {
|
|
309
|
-
entries.set(declared.path, await readFile(resolve(root, declared.path), 'utf8'));
|
|
310
|
-
counts.push({ path: declared.path, files: 1 });
|
|
311
|
-
continue;
|
|
312
|
-
}
|
|
313
|
-
const found = await walk(root, declared.path, declared, entries);
|
|
314
|
-
if (found === 0 && !declared.optional) {
|
|
315
|
-
throw codedError(SCAN_PATH_ERROR, `${declared.path} holds no file the "${gate}" section asked for, so a scan of nothing would report zero violations for the wrong reason; mark it optional if it may be empty`);
|
|
316
|
-
}
|
|
317
|
-
counts.push({ path: declared.path, files: found });
|
|
318
|
-
}
|
|
319
|
-
return { entries, counts };
|
|
320
|
-
}
|
|
321
210
|
function flattenField(key, value, into) {
|
|
322
211
|
if (typeof value === 'string') {
|
|
323
212
|
into.set(key, value);
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { lstat, readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
/** A path or manifest field the configuration named and the tree does not have. */
|
|
5
|
+
export const SCAN_PATH_ERROR = 'EVAL_QUALITY_SCAN_PATH';
|
|
6
|
+
/** A tree the gate could not read to the end, so the scan proves nothing. */
|
|
7
|
+
export const SCAN_UNREADABLE = 'EVAL_QUALITY_SCAN_UNREADABLE';
|
|
8
|
+
const codedError = (code, message) => Object.assign(new Error(message), { code });
|
|
9
|
+
const detail = (error) => error instanceof Error ? error.message : String(error);
|
|
10
|
+
const NonEmpty = z.string().min(1);
|
|
11
|
+
const isSafeRelative = (value) => !value.startsWith('/') &&
|
|
12
|
+
!/^[A-Za-z]:/.test(value) &&
|
|
13
|
+
!value.includes('\\') &&
|
|
14
|
+
value.split('/').every((segment) => segment !== '' && segment !== '..');
|
|
15
|
+
const RELATIVE_PATH_MESSAGE = 'is not a repository-relative path: write it with forward slashes, no leading slash, no drive letter, and no ".." segment, so a configuration can only name files beneath itself';
|
|
16
|
+
/** Shared with `lineage-ownership.ts`; see the note on `ScannedPathList`. */
|
|
17
|
+
export const RelativePath = NonEmpty.max(400).refine(isSafeRelative, RELATIVE_PATH_MESSAGE);
|
|
18
|
+
/** The same shape with a trailing slash allowed, for a directory prefix. */
|
|
19
|
+
export const RelativePrefix = NonEmpty.max(400).refine((value) => isSafeRelative(value.endsWith('/') ? value.slice(0, -1) : value), RELATIVE_PATH_MESSAGE);
|
|
20
|
+
const ScannedPath = z.strictObject({
|
|
21
|
+
path: RelativePath.describe('A directory to walk, or a single file to read, relative to this configuration file.'),
|
|
22
|
+
recursive: z
|
|
23
|
+
.boolean()
|
|
24
|
+
.default(true)
|
|
25
|
+
.describe('Whether a directory is walked to the bottom. Ignored when the path names a file.'),
|
|
26
|
+
extensions: z
|
|
27
|
+
.array(z
|
|
28
|
+
.string()
|
|
29
|
+
.regex(/^\.[A-Za-z0-9][A-Za-z0-9.]*$/, 'is not a file extension; write it with its leading dot, as ".ts"'))
|
|
30
|
+
.min(1)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe('Which files under this path are read. Leave it out and every file is read whatever its name, which is what a tree of generated data needs.'),
|
|
33
|
+
optional: z
|
|
34
|
+
.boolean()
|
|
35
|
+
.default(false)
|
|
36
|
+
.describe('Whether this path may contribute nothing. False, the default, fails when the path is absent or holds no matching file, so a generated tree nobody built cannot read as a clean scan.'),
|
|
37
|
+
});
|
|
38
|
+
/**
|
|
39
|
+
* The scanned-set declaration. It lived in `package-boundary.ts` while that gate
|
|
40
|
+
* was its only owner, on a note saying a third gate needing it was the point at
|
|
41
|
+
* which it earned a module of its own. The two documentation gates made three.
|
|
42
|
+
*/
|
|
43
|
+
export const ScannedPathList = z.array(ScannedPath).min(1);
|
|
44
|
+
const matchesExtension = (declared, name) => declared.extensions === undefined ||
|
|
45
|
+
declared.extensions.some((extension) => name.endsWith(extension));
|
|
46
|
+
async function walk(root, posix, declared, entries) {
|
|
47
|
+
let dirents;
|
|
48
|
+
try {
|
|
49
|
+
dirents = await readdir(resolve(root, posix), { withFileTypes: true });
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw codedError(SCAN_UNREADABLE, `${posix} could not be read: ${detail(error)}`);
|
|
53
|
+
}
|
|
54
|
+
let count = 0;
|
|
55
|
+
for (const entry of dirents) {
|
|
56
|
+
const child = `${posix}/${entry.name}`;
|
|
57
|
+
if (entry.isSymbolicLink()) {
|
|
58
|
+
throw codedError(SCAN_UNREADABLE, `${child} is a symbolic link; this scan does not follow links, and skipping one would leave a file unscanned while the run reported a clean tree`);
|
|
59
|
+
}
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
if (!declared.recursive)
|
|
62
|
+
continue;
|
|
63
|
+
count += await walk(root, child, declared, entries);
|
|
64
|
+
}
|
|
65
|
+
else if (entry.isFile() && matchesExtension(declared, entry.name)) {
|
|
66
|
+
entries.set(child, await readFile(resolve(root, child), 'utf8'));
|
|
67
|
+
count += 1;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return count;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Everything the declared paths hold, keyed by the path a report prints. Fails
|
|
74
|
+
* closed on the three ways a walk under-reports: a path that is not there, a
|
|
75
|
+
* path that matched no file, and a link whose target could sit anywhere.
|
|
76
|
+
*/
|
|
77
|
+
export async function discoverEntries(root, paths, gate) {
|
|
78
|
+
const entries = new Map();
|
|
79
|
+
const counts = [];
|
|
80
|
+
for (const declared of paths) {
|
|
81
|
+
let info;
|
|
82
|
+
try {
|
|
83
|
+
info = await lstat(resolve(root, declared.path));
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== 'ENOENT') {
|
|
87
|
+
throw codedError(SCAN_UNREADABLE, `${declared.path} could not be read: ${detail(error)}`);
|
|
88
|
+
}
|
|
89
|
+
if (declared.optional) {
|
|
90
|
+
counts.push({ path: declared.path, files: 0 });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
throw codedError(SCAN_PATH_ERROR, `${declared.path} does not exist, and the "${gate}" section names it under paths; mark it optional if it may be absent`);
|
|
94
|
+
}
|
|
95
|
+
if (info.isSymbolicLink()) {
|
|
96
|
+
throw codedError(SCAN_UNREADABLE, `${declared.path} is a symbolic link, and this scan does not follow links`);
|
|
97
|
+
}
|
|
98
|
+
if (info.isFile()) {
|
|
99
|
+
entries.set(declared.path, await readFile(resolve(root, declared.path), 'utf8'));
|
|
100
|
+
counts.push({ path: declared.path, files: 1 });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const found = await walk(root, declared.path, declared, entries);
|
|
104
|
+
if (found === 0 && !declared.optional) {
|
|
105
|
+
throw codedError(SCAN_PATH_ERROR, `${declared.path} holds no file the "${gate}" section asked for, so a scan of nothing would report zero violations for the wrong reason; mark it optional if it may be empty`);
|
|
106
|
+
}
|
|
107
|
+
counts.push({ path: declared.path, files: found });
|
|
108
|
+
}
|
|
109
|
+
return { entries, counts };
|
|
110
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
// The one place the two source-scanning gates reach TypeScript.
|
|
2
|
+
//
|
|
3
|
+
// The scanner lives at `typescript/unstable/ast`, a subpath whose own name says
|
|
4
|
+
// it may move, and it exists from TypeScript 7.0: the 5.x line has no such
|
|
5
|
+
// subpath and the 7.x main entry exports no scanner. The optional peer range
|
|
6
|
+
// says `>=5.7.0` and stays that wide on purpose. npm resolves an optional peer
|
|
7
|
+
// that is present, so a range of `>=7` would turn `npm install` red for every
|
|
8
|
+
// consumer with TypeScript 5 in its tree and no interest in these two gates.
|
|
9
|
+
// The version fact lives here instead, spoken at the one moment it matters:
|
|
10
|
+
// when a consumer runs one of the two gates.
|
|
11
|
+
//
|
|
12
|
+
// Three refusals, each its own because the repair is different. The package
|
|
13
|
+
// is absent; the package is a version with no such subpath; the subpath is
|
|
14
|
+
// there and lacks a member this build reads. The third is the quiet one: a
|
|
15
|
+
// renamed enum member reads as `undefined`, `token.kind === undefined` never
|
|
16
|
+
// matches, and the rule it guarded switches off with every gate green. So every
|
|
17
|
+
// `SyntaxKind` member the three scanner modules read is listed below, and
|
|
18
|
+
// `tests/architecture/typescript-scanner.test.ts` derives the same list from
|
|
19
|
+
// their sources, so the list is not a copy a hand maintains.
|
|
20
|
+
//
|
|
21
|
+
// Run by `node` directly: Node's type stripping erases types only, so no
|
|
22
|
+
// TypeScript enum, namespace, parameter property, or non-type re-export may
|
|
23
|
+
// appear in this file or anything it imports, or the gate fails at load.
|
|
24
|
+
/** `error.code` on every refusal here; the binary maps it to the usage exit. */
|
|
25
|
+
export const TYPESCRIPT_UNAVAILABLE = 'EVAL_QUALITY_TYPESCRIPT_UNAVAILABLE';
|
|
26
|
+
/** Where the scanner is read from, and the first TypeScript that ships it. */
|
|
27
|
+
export const SCANNER_SUBPATH = 'typescript/unstable/ast';
|
|
28
|
+
export const SCANNER_SHIPS_FROM = '7.0.0';
|
|
29
|
+
/** Every `SyntaxKind` member `token-scan.ts`, `dependency-direction.ts` and `lineage-ownership.ts` read. */
|
|
30
|
+
export const REQUIRED_SYNTAX_KINDS = [
|
|
31
|
+
'AmpersandToken',
|
|
32
|
+
'AnyKeyword',
|
|
33
|
+
'AsKeyword',
|
|
34
|
+
'AsteriskToken',
|
|
35
|
+
'AsyncKeyword',
|
|
36
|
+
'AwaitKeyword',
|
|
37
|
+
'BarToken',
|
|
38
|
+
'BigIntKeyword',
|
|
39
|
+
'BigIntLiteral',
|
|
40
|
+
'BooleanKeyword',
|
|
41
|
+
'CaseKeyword',
|
|
42
|
+
'CatchKeyword',
|
|
43
|
+
'ClassKeyword',
|
|
44
|
+
'CloseBraceToken',
|
|
45
|
+
'CloseBracketToken',
|
|
46
|
+
'CloseParenToken',
|
|
47
|
+
'ColonToken',
|
|
48
|
+
'CommaToken',
|
|
49
|
+
'ConstKeyword',
|
|
50
|
+
'DefaultKeyword',
|
|
51
|
+
'DotToken',
|
|
52
|
+
'EndOfFile',
|
|
53
|
+
'EqualsGreaterThanToken',
|
|
54
|
+
'EqualsToken',
|
|
55
|
+
'ExclamationToken',
|
|
56
|
+
'ExportKeyword',
|
|
57
|
+
'ExtendsKeyword',
|
|
58
|
+
'FalseKeyword',
|
|
59
|
+
'FirstAssignment',
|
|
60
|
+
'ForKeyword',
|
|
61
|
+
'FromKeyword',
|
|
62
|
+
'FunctionKeyword',
|
|
63
|
+
'GetKeyword',
|
|
64
|
+
'Identifier',
|
|
65
|
+
'IfKeyword',
|
|
66
|
+
'ImportKeyword',
|
|
67
|
+
'InterfaceKeyword',
|
|
68
|
+
'LastAssignment',
|
|
69
|
+
'LessThanToken',
|
|
70
|
+
'LetKeyword',
|
|
71
|
+
'MinusMinusToken',
|
|
72
|
+
'NeverKeyword',
|
|
73
|
+
'NewKeyword',
|
|
74
|
+
'NoSubstitutionTemplateLiteral',
|
|
75
|
+
'NullKeyword',
|
|
76
|
+
'NumberKeyword',
|
|
77
|
+
'NumericLiteral',
|
|
78
|
+
'ObjectKeyword',
|
|
79
|
+
'OpenBraceToken',
|
|
80
|
+
'OpenBracketToken',
|
|
81
|
+
'OpenParenToken',
|
|
82
|
+
'PlusPlusToken',
|
|
83
|
+
'PrivateKeyword',
|
|
84
|
+
'ProtectedKeyword',
|
|
85
|
+
'PublicKeyword',
|
|
86
|
+
'QuestionDotToken',
|
|
87
|
+
'QuestionToken',
|
|
88
|
+
'ReadonlyKeyword',
|
|
89
|
+
'RegularExpressionLiteral',
|
|
90
|
+
'RequireKeyword',
|
|
91
|
+
'ReturnKeyword',
|
|
92
|
+
'SemicolonToken',
|
|
93
|
+
'SetKeyword',
|
|
94
|
+
'SlashEqualsToken',
|
|
95
|
+
'SlashToken',
|
|
96
|
+
'StaticKeyword',
|
|
97
|
+
'StringKeyword',
|
|
98
|
+
'StringLiteral',
|
|
99
|
+
'SuperKeyword',
|
|
100
|
+
'SymbolKeyword',
|
|
101
|
+
'TemplateHead',
|
|
102
|
+
'TemplateTail',
|
|
103
|
+
'ThisKeyword',
|
|
104
|
+
'TrueKeyword',
|
|
105
|
+
'TypeKeyword',
|
|
106
|
+
'UndefinedKeyword',
|
|
107
|
+
'UnknownKeyword',
|
|
108
|
+
'VarKeyword',
|
|
109
|
+
'VoidKeyword',
|
|
110
|
+
'WhileKeyword',
|
|
111
|
+
'WithKeyword',
|
|
112
|
+
];
|
|
113
|
+
const codedError = (message) => Object.assign(new Error(message), { code: TYPESCRIPT_UNAVAILABLE });
|
|
114
|
+
const defaultLoad = () => import('typescript/unstable/ast');
|
|
115
|
+
// `typescript/package.json` is on the package's export map, so the version is
|
|
116
|
+
// read from the install itself. A read that fails names no version rather than
|
|
117
|
+
// failing the refusal that wanted it.
|
|
118
|
+
const defaultVersion = async () => {
|
|
119
|
+
try {
|
|
120
|
+
const manifest = (await import('typescript/package.json', {
|
|
121
|
+
with: { type: 'json' },
|
|
122
|
+
}));
|
|
123
|
+
const version = manifest.default?.version;
|
|
124
|
+
return typeof version === 'string' ? version : null;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
/** The sentence for an absent package, shared so both gates say the same thing. */
|
|
131
|
+
export const absentMessage = (gate) => `the ${gate} gate reads your source with the TypeScript scanner, and the optional peer dependency "typescript" is not installed here. Install it (npm install --save-dev typescript), or drop the "${gate}" section from your configuration to stop invoking this gate. Only the dependency-direction and field-ownership gates need it.`;
|
|
132
|
+
/** Whether `error` carries `code` as a `NodeJS.ErrnoException` would, without assuming `error` is an object at all. */
|
|
133
|
+
export const isCode = (error, code) => error !== null &&
|
|
134
|
+
typeof error === 'object' &&
|
|
135
|
+
error.code === code;
|
|
136
|
+
/**
|
|
137
|
+
* The scanner module, or a refusal carrying `TYPESCRIPT_UNAVAILABLE` that names
|
|
138
|
+
* the gate, the installed version and the repair. Anything that is not one of
|
|
139
|
+
* the three refusals is rethrown unchanged.
|
|
140
|
+
*
|
|
141
|
+
* `load` and `readVersion` are injectable so each refusal has a case that
|
|
142
|
+
* uninstalls nothing.
|
|
143
|
+
*/
|
|
144
|
+
export async function loadTypeScriptScanner(gate, load = defaultLoad, readVersion = defaultVersion) {
|
|
145
|
+
let loaded;
|
|
146
|
+
try {
|
|
147
|
+
loaded = await load();
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (isCode(error, 'ERR_MODULE_NOT_FOUND')) {
|
|
151
|
+
throw codedError(absentMessage(gate));
|
|
152
|
+
}
|
|
153
|
+
if (isCode(error, 'ERR_PACKAGE_PATH_NOT_EXPORTED')) {
|
|
154
|
+
const version = (await readVersion().catch(() => null)) ?? 'an unknown version';
|
|
155
|
+
throw codedError(`the ${gate} gate reads your source with the TypeScript scanner at ${SCANNER_SUBPATH}, and typescript ${version} carries no such subpath; TypeScript ships it from ${SCANNER_SHIPS_FROM}. Install typescript 7 to run this gate, or drop the "${gate}" section from your configuration to stop invoking it.`);
|
|
156
|
+
}
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
const module = (loaded ?? {});
|
|
160
|
+
const missing = [];
|
|
161
|
+
if (typeof module.createScanner !== 'function')
|
|
162
|
+
missing.push('createScanner');
|
|
163
|
+
if (typeof module.computeLineStarts !== 'function') {
|
|
164
|
+
missing.push('computeLineStarts');
|
|
165
|
+
}
|
|
166
|
+
const kinds = module.SyntaxKind;
|
|
167
|
+
if (kinds === null || typeof kinds !== 'object') {
|
|
168
|
+
missing.push('SyntaxKind');
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
for (const name of REQUIRED_SYNTAX_KINDS) {
|
|
172
|
+
if (typeof kinds[name] !== 'number')
|
|
173
|
+
missing.push(`SyntaxKind.${name}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (missing.length > 0) {
|
|
177
|
+
const version = (await readVersion().catch(() => null)) ?? 'an unknown version';
|
|
178
|
+
throw codedError(`the ${gate} gate reads ${missing.join(', ')} from ${SCANNER_SUBPATH}, and typescript ${version} ships it without ${missing.length === 1 ? 'that name' : 'those names'}; a member this gate cannot find would switch a rule off silently, so it refuses instead. This build reads the scanner TypeScript 7 ships.`);
|
|
179
|
+
}
|
|
180
|
+
return module;
|
|
181
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -4,12 +4,14 @@ export type { EvalContract } from './core/schemas/eval-contract.ts';
|
|
|
4
4
|
export { EVAL_CONTRACT_SCHEMA_VERSION } from './core/schemas/eval-contract.ts';
|
|
5
5
|
export type { EvaluatorConfiguration } from './core/schemas/evaluator-configuration.ts';
|
|
6
6
|
export { EVALUATOR_CONFIGURATION_SCHEMA_VERSION } from './core/schemas/evaluator-configuration.ts';
|
|
7
|
-
export type { EvidenceArtifact } from './core/schemas/evidence-artifact.ts';
|
|
7
|
+
export type { CheckResolutionValue, EvidenceArtifact, } from './core/schemas/evidence-artifact.ts';
|
|
8
8
|
export { EVIDENCE_ARTIFACT_SCHEMA_VERSION } from './core/schemas/evidence-artifact.ts';
|
|
9
|
+
export type { Expression, Operand } from './core/schemas/expression.ts';
|
|
9
10
|
export type { IsolationManifest } from './core/schemas/isolation-manifest.ts';
|
|
10
11
|
export { ISOLATION_MANIFEST_SCHEMA_VERSION } from './core/schemas/isolation-manifest.ts';
|
|
11
12
|
export type { PreflightCheck, PreflightVerdict, } from './core/schemas/preflight-verdict.ts';
|
|
12
13
|
export { PREFLIGHT_VERDICT_SCHEMA_VERSION } from './core/schemas/preflight-verdict.ts';
|
|
14
|
+
export type { JsonValue } from './core/schemas/primitives.ts';
|
|
13
15
|
export type { PrivateArtifactManifest } from './core/schemas/private-artifact-manifest.ts';
|
|
14
16
|
export { PRIVATE_ARTIFACT_MANIFEST_SCHEMA_VERSION } from './core/schemas/private-artifact-manifest.ts';
|
|
15
17
|
export type { Probe } from './core/schemas/probe.ts';
|
|
@@ -19,7 +21,7 @@ export type { ScoringPolicy } from './core/schemas/scoring-policy.ts';
|
|
|
19
21
|
export { SCORING_POLICY_SCHEMA_VERSION } from './core/schemas/scoring-policy.ts';
|
|
20
22
|
export type { SealedEvaluatorBrief } from './core/schemas/sealed-evaluator-brief.ts';
|
|
21
23
|
export { SEALED_EVALUATOR_BRIEF_SCHEMA_VERSION } from './core/schemas/sealed-evaluator-brief.ts';
|
|
22
|
-
export type { SealedRunRecord } from './core/schemas/sealed-run-record.ts';
|
|
24
|
+
export type { Observation, SealedRunRecord, } from './core/schemas/sealed-run-record.ts';
|
|
23
25
|
export { SEALED_RUN_RECORD_SCHEMA_VERSION } from './core/schemas/sealed-run-record.ts';
|
|
24
26
|
export type { FixtureReset, ManifestationWitness, SensitivityWitness, SensitivityWitnessLeg, WitnessChannel, WitnessInputs, } from './core/schemas/sensitivity-witness.ts';
|
|
25
|
-
export declare const VERSION = "3.
|
|
27
|
+
export declare const VERSION = "3.3.0";
|
package/dist/index.js
CHANGED
|
@@ -38,4 +38,4 @@ export { PROBE_SCHEMA_VERSION } from './core/schemas/probe.js';
|
|
|
38
38
|
export { SCORING_POLICY_SCHEMA_VERSION } from './core/schemas/scoring-policy.js';
|
|
39
39
|
export { SEALED_EVALUATOR_BRIEF_SCHEMA_VERSION } from './core/schemas/sealed-evaluator-brief.js';
|
|
40
40
|
export { SEALED_RUN_RECORD_SCHEMA_VERSION } from './core/schemas/sealed-run-record.js';
|
|
41
|
-
export const VERSION = '3.
|
|
41
|
+
export const VERSION = '3.3.0';
|
|
@@ -56,12 +56,12 @@ export declare const probeParsers: {
|
|
|
56
56
|
}>;
|
|
57
57
|
pathTemplate: import("zod").ZodString;
|
|
58
58
|
channels: import("zod").ZodObject<{
|
|
59
|
-
path: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../
|
|
60
|
-
query: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../
|
|
59
|
+
path: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>>;
|
|
60
|
+
query: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>>;
|
|
61
61
|
header: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
|
|
62
62
|
body: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
63
63
|
kind: import("zod").ZodLiteral<"json">;
|
|
64
|
-
value: import("zod").ZodType<import("../
|
|
64
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
65
65
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
66
66
|
kind: import("zod").ZodLiteral<"absent">;
|
|
67
67
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
@@ -74,12 +74,12 @@ export declare const probeParsers: {
|
|
|
74
74
|
executable: import("zod").ZodString;
|
|
75
75
|
subcommandPath: import("zod").ZodArray<import("zod").ZodString>;
|
|
76
76
|
channels: import("zod").ZodObject<{
|
|
77
|
-
argument: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../
|
|
78
|
-
option: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../
|
|
77
|
+
argument: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>>;
|
|
78
|
+
option: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>>;
|
|
79
79
|
environment: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
|
|
80
80
|
stdin: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
81
81
|
kind: import("zod").ZodLiteral<"json">;
|
|
82
|
-
value: import("zod").ZodType<import("../
|
|
82
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
83
83
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
84
84
|
kind: import("zod").ZodLiteral<"text">;
|
|
85
85
|
value: import("zod").ZodString;
|
|
@@ -94,7 +94,7 @@ export declare const probeParsers: {
|
|
|
94
94
|
kind: import("zod").ZodLiteral<"mcp">;
|
|
95
95
|
toolName: import("zod").ZodString;
|
|
96
96
|
channels: import("zod").ZodObject<{
|
|
97
|
-
arguments: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../
|
|
97
|
+
arguments: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>>;
|
|
98
98
|
}, import("zod/v4/core").$strict>;
|
|
99
99
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
100
100
|
readonly response: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
@@ -106,7 +106,7 @@ export declare const probeParsers: {
|
|
|
106
106
|
headers: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodString>;
|
|
107
107
|
body: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
108
108
|
kind: import("zod").ZodLiteral<"json">;
|
|
109
|
-
value: import("zod").ZodType<import("../
|
|
109
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
110
110
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
111
111
|
kind: import("zod").ZodLiteral<"text">;
|
|
112
112
|
value: import("zod").ZodString;
|
|
@@ -121,7 +121,7 @@ export declare const probeParsers: {
|
|
|
121
121
|
exitCode: import("zod").ZodInt;
|
|
122
122
|
stdout: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
123
123
|
kind: import("zod").ZodLiteral<"json">;
|
|
124
|
-
value: import("zod").ZodType<import("../
|
|
124
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
125
125
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
126
126
|
kind: import("zod").ZodLiteral<"text">;
|
|
127
127
|
value: import("zod").ZodString;
|
|
@@ -130,7 +130,7 @@ export declare const probeParsers: {
|
|
|
130
130
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
131
131
|
stderr: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
132
132
|
kind: import("zod").ZodLiteral<"json">;
|
|
133
|
-
value: import("zod").ZodType<import("../
|
|
133
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
134
134
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
135
135
|
kind: import("zod").ZodLiteral<"text">;
|
|
136
136
|
value: import("zod").ZodString;
|
|
@@ -139,7 +139,7 @@ export declare const probeParsers: {
|
|
|
139
139
|
}, import("zod/v4/core").$strict>], "kind">;
|
|
140
140
|
artifacts: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
141
141
|
kind: import("zod").ZodLiteral<"json">;
|
|
142
|
-
value: import("zod").ZodType<import("../
|
|
142
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
143
143
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
144
144
|
kind: import("zod").ZodLiteral<"text">;
|
|
145
145
|
value: import("zod").ZodString;
|
|
@@ -154,7 +154,7 @@ export declare const probeParsers: {
|
|
|
154
154
|
isError: import("zod").ZodBoolean;
|
|
155
155
|
result: import("zod").ZodDiscriminatedUnion<[import("zod").ZodObject<{
|
|
156
156
|
kind: import("zod").ZodLiteral<"json">;
|
|
157
|
-
value: import("zod").ZodType<import("../
|
|
157
|
+
value: import("zod").ZodType<import("../index.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../index.ts").JsonValue, unknown>>;
|
|
158
158
|
}, import("zod/v4/core").$strict>, import("zod").ZodObject<{
|
|
159
159
|
kind: import("zod").ZodLiteral<"text">;
|
|
160
160
|
value: import("zod").ZodString;
|