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.
@@ -1,22 +1,46 @@
1
- import { lstat, readdir, readFile } from 'node:fs/promises';
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 `check-doc-claims.ts:943` records the same
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. The
18
- * input side: `MAX_SCANNED_LINE` on the text a pattern is matched against, so a
19
- * minified bundle or an embedded data URI cannot hand a pattern a megabyte.
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: z
105
- .string()
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
- if (BACKREFERENCE.test(pattern.match)) {
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.1.0";
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.1.0';
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
60
- query: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
78
- option: import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodType<import("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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("../core/schemas/primitives.ts").JsonValue, unknown, import("zod/v4/core").$ZodTypeInternals<import("../core/schemas/primitives.ts").JsonValue, unknown>>;
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;