@boyingliu01/xp-gate 0.31.4 → 0.32.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/adapters/java.sh +256 -4
- package/adapters/powershell.sh +45 -6
- package/bin/xp-gate.js +8 -2
- package/build-integrity/__tests__/gate-10.test.ts +88 -1
- package/build-integrity/gate-10.ts +51 -16
- package/dsh.bundle.patch.yml +15 -0
- package/gate-13-compiler-strictness.sh +1031 -0
- package/gate-3.sh +48 -13
- package/hooks/gate-13-compiler-strictness.sh +1031 -0
- package/hooks/gate-3.sh +48 -13
- package/hooks/lib/coverage-total.sh +18 -2
- package/hooks/lib/java-test-selection.sh +251 -0
- package/hooks/lib/jscpd-run.sh +9 -0
- package/hooks/lib/validate-code-walkthrough.cjs +186 -29
- package/hooks/pre-commit +149 -2
- package/lib/__tests__/detect-deps-dsh-profile.test.js +106 -0
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +140 -0
- package/lib/__tests__/gate-runner.test.js +29 -2
- package/lib/__tests__/issue-586-principles-cmdline.test.js +35 -0
- package/lib/__tests__/issue-618-gate-not-run.test.js +100 -0
- package/lib/__tests__/shared-utils.test.js +21 -0
- package/lib/check.js +6 -2
- package/lib/detect-deps.js +42 -10
- package/lib/doctor-tui.js +111 -21
- package/lib/gate-runner.js +111 -37
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/principles.js +48 -6
- package/lib/shared-utils.js +8 -0
- package/mock-policy/AGENTS.md +3 -3
- package/mock-policy/gate-m3.ts +57 -23
- package/mutation/AGENTS.md +3 -3
- package/mutation/__tests__/pitest-gradle-availability.test.ts +74 -0
- package/mutation/__tests__/test-filter-parity.test.ts +5 -2
- package/mutation/gate-m.ts +30 -24
- package/mutation/runners/mutmut-runner.ts +80 -30
- package/mutation/runners/pitest-runner.ts +60 -42
- package/package.json +14 -2
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/lib/command.d.ts +38 -0
- package/plugins/dsh/lib/command.d.ts.map +1 -0
- package/plugins/dsh/lib/command.js +87 -0
- package/plugins/dsh/lib/credentials.d.ts +39 -0
- package/plugins/dsh/lib/credentials.d.ts.map +1 -0
- package/plugins/dsh/lib/credentials.js +57 -0
- package/plugins/dsh/lib/delphi-config.d.ts +129 -0
- package/plugins/dsh/lib/delphi-config.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-config.js +369 -0
- package/plugins/dsh/lib/delphi-run.d.ts +24 -0
- package/plugins/dsh/lib/delphi-run.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi-run.js +113 -0
- package/plugins/dsh/lib/delphi.d.ts +94 -0
- package/plugins/dsh/lib/delphi.d.ts.map +1 -0
- package/plugins/dsh/lib/delphi.js +207 -0
- package/plugins/dsh/lib/dialect.d.ts +24 -0
- package/plugins/dsh/lib/dialect.d.ts.map +1 -0
- package/plugins/dsh/lib/dialect.js +36 -0
- package/plugins/dsh/lib/evidence.d.ts +70 -0
- package/plugins/dsh/lib/evidence.d.ts.map +1 -0
- package/plugins/dsh/lib/evidence.js +92 -0
- package/plugins/dsh/lib/gate-runner.d.ts +49 -0
- package/plugins/dsh/lib/gate-runner.d.ts.map +1 -0
- package/plugins/dsh/lib/gate-runner.js +95 -0
- package/plugins/dsh/lib/git-context.d.ts +26 -0
- package/plugins/dsh/lib/git-context.d.ts.map +1 -0
- package/plugins/dsh/lib/git-context.js +30 -0
- package/plugins/dsh/lib/hooks.d.ts +42 -0
- package/plugins/dsh/lib/hooks.d.ts.map +1 -0
- package/plugins/dsh/lib/hooks.js +113 -0
- package/plugins/dsh/lib/index.d.ts +20 -0
- package/plugins/dsh/lib/index.d.ts.map +1 -0
- package/plugins/dsh/lib/index.js +430 -0
- package/plugins/dsh/lib/skill-provider.d.ts +15 -0
- package/plugins/dsh/lib/skill-provider.d.ts.map +1 -0
- package/plugins/dsh/lib/skill-provider.js +205 -0
- package/plugins/dsh/lib/sprint.d.ts +60 -0
- package/plugins/dsh/lib/sprint.d.ts.map +1 -0
- package/plugins/dsh/lib/sprint.js +92 -0
- package/plugins/dsh/package.json +14 -8
- package/plugins/dsh/scripts/verify-skill-provider.mjs +113 -0
- package/plugins/dsh/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/dsh/src/command-single-form.test.ts +67 -0
- package/plugins/dsh/src/command.test.ts +21 -17
- package/plugins/dsh/src/command.ts +19 -21
- package/plugins/dsh/src/compat.test.ts +200 -0
- package/plugins/dsh/src/contract.test.ts +7 -1
- package/plugins/dsh/src/credentials.test.ts +93 -0
- package/plugins/dsh/src/credentials.ts +71 -0
- package/plugins/dsh/src/delphi-cancellation.test.ts +71 -0
- package/plugins/dsh/src/delphi-config-apikey-override.test.ts +94 -0
- package/plugins/dsh/src/delphi-config-credential-refs.test.ts +75 -0
- package/plugins/dsh/src/delphi-config.ts +146 -53
- package/plugins/dsh/src/delphi-run.ts +27 -4
- package/plugins/dsh/src/delphi.ts +133 -65
- package/plugins/dsh/src/evidence.test.ts +74 -0
- package/plugins/dsh/src/evidence.ts +141 -0
- package/plugins/dsh/src/gate-runner.test.ts +14 -6
- package/plugins/dsh/src/gate-runner.ts +68 -5
- package/plugins/dsh/src/git-context.test.ts +39 -0
- package/plugins/dsh/src/git-context.ts +38 -0
- package/plugins/dsh/src/hooks.test.ts +87 -0
- package/plugins/dsh/src/hooks.ts +126 -0
- package/plugins/dsh/src/index.test.ts +63 -6
- package/plugins/dsh/src/index.ts +340 -13
- package/plugins/dsh/src/skill-provider.test.ts +194 -0
- package/plugins/dsh/src/skill-provider.ts +231 -0
- package/plugins/dsh/src/sprint.test.ts +64 -0
- package/plugins/dsh/src/sprint.ts +122 -0
- package/plugins/dsh/src/tri-state.test.ts +123 -0
- package/plugins/opencode/index.ts +192 -58
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +3 -3
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +3 -3
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +3 -3
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +3 -3
- package/principles/AGENTS.md +3 -3
- package/principles/__tests__/analyze-read-failure.test.ts +70 -0
- package/principles/__tests__/baseline-prune.test.ts +61 -0
- package/principles/__tests__/cli-files-separators.test.ts +69 -0
- package/principles/__tests__/config-merge-warnings.test.ts +148 -0
- package/principles/adapters/java.ts +22 -3
- package/principles/adapters/typescript.ts +70 -29
- package/principles/analyzer.ts +36 -3
- package/principles/baseline-prune.ts +19 -7
- package/principles/config.ts +56 -35
- package/principles/index.ts +122 -40
- package/principles/rules/clean-code/deep-nesting.ts +36 -22
- package/principles/rules/clean-code/unused-imports.ts +30 -19
- package/principles/rules/solid/dip.ts +30 -20
- package/principles/rules/solid/lsp.ts +17 -2
- package/principles/rules/solid/ocp.ts +33 -23
- package/principles/rules/solid/srp.ts +65 -23
- package/scripts/delphi-external-review.cjs +25 -2
- package/scripts/sync-package-content.js +28 -1
- package/skills/delphi-review/AGENTS.md +3 -3
- package/skills/sprint-flow/AGENTS.md +3 -3
- package/skills/test-specification-alignment/AGENTS.md +3 -3
package/lib/gate-runner.js
CHANGED
|
@@ -129,20 +129,63 @@ function firstExisting(candidates) {
|
|
|
129
129
|
}
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/**
|
|
133
|
+
* Write the `--all` file list to a temporary file and return its path.
|
|
134
|
+
*
|
|
135
|
+
* The list is one line per tracked file (thousands on a real repository), so it
|
|
136
|
+
* cannot travel as a `bash -c` argument: on Windows the whole script is a single
|
|
137
|
+
* command-line string capped at 32,767 characters, and exceeding it surfaced as
|
|
138
|
+
* `spawnSync bash ENAMETOOLONG` — Gate 3 could not run at all under `--all`.
|
|
139
|
+
*
|
|
140
|
+
* Inlining was also semantically broken: `JSON.stringify` escapes the separators
|
|
141
|
+
* to a literal `\n`, and bash does not interpret escape sequences inside double
|
|
142
|
+
* quotes, so the gates received one long non-existent path instead of a list.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} contents newline-separated file list
|
|
145
|
+
* @returns {string} path to the temp file
|
|
146
|
+
*/
|
|
147
|
+
function writeAllModeFileList(contents) {
|
|
148
|
+
const listFile = path.join(os.tmpdir(), `xp-gate-all-files-${process.pid}-${Date.now()}.txt`);
|
|
149
|
+
try {
|
|
150
|
+
fs.writeFileSync(listFile, contents, 'utf8');
|
|
151
|
+
} catch (err) {
|
|
152
|
+
throw new Error(`xp-gate: cannot write the --all file list to ${listFile}: ${err.message}`);
|
|
153
|
+
}
|
|
154
|
+
return listFile;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Remove the temporary file list, if one was written.
|
|
159
|
+
*
|
|
160
|
+
* Best effort: a leftover file in the temp directory is not worth failing a gate
|
|
161
|
+
* over, but a failure other than ENOENT is named so it does not go unnoticed.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} listFile path to remove, or the empty string
|
|
164
|
+
*/
|
|
165
|
+
function removeFileList(listFile) {
|
|
166
|
+
if (!listFile) return;
|
|
167
|
+
try {
|
|
168
|
+
fs.unlinkSync(listFile);
|
|
169
|
+
} catch (err) {
|
|
170
|
+
if (err.code !== 'ENOENT') {
|
|
171
|
+
console.error(`[xp-gate] could not remove ${listFile}: ${err.message}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
132
176
|
/**
|
|
133
177
|
* The shell lines that populate CHANGED_FILES, chosen by scan scope.
|
|
134
178
|
*
|
|
135
179
|
* Extracted so `runGateAdapter` stays readable: the diff form is three lines of
|
|
136
|
-
* git plumbing and the tree form is a single
|
|
137
|
-
*
|
|
180
|
+
* git plumbing and the tree form is a single read, and mixing them inline made
|
|
181
|
+
* the wrapper harder to follow than the thing it wraps.
|
|
138
182
|
*
|
|
139
183
|
* @param {boolean} allMode true for `--all` (whole tree)
|
|
140
|
-
* @param {string} allModeFiles pre-resolved file list for all mode
|
|
141
184
|
* @returns {string[]} shell lines
|
|
142
185
|
*/
|
|
143
|
-
function buildFileSourceLines(allMode
|
|
186
|
+
function buildFileSourceLines(allMode) {
|
|
144
187
|
if (allMode) {
|
|
145
|
-
return ['#589: --all scans the tracked tree, not the diff',
|
|
188
|
+
return ['#589: --all scans the tracked tree, not the diff ($5 = file list)', 'CHANGED_FILES="$(cat "$5" 2>/dev/null || true)"'];
|
|
146
189
|
}
|
|
147
190
|
// The historical diff form, unchanged: staged first, then anything vs HEAD.
|
|
148
191
|
// Interpolating the literal `${CHANGED_FILES}` brace-by-brace would be torn apart
|
|
@@ -200,16 +243,20 @@ function runGateAdapter(scriptPath, cwd = process.cwd(), opts = {}) {
|
|
|
200
243
|
// CHANGED_FILES must see the whole tree, not the diff — otherwise a full check
|
|
201
244
|
// scans less than an ordinary commit and reads like a pass.
|
|
202
245
|
const allMode = opts.scope === 'all';
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
246
|
+
const listFile = allMode ? writeAllModeFileList(resolveAllModeChangedFiles(cwd)) : '';
|
|
247
|
+
try {
|
|
248
|
+
const wrap = buildPreamble(allMode, buildFileSourceLines(allMode));
|
|
249
|
+
|
|
250
|
+
// execFileSync avoids an extra /bin/sh layer, so bash receives the script as a
|
|
251
|
+
// single argv arg (no shell-metacharacter injection through the path).
|
|
252
|
+
execFileSync(
|
|
253
|
+
bashCommand,
|
|
254
|
+
['--noprofile', '--norc', '-c', wrap, 'xp-gate', commonScript, nowMsScript, scriptPath, process.execPath, listFile],
|
|
255
|
+
{ stdio: 'inherit', cwd }
|
|
256
|
+
);
|
|
257
|
+
} finally {
|
|
258
|
+
removeFileList(listFile);
|
|
259
|
+
}
|
|
213
260
|
}
|
|
214
261
|
|
|
215
262
|
/**
|
|
@@ -253,6 +300,16 @@ function buildPreamble(allMode, fileSourceLines) {
|
|
|
253
300
|
*/
|
|
254
301
|
const TS_GATE_TIMEOUT_MS = 120000;
|
|
255
302
|
|
|
303
|
+
/**
|
|
304
|
+
* #618: sentinel for "this gate ran nothing at all" -- no TypeScript module and
|
|
305
|
+
* no bash fallback. It must never be conflated with 0 ("ran, found nothing").
|
|
306
|
+
* Returned up through runGate() so the --all summary counts it as skipped
|
|
307
|
+
* rather than passed: three advertised gates were analysing nothing and sitting
|
|
308
|
+
* inside the passed count, which is the same defect class as a vacuous pass
|
|
309
|
+
* elsewhere -- a dimension nobody measured, scored as if measured.
|
|
310
|
+
*/
|
|
311
|
+
const GATE_NOT_RUN = 'NOT_RUN';
|
|
312
|
+
|
|
256
313
|
/**
|
|
257
314
|
* Run a TypeScript gate module via npx tsx.
|
|
258
315
|
* Finds the .ts entry point, spawns tsx with --run flag.
|
|
@@ -266,11 +323,13 @@ function runTsGate(gateModule, targetPath, opts = {}) {
|
|
|
266
323
|
const gateNum = gateModule.replace('gate-', '');
|
|
267
324
|
const script = resolveGateScript(gateNum, targetCwd);
|
|
268
325
|
if (script) {
|
|
269
|
-
runGateAdapter(script, targetCwd, opts);
|
|
270
|
-
} else {
|
|
271
|
-
console.log(`Gate ${gateModule}: TypeScript module not found and no bash fallback available.`);
|
|
326
|
+
return runGateAdapter(script, targetCwd, opts);
|
|
272
327
|
}
|
|
273
|
-
return 0
|
|
328
|
+
// #618: nothing ran. Before this, the function fell through to `return 0`
|
|
329
|
+
// and the --all summary counted the gate as passed.
|
|
330
|
+
console.log(`Gate ${gateModule}: TypeScript module not found and no bash fallback available.`);
|
|
331
|
+
console.log(`⏭️ SKIPPED - Gate ${gateModule} did not run (no module, no bash fallback)`);
|
|
332
|
+
return GATE_NOT_RUN;
|
|
274
333
|
}
|
|
275
334
|
|
|
276
335
|
const args = ['-y', 'tsx', entry, '--run'];
|
|
@@ -484,9 +543,8 @@ function findConfig(basePath, fileName) {
|
|
|
484
543
|
/**
|
|
485
544
|
* The basename of the first `gate-<num>-*.sh` in a directory, or null.
|
|
486
545
|
*
|
|
487
|
-
* A missing directory is the normal case
|
|
488
|
-
* so it returns null rather than throwing
|
|
489
|
-
* match" because the caller falls through to the next candidate either way.
|
|
546
|
+
* A missing directory is the normal case -- most projects have no
|
|
547
|
+
* `githooks/gates/` -- so it returns null rather than throwing.
|
|
490
548
|
*
|
|
491
549
|
* @param {string} dir directory to list
|
|
492
550
|
* @param {string} gateNum gate number as a string
|
|
@@ -501,22 +559,37 @@ function matchGateFileIn(dir, gateNum) {
|
|
|
501
559
|
}
|
|
502
560
|
}
|
|
503
561
|
|
|
562
|
+
/**
|
|
563
|
+
* Locate the bash script that implements a gate, for the TS-less fallback path.
|
|
564
|
+
*
|
|
565
|
+
* Candidate order:
|
|
566
|
+
* 1. `<cwd>/githooks/gates/gate-<n>-*.sh` -- a consumer's OWN gate scripts. The
|
|
567
|
+
* hyphenated name is what lets a project keep several variants of one gate
|
|
568
|
+
* (`gate-9-public-fallback.sh`), which the flat `gate-<n>.sh` name cannot
|
|
569
|
+
* express. `xp-gate init` does not create this directory and the npm package
|
|
570
|
+
* does not ship it, so in a stock project it simply never matches. It is
|
|
571
|
+
* still a supported extension point, pinned by the REQ-standalone-gates-05
|
|
572
|
+
* and -06 tests in lib/__tests__/gate-runner.test.js.
|
|
573
|
+
* 2. `<cwd>/githooks/gate-<n>.sh` -- the flat copy `xp-gate init` installs.
|
|
574
|
+
* 3. `~/.config/xp-gate/adapters/gate-<n>.sh` -- the global install.
|
|
575
|
+
*
|
|
576
|
+
* NOTE: this repository's own `githooks/gates/` was a different thing entirely --
|
|
577
|
+
* five drifted copies of the canonical gates that only ever matched inside this
|
|
578
|
+
* checkout, shadowing `githooks/gate-N.sh` (36 lines apart for gate-9, still
|
|
579
|
+
* carrying the GATE_10_STATUS artifact from #532). Those were deleted; the
|
|
580
|
+
* lookup path above was not, because consumer projects legitimately use it.
|
|
581
|
+
*
|
|
582
|
+
* @param {string} gateNum gate number as a string
|
|
583
|
+
* @param {string} cwd project root to search
|
|
584
|
+
* @returns {string|null}
|
|
585
|
+
*/
|
|
504
586
|
function resolveGateScript(gateNum, cwd = process.cwd()) {
|
|
505
|
-
const
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
];
|
|
587
|
+
const gatesDir = path.join(cwd, 'githooks', 'gates');
|
|
588
|
+
const flexible = matchGateFileIn(gatesDir, gateNum);
|
|
589
|
+
if (flexible) return path.join(gatesDir, flexible);
|
|
509
590
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
if (pattern.includes('*')) {
|
|
513
|
-
const dir = path.dirname(pattern);
|
|
514
|
-
const match = matchGateFileIn(dir, gateNum);
|
|
515
|
-
if (match) return path.join(dir, match);
|
|
516
|
-
} else if (fs.existsSync(pattern)) {
|
|
517
|
-
return pattern;
|
|
518
|
-
}
|
|
519
|
-
}
|
|
591
|
+
const projectScript = path.join(cwd, 'githooks', `gate-${gateNum}.sh`);
|
|
592
|
+
if (fs.existsSync(projectScript)) return projectScript;
|
|
520
593
|
|
|
521
594
|
// Check global install
|
|
522
595
|
const globalDir = path.join(os.homedir(), '.config', 'xp-gate', 'adapters');
|
|
@@ -573,6 +646,7 @@ async function runGate(gateId, targetPath, opts = {}) {
|
|
|
573
646
|
// failing gate from a passing one. Handlers that return nothing at all
|
|
574
647
|
// (gate 2, whose execFileSync throws on a non-zero exit instead) are
|
|
575
648
|
// still treated as pass, because their failure signal is the throw below.
|
|
649
|
+
if (result === GATE_NOT_RUN) return GATE_NOT_RUN;
|
|
576
650
|
return typeof result === 'number' ? result : 0;
|
|
577
651
|
} catch (err) {
|
|
578
652
|
// Covers a synchronous throw AND a rejection from the awaited handler.
|
|
@@ -585,4 +659,4 @@ async function runGate(gateId, targetPath, opts = {}) {
|
|
|
585
659
|
return 1;
|
|
586
660
|
}
|
|
587
661
|
|
|
588
|
-
module.exports = { GATE_REGISTRY, getGateInfo, getAllGates, runGate, resolveAlias, getAliases, runGateAdapter, findTsGateEntry, resolveGateScript, SOURCE_FILE_EXTENSIONS, resolveAllModeChangedFiles, describeEmptyChangedFiles, describeChangedFilesScope };
|
|
662
|
+
module.exports = { GATE_REGISTRY, GATE_NOT_RUN, getGateInfo, getAllGates, runGate, resolveAlias, getAliases, runGateAdapter, findTsGateEntry, resolveGateScript, SOURCE_FILE_EXTENSIONS, resolveAllModeChangedFiles, describeEmptyChangedFiles, describeChangedFilesScope };
|
package/lib/principles.js
CHANGED
|
@@ -44,21 +44,41 @@ function buildPrinciplesInvocation(files, format) {
|
|
|
44
44
|
const tsxCli = [findLocalTsxCli(), path.resolve(__dirname, '..', 'node_modules', 'tsx', 'dist', 'cli.mjs')].find(
|
|
45
45
|
(candidate) => candidate && fs.existsSync(candidate)
|
|
46
46
|
);
|
|
47
|
+
// Second-best shape: run npm's own npx entry with node.exe. A bare `npx` cannot be
|
|
48
|
+
// spawned without a shell on Windows (CreateProcess only resolves `.exe`, and the
|
|
49
|
+
// installed shim is `npx.cmd`), which surfaced as spawnSync ENOENT (#<new>).
|
|
50
|
+
const npxCli = tsxCli ? null : findNpxCli();
|
|
47
51
|
|
|
48
|
-
|
|
49
|
-
|
|
52
|
+
let command;
|
|
53
|
+
let prefix;
|
|
54
|
+
let useShell;
|
|
55
|
+
if (tsxCli) {
|
|
56
|
+
command = process.execPath;
|
|
57
|
+
prefix = [tsxCli, entry];
|
|
58
|
+
useShell = false;
|
|
59
|
+
} else if (npxCli) {
|
|
60
|
+
command = process.execPath;
|
|
61
|
+
prefix = [npxCli, '-y', 'tsx', entry];
|
|
62
|
+
useShell = false;
|
|
63
|
+
} else {
|
|
64
|
+
// Last resort: a bare npx on PATH. Only a shell can resolve it on Windows, so the
|
|
65
|
+
// flag has to travel with the plan instead of being hard-coded false.
|
|
66
|
+
command = 'npx';
|
|
67
|
+
prefix = ['-y', 'tsx', entry];
|
|
68
|
+
useShell = process.platform === 'win32';
|
|
69
|
+
}
|
|
50
70
|
|
|
51
71
|
if (listChars <= ARGV_CHAR_BUDGET) {
|
|
52
72
|
return {
|
|
53
73
|
command,
|
|
54
74
|
args: [...prefix, '--files', files.join(' '), '--format', format],
|
|
55
|
-
useShell
|
|
75
|
+
useShell,
|
|
56
76
|
cleanup: null,
|
|
57
77
|
};
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
// By reference: one short argument instead of thousands of characters.
|
|
61
|
-
return buildByReferenceInvocation(prefix, files, format, command);
|
|
81
|
+
return buildByReferenceInvocation(prefix, files, format, command, useShell);
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
/**
|
|
@@ -73,9 +93,10 @@ function buildPrinciplesInvocation(files, format) {
|
|
|
73
93
|
* @param {string[]} files source files to analyse
|
|
74
94
|
* @param {string} format 'json' or 'text'
|
|
75
95
|
* @param {string} command executable to run
|
|
96
|
+
* @param {boolean} useShell whether the spawn needs a shell to resolve the command
|
|
76
97
|
* @returns {{command: string, args: string[], useShell: boolean, cleanup: string|null}}
|
|
77
98
|
*/
|
|
78
|
-
function buildByReferenceInvocation(prefix, files, format, command) {
|
|
99
|
+
function buildByReferenceInvocation(prefix, files, format, command, useShell) {
|
|
79
100
|
const listFile = path.join(os.tmpdir(), `xp-gate-principles-${process.pid}-${Date.now()}.txt`);
|
|
80
101
|
try {
|
|
81
102
|
fs.writeFileSync(listFile, files.join('\n'));
|
|
@@ -88,7 +109,7 @@ function buildByReferenceInvocation(prefix, files, format, command) {
|
|
|
88
109
|
command,
|
|
89
110
|
// The @ prefix is the engine's own "read the list from this file" form.
|
|
90
111
|
args: [...prefix, '--files', `@${listFile}`, '--format', format],
|
|
91
|
-
useShell
|
|
112
|
+
useShell,
|
|
92
113
|
cleanup: listFile,
|
|
93
114
|
};
|
|
94
115
|
}
|
|
@@ -120,6 +141,27 @@ function findLocalTsxCli() {
|
|
|
120
141
|
return null;
|
|
121
142
|
}
|
|
122
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Path to npm's own npx CLI entry (`npx-cli.js`), or null.
|
|
146
|
+
*
|
|
147
|
+
* `spawnSync('npx', ...)` without a shell works on POSIX (npx is a symlinked JS
|
|
148
|
+
* script) but fails on Windows with ENOENT: CreateProcess resolves only `.exe`
|
|
149
|
+
* names, and the installed entry point is `npx.cmd`. Running npm's JS entry with
|
|
150
|
+
* the current node binary is equivalent and platform-independent.
|
|
151
|
+
*
|
|
152
|
+
* @returns {string|null}
|
|
153
|
+
*/
|
|
154
|
+
function findNpxCli() {
|
|
155
|
+
const execDir = path.dirname(process.execPath);
|
|
156
|
+
const candidates = [
|
|
157
|
+
// Windows/self-contained Node: <nodeDir>/node_modules/npm/bin/npx-cli.js
|
|
158
|
+
path.join(execDir, 'node_modules', 'npm', 'bin', 'npx-cli.js'),
|
|
159
|
+
// POSIX layout: <prefix>/bin/node → <prefix>/lib/node_modules/npm/bin/npx-cli.js
|
|
160
|
+
path.resolve(execDir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npx-cli.js'),
|
|
161
|
+
];
|
|
162
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || null;
|
|
163
|
+
}
|
|
164
|
+
|
|
123
165
|
/**
|
|
124
166
|
* Was this failure the tool not running, or the code having problems?
|
|
125
167
|
*
|
package/lib/shared-utils.js
CHANGED
|
@@ -57,6 +57,14 @@ function copyAdapters(srcDir, destDir) {
|
|
|
57
57
|
if (fs.existsSync(adapterCommon)) {
|
|
58
58
|
fs.copyFileSync(adapterCommon, path.join(destDir, 'adapter-common.sh'));
|
|
59
59
|
}
|
|
60
|
+
// Shared shell helpers the fragments `source` at runtime (ccn-ratchet.sh and
|
|
61
|
+
// friends). Without this, gate-3.sh resolves $GATE_DIR/lib/ccn-ratchet.sh to a
|
|
62
|
+
// path that no installer ever created, and `.` failing there silently disabled
|
|
63
|
+
// the complexity ratchet: a CCN regression was graded PASS.
|
|
64
|
+
const libSrc = path.join(srcDir, 'hooks', 'lib');
|
|
65
|
+
if (fs.existsSync(libSrc)) {
|
|
66
|
+
copyDirRecursive(libSrc, path.join(destDir, 'lib'));
|
|
67
|
+
}
|
|
60
68
|
if (fs.existsSync(adaptersDir)) {
|
|
61
69
|
fs.readdirSync(adaptersDir).forEach(f => {
|
|
62
70
|
if (f.endsWith('.sh')) {
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
|
package/mock-policy/gate-m3.ts
CHANGED
|
@@ -86,10 +86,28 @@ interface ValidateImportInput {
|
|
|
86
86
|
engine: MockDecisionEngine;
|
|
87
87
|
layer: ReturnType<typeof detectTestLayer>;
|
|
88
88
|
severity: 'warning' | 'error';
|
|
89
|
+
lenientUnit: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Is this file exempt from the strategy check?
|
|
94
|
+
*
|
|
95
|
+
* A lenient unit layer only RECOMMENDS mocking every dependency. Reporting a test
|
|
96
|
+
* that injects a hand-written fake instead would flag tests that are already
|
|
97
|
+
* isolated, which is how a gate teaches people to ignore it.
|
|
98
|
+
*/
|
|
99
|
+
function isLenientUnitFile(
|
|
100
|
+
lenientUnit: boolean,
|
|
101
|
+
layer: ReturnType<typeof detectTestLayer>,
|
|
102
|
+
): boolean {
|
|
103
|
+
if (!lenientUnit) return false;
|
|
104
|
+
return layer === 'unit' || layer === 'unknown';
|
|
89
105
|
}
|
|
90
106
|
|
|
91
107
|
function validateImport(input: ValidateImportInput): MockPolicyViolation[] {
|
|
92
|
-
const { testFile, content, importPath, engine, layer, severity } = input;
|
|
108
|
+
const { testFile, content, importPath, engine, layer, severity, lenientUnit } = input;
|
|
109
|
+
if (isLenientUnitFile(lenientUnit, layer)) return [];
|
|
110
|
+
|
|
93
111
|
const decision = engine.decide(importPath, layer);
|
|
94
112
|
const actualStrategy = detectMockUsage(content, importPath);
|
|
95
113
|
const violations: MockPolicyViolation[] = [];
|
|
@@ -117,9 +135,22 @@ async function validateFile(
|
|
|
117
135
|
engine: MockDecisionEngine,
|
|
118
136
|
projectRoot: string,
|
|
119
137
|
severity: 'warning' | 'error',
|
|
138
|
+
lenientUnit: boolean,
|
|
120
139
|
): Promise<MockPolicyViolation[]> {
|
|
121
140
|
const fullPath = isAbsolute(testFile) ? testFile : join(projectRoot, testFile);
|
|
122
|
-
|
|
141
|
+
// A changed-file list naming a file that cannot be read is broken input, and
|
|
142
|
+
// gate-m3.test.ts pins the rejection. Reporting "no violations" for a file nobody
|
|
143
|
+
// could read is the one failure a gate must never have, so the error is caught
|
|
144
|
+
// only to add the path and then rethrown.
|
|
145
|
+
let content: string;
|
|
146
|
+
try {
|
|
147
|
+
content = await readFile(fullPath, 'utf-8');
|
|
148
|
+
} catch (error) {
|
|
149
|
+
// String(error) rather than (error as Error).message: a thrower is free to
|
|
150
|
+
// reject with a non-Error, and that cast would render the cause as
|
|
151
|
+
// "undefined" — losing the only diagnostic this rethrow exists to add.
|
|
152
|
+
throw new Error(`cannot read test file ${testFile}: ${String(error)}`);
|
|
153
|
+
}
|
|
123
154
|
const layer = detectTestLayer(testFile);
|
|
124
155
|
const imports = collectImports(content);
|
|
125
156
|
|
|
@@ -130,9 +161,25 @@ async function validateFile(
|
|
|
130
161
|
engine,
|
|
131
162
|
layer,
|
|
132
163
|
severity,
|
|
164
|
+
lenientUnit,
|
|
133
165
|
}));
|
|
134
166
|
}
|
|
135
167
|
|
|
168
|
+
/** Every import mentioned across the given test files, deduplicated. */
|
|
169
|
+
async function collectAllImports(testFiles: string[], projectRoot: string): Promise<string[]> {
|
|
170
|
+
const all: string[] = [];
|
|
171
|
+
for (const testFile of testFiles) {
|
|
172
|
+
try {
|
|
173
|
+
const fullPath = isAbsolute(testFile) ? testFile : join(projectRoot, testFile);
|
|
174
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
175
|
+
all.push(...collectImports(content));
|
|
176
|
+
} catch {
|
|
177
|
+
// Unreadable file — the per-file pass reports nothing for it either.
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return [...new Set(all)];
|
|
181
|
+
}
|
|
182
|
+
|
|
136
183
|
export async function runGateM3(
|
|
137
184
|
changedFiles: string[],
|
|
138
185
|
projectRoot: string = process.cwd(),
|
|
@@ -151,38 +198,25 @@ export async function runGateM3(
|
|
|
151
198
|
const config = await loadMockPolicyConfig(projectRoot);
|
|
152
199
|
const severity: 'warning' | 'error' = config.severity;
|
|
153
200
|
|
|
154
|
-
// Collect all imports from all test files
|
|
155
|
-
const allImports: string[] = [];
|
|
156
|
-
for (const testFile of testFiles) {
|
|
157
|
-
try {
|
|
158
|
-
const fullPath = isAbsolute(testFile) ? testFile : join(projectRoot, testFile);
|
|
159
|
-
const content = await readFile(fullPath, 'utf-8');
|
|
160
|
-
allImports.push(...collectImports(content));
|
|
161
|
-
} catch {
|
|
162
|
-
// File not readable — skip
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
201
|
const scope = await scanProjectScope({
|
|
167
202
|
projectRoot,
|
|
168
|
-
imports:
|
|
203
|
+
imports: await collectAllImports(testFiles, projectRoot),
|
|
169
204
|
boundary: config.projectBoundary,
|
|
170
205
|
});
|
|
171
206
|
|
|
172
207
|
const engine = new MockDecisionEngine(scope, config, projectRoot);
|
|
173
|
-
|
|
208
|
+
// Resolved once from config: a lenient unit layer is advisory, so its files skip
|
|
209
|
+
// strategy comparison entirely.
|
|
210
|
+
const lenientUnit = config.layers.unit.mockPolicy === 'lenient';
|
|
174
211
|
|
|
212
|
+
const allViolations: MockPolicyViolation[] = [];
|
|
175
213
|
let integrationCount = 0;
|
|
176
214
|
for (const testFile of testFiles) {
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
const violations = await validateFile(testFile, engine, projectRoot, severity);
|
|
181
|
-
allViolations.push(...violations);
|
|
215
|
+
if (detectTestLayer(testFile) === 'integration') integrationCount++;
|
|
216
|
+
allViolations.push(...await validateFile(testFile, engine, projectRoot, severity, lenientUnit));
|
|
182
217
|
}
|
|
183
218
|
|
|
184
|
-
const
|
|
185
|
-
const blocked = severity === 'error' && hasBlockableViolations;
|
|
219
|
+
const blocked = severity === 'error' && allViolations.some(v => v.severity === 'error');
|
|
186
220
|
|
|
187
221
|
return {
|
|
188
222
|
exitCode: blocked ? 1 : 0,
|
package/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-10-
|
|
4
|
-
**Commit:**
|
|
3
|
+
**Generated:** 2026-10-28
|
|
4
|
+
**Commit:** 0f58c300
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.32.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**Gate M** (incremental mutation testing) + **Gate M2** helpers (test-layer detection used by `src/mock-policy/`). Pre-push quality gate. TypeScript-only; uses Stryker.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-630
|
|
3
|
+
* @intent A Gradle build file alone is not enough: PITest is launched through
|
|
4
|
+
* `./gradlew` or a `gradle` on PATH. The pre-existing isAvailable tests
|
|
5
|
+
* mocked EVERY existsSync to true and mocked the gradle launcher to
|
|
6
|
+
* succeed, so "build.gradle configures pitest but no launcher can be
|
|
7
|
+
* run" was never exercised, and deleting the launcher guard left the
|
|
8
|
+
* suite green. On a machine with a pom-free Gradle project and no gradle
|
|
9
|
+
* installed, isAvailable() would claim the runner works, and Gate M
|
|
10
|
+
* would fail later with a confusing "gradle not found".
|
|
11
|
+
*
|
|
12
|
+
* These cases live here rather than in pitest-runner.test.ts because
|
|
13
|
+
* that file carries pre-existing warnings; editing it would trigger the
|
|
14
|
+
* Boy Scout "<=5 warnings must clear to zero" rule for unrelated code.
|
|
15
|
+
* @covers AC-630-01, AC-630-02
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
19
|
+
import { PitestRunner } from '../runners/pitest-runner';
|
|
20
|
+
|
|
21
|
+
// `fs` is mocked module-wide; each case sets the answers it needs.
|
|
22
|
+
vi.mock('fs', () => ({
|
|
23
|
+
existsSync: vi.fn(),
|
|
24
|
+
readFileSync: vi.fn(),
|
|
25
|
+
readdirSync: vi.fn(),
|
|
26
|
+
}));
|
|
27
|
+
// `execSync` decides whether a launcher runs; `spawn` is unused by isAvailable
|
|
28
|
+
// but is imported by the runner module.
|
|
29
|
+
vi.mock('child_process', () => ({ execSync: vi.fn(), spawn: vi.fn() }));
|
|
30
|
+
|
|
31
|
+
describe('PitestRunner.isAvailable — Gradle launcher requirement', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('AC-630-01: no ./gradlew and gradle not runnable -> unavailable despite a pitest build file', async () => {
|
|
37
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
38
|
+
const { execSync } = await import('child_process');
|
|
39
|
+
// Maven throws; the gradle launcher also throws (not installed).
|
|
40
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
41
|
+
throw new Error('not found');
|
|
42
|
+
});
|
|
43
|
+
// build.gradle exists and configures pitest; ./gradlew does NOT exist.
|
|
44
|
+
vi.mocked(existsSync).mockImplementation((p: unknown) =>
|
|
45
|
+
String(p).endsWith('build.gradle'),
|
|
46
|
+
);
|
|
47
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
48
|
+
'plugins { id "info.solidsoft.pitest" version "1.15.0" }',
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const runner = new PitestRunner();
|
|
52
|
+
expect(await runner.isAvailable()).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('AC-630-02: ./gradlew present -> the gradle launcher is not required on PATH', async () => {
|
|
56
|
+
const { existsSync, readFileSync } = await import('fs');
|
|
57
|
+
const { execSync } = await import('child_process');
|
|
58
|
+
// Every launcher probe throws, but ./gradlew exists, so a wrapper-based
|
|
59
|
+
// build must still count as available.
|
|
60
|
+
vi.mocked(execSync).mockImplementation(() => {
|
|
61
|
+
throw new Error('not found');
|
|
62
|
+
});
|
|
63
|
+
vi.mocked(existsSync).mockImplementation((p: unknown) => {
|
|
64
|
+
const s = String(p);
|
|
65
|
+
return s.includes('gradlew') || s.endsWith('build.gradle.kts');
|
|
66
|
+
});
|
|
67
|
+
vi.mocked(readFileSync).mockReturnValue(
|
|
68
|
+
'plugins { id("info.solidsoft.gradle.pitest") version "1.15.0" }',
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const runner = new PitestRunner();
|
|
72
|
+
expect(await runner.isAvailable()).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -31,7 +31,10 @@ describe('test-path filter parity between pre-push and gate-m (#480)', () => {
|
|
|
31
31
|
it('both sides exclude declaration files and spec-named tests', () => {
|
|
32
32
|
expect(prePushFilter).toContain(String.raw`\.d\.ts$`);
|
|
33
33
|
expect(prePushFilter).toContain(String.raw`\.spec\.`);
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
// gate-m states each exclusion as an entry in EXCLUDED_PATH_PATTERNS rather
|
|
35
|
+
// than as an endsWith/includes call, so the parity assertion follows the
|
|
36
|
+
// regex. What it pins is unchanged: the TS side still excludes both.
|
|
37
|
+
expect(gateM).toContain(String.raw`pattern: /\.d\.ts$/`);
|
|
38
|
+
expect(gateM).toContain(String.raw`pattern: /\.spec\./`);
|
|
36
39
|
});
|
|
37
40
|
});
|
package/mutation/gate-m.ts
CHANGED
|
@@ -80,39 +80,45 @@ function parseTimeout(options: GateMOptions, args: string[], i: number): void {
|
|
|
80
80
|
|
|
81
81
|
// ── File filtering: excludes tests, declarations, adapters ──
|
|
82
82
|
// Delegate per-language filtering to a helper.
|
|
83
|
-
|
|
83
|
+
//
|
|
84
|
+
// Every exclusion is a "does this normalized path look like X?" question, so
|
|
85
|
+
// they are stated as data rather than as a chain of early returns. Keeping the
|
|
86
|
+
// list declarative is what makes the rules reviewable: a new convention is one
|
|
87
|
+
// line, and the reasons stay attached to the pattern they justify.
|
|
84
88
|
export function filterSourceFiles(files: string[]): string[] {
|
|
85
89
|
return files.filter(file => {
|
|
86
|
-
const ext = path.extname(file);
|
|
87
90
|
// One separator view for every pattern below: the path arrives from the
|
|
88
91
|
// adapter layer, which on Windows yields backslash-delimited relative paths.
|
|
89
92
|
const normalized = file.replace(/\\/g, '/');
|
|
90
|
-
|
|
91
|
-
if (normalized.includes('.test.') || normalized.includes('_test.')) return false;
|
|
92
|
-
// Skip spec-named test files: the pre-push filter already excludes them, and
|
|
93
|
-
// a mutant inside a *.spec.ts is scored against a production budget (#480).
|
|
94
|
-
if (normalized.includes('.spec.')) return false;
|
|
95
|
-
// Skip test-tree directories: helpers/fixtures/factories there are test
|
|
96
|
-
// infrastructure judged by the suites that run them, not by mutation
|
|
97
|
-
// scoring budgeted for production code (#480)
|
|
98
|
-
if (/(^|\/)(tests?|__tests__)\//.test(normalized)) return false;
|
|
99
|
-
// Skip Java/Kotlin test files: FooTest.java, FooSpec.kt, etc. Case-sensitive
|
|
100
|
-
// "Test"/"Spec"/"IT" suffix matching is what the Surefire/Failsafe conventions
|
|
101
|
-
// actually produce -- `[A-Z]Test\.only` caught MyTest.java but not FooTest.java,
|
|
102
|
-
// so most test classes stayed in the mutation candidate list.
|
|
103
|
-
if (/Test\.(java|kt|kts)$/.test(normalized)) return false;
|
|
104
|
-
if (/Tests\.(java|kt|kts)$/.test(normalized)) return false;
|
|
105
|
-
if (/(IT|Spec)\.(java|kt|kts)$/.test(normalized)) return false;
|
|
106
|
-
// Skip declaration files
|
|
107
|
-
if (normalized.endsWith('.d.ts')) return false;
|
|
108
|
-
// Skip adapter files
|
|
109
|
-
if (normalized.includes('/adapters/')) return false;
|
|
93
|
+
if (isExcludedPath(normalized)) return false;
|
|
110
94
|
// Skip files with no registered runner
|
|
111
|
-
|
|
112
|
-
return runner !== undefined;
|
|
95
|
+
return resolveRunner(path.extname(file)) !== undefined;
|
|
113
96
|
});
|
|
114
97
|
}
|
|
115
98
|
|
|
99
|
+
const EXCLUDED_PATH_PATTERNS: readonly { pattern: RegExp; reason: string }[] = [
|
|
100
|
+
{ pattern: /\.test\.|_test\./, reason: 'test files' },
|
|
101
|
+
// Spec-named test files: the pre-push filter already excludes them, and a
|
|
102
|
+
// mutant inside a *.spec.ts is scored against a production budget (#480).
|
|
103
|
+
{ pattern: /\.spec\./, reason: 'spec files' },
|
|
104
|
+
// Test-tree directories: helpers/fixtures/factories there are test
|
|
105
|
+
// infrastructure judged by the suites that run them, not by mutation
|
|
106
|
+
// scoring budgeted for production code (#480)
|
|
107
|
+
{ pattern: /(^|\/)(tests?|__tests__)\//, reason: 'test trees' },
|
|
108
|
+
// Java/Kotlin test files: FooTest.java, FooSpec.kt, etc. Case-sensitive
|
|
109
|
+
// "Test"/"Spec"/"IT" suffix matching is what the Surefire/Failsafe
|
|
110
|
+
// conventions actually produce -- `[A-Z]Test\.only` caught MyTest.java but
|
|
111
|
+
// not FooTest.java, so most test classes stayed in the candidate list.
|
|
112
|
+
{ pattern: /(Test|Tests|IT|Spec)\.(java|kt|kts)$/, reason: 'JVM test classes' },
|
|
113
|
+
{ pattern: /\.d\.ts$/, reason: 'declaration files' },
|
|
114
|
+
{ pattern: /\/adapters\//, reason: 'adapter files' },
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/** True when `normalized` matches any exclusion rule. */
|
|
118
|
+
function isExcludedPath(normalized: string): boolean {
|
|
119
|
+
return EXCLUDED_PATH_PATTERNS.some(({ pattern }) => pattern.test(normalized));
|
|
120
|
+
}
|
|
121
|
+
|
|
116
122
|
async function fileExists(filePath: string): Promise<boolean> {
|
|
117
123
|
try {
|
|
118
124
|
await fs.access(filePath);
|