@boyingliu01/xp-gate 0.26.0 → 0.27.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/bin/xp-gate.js +161 -28
- package/hooks/pre-commit +27 -10
- package/lib/__tests__/check-alignment-cli.test.mjs +77 -0
- package/lib/__tests__/doctor-469-hook-env.test.js +12 -7
- package/lib/__tests__/doctor-module-resolution-parity.test.js +15 -6
- package/lib/__tests__/doctor-tui-skill-freshness.test.js +16 -4
- package/lib/__tests__/test-alignment.test.ts +37 -0
- package/lib/__tests__/windows-bash-probe.test.js +73 -0
- package/lib/check-alignment-runner.cjs +74 -0
- package/lib/detect-deps.js +82 -19
- package/lib/gate-runner.js +12 -5
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/sprint-status.js +5 -2
- package/lib/test-alignment.ts +136 -12
- package/mock-policy/AGENTS.md +2 -2
- package/mutation/AGENTS.md +2 -2
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/README.md +1 -1
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/scripts/prepack.cjs +2 -2
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/src/skills.test.ts +20 -7
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/scripts/prepack.cjs +0 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/principles/rules/__tests__/clean-code/code-duplication.test.ts +9 -8
- package/principles/rules/__tests__/clean-code/deep-nesting.test.ts +3 -4
- package/principles/rules/__tests__/clean-code/god-class.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/large-file.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/long-function.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/many-exports.test.ts +2 -3
- package/principles/rules/__tests__/clean-code/missing-error-handling.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/too-many-params.test.ts +2 -4
- package/principles/rules/__tests__/clean-code/unused-imports.test.ts +4 -4
- package/principles/rules/__tests__/solid/dip.test.ts +2 -4
- package/principles/rules/__tests__/solid/isp.test.ts +2 -4
- package/principles/rules/__tests__/solid/lsp.test.ts +2 -4
- package/principles/rules/__tests__/solid/ocp.test.ts +2 -4
- package/principles/rules/__tests__/solid/srp.test.ts +2 -4
- package/principles/rules/clean-code/code-duplication.ts +12 -14
- package/principles/rules/clean-code/deep-nesting.ts +18 -20
- package/principles/rules/clean-code/god-class.ts +32 -34
- package/principles/rules/clean-code/large-file.ts +14 -16
- package/principles/rules/clean-code/long-function.ts +26 -28
- package/principles/rules/clean-code/magic-numbers.ts +39 -38
- package/principles/rules/clean-code/many-exports.ts +21 -23
- package/principles/rules/clean-code/missing-error-handling.ts +13 -15
- package/principles/rules/clean-code/too-many-params.ts +15 -17
- package/principles/rules/clean-code/unused-imports.ts +13 -15
- package/principles/rules/solid/dip.ts +16 -18
- package/principles/rules/solid/isp.ts +17 -19
- package/principles/rules/solid/lsp.ts +4 -6
- package/principles/rules/solid/ocp.ts +17 -19
- package/principles/rules/solid/srp.ts +20 -22
- package/scripts/__tests__/sync-package-content.test.js +0 -1
- package/scripts/sync-package-content.js +0 -1
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/claude-code/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/claude-code/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/claude-code/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/claude-code/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/dsh/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/dsh/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/dsh/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/dsh/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/opencode/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/opencode/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/opencode/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/opencode/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/plugins/qoder/skills/clipboard-vision/SKILL.md +0 -48
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/plugins/qoder/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/plugins/qoder/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/plugins/qoder/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
- package/skills/clipboard-vision/SKILL.md +0 -48
- package/skills/clipboard-vision/__tests__/clipboard-vision-distribution.test.cjs +0 -64
- package/skills/clipboard-vision/__tests__/clipboard-vision-security.test.cjs +0 -185
- package/skills/clipboard-vision/__tests__/clipboard-vision.Tests.ps1 +0 -138
- package/skills/clipboard-vision/__tests__/clipboard-vision.test.cjs +0 -273
- package/skills/clipboard-vision/describe-clipboard.ps1 +0 -115
- package/skills/clipboard-vision/describe-clipboard.sh +0 -161
- package/skills/clipboard-vision/scripts/clipboard-vision.ps1 +0 -86
- package/skills/clipboard-vision/scripts/clipboard-vision.sh +0 -137
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* check-alignment-runner.cjs — Thin argv shim for test-alignment.ts
|
|
4
|
+
*
|
|
5
|
+
* Bridges the `xp-gate check-alignment` CLI to the TypeScript engine without
|
|
6
|
+
* shell-command interpolation of user-supplied paths (#392-#6).
|
|
7
|
+
*
|
|
8
|
+
* It is designed to be launched EITHER way:
|
|
9
|
+
* 1. In-repo (fast path): `node` loads this file after xp-gate.js already
|
|
10
|
+
* called `require('tsx/cjs')`, so requiring `./test-alignment.ts` works.
|
|
11
|
+
* 2. Published global install (zero-install path): `npx -y tsx` runs this
|
|
12
|
+
* file, and npx self-provisions tsx on demand (never a package dep), so
|
|
13
|
+
* `./test-alignment.ts` loads even though the package ships zero deps.
|
|
14
|
+
*
|
|
15
|
+
* All user-supplied paths are passed as discrete argv elements, never embedded
|
|
16
|
+
* into a shell command string, preserving the #392-#6 injection fix.
|
|
17
|
+
*/
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const { execFileSync } = require('child_process');
|
|
21
|
+
|
|
22
|
+
// --- argv parsing ----------------------------------------------------------
|
|
23
|
+
function argValue(argv, flag) {
|
|
24
|
+
const idx = argv.indexOf(flag);
|
|
25
|
+
return idx >= 0 && idx + 1 < argv.length ? argv[idx + 1] : undefined;
|
|
26
|
+
}
|
|
27
|
+
function argFlag(argv, flag) {
|
|
28
|
+
return argv.includes(flag);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const specPath = argValue(process.argv, '--spec');
|
|
32
|
+
const testDirsArg = argValue(process.argv, '--tests');
|
|
33
|
+
const outputPath = argValue(process.argv, '--output');
|
|
34
|
+
const jsonMode = argFlag(process.argv, '--json');
|
|
35
|
+
|
|
36
|
+
if (!specPath) {
|
|
37
|
+
console.error('check-alignment: --spec <path> is required');
|
|
38
|
+
process.exit(1);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const testDirs = testDirsArg ? [testDirsArg] : undefined;
|
|
42
|
+
|
|
43
|
+
let headCommit = 'unknown';
|
|
44
|
+
try {
|
|
45
|
+
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim() || 'unknown';
|
|
46
|
+
} catch (err) {
|
|
47
|
+
headCommit = 'unknown';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { runFullAlignment } = require('./test-alignment.ts');
|
|
51
|
+
|
|
52
|
+
let report;
|
|
53
|
+
try {
|
|
54
|
+
report = runFullAlignment({
|
|
55
|
+
specPath,
|
|
56
|
+
testDirs,
|
|
57
|
+
headCommit,
|
|
58
|
+
outputPath: outputPath || undefined,
|
|
59
|
+
});
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('check-alignment failed:', err instanceof Error ? err.message : String(err));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (jsonMode) {
|
|
66
|
+
console.log(JSON.stringify(report, null, 2));
|
|
67
|
+
} else {
|
|
68
|
+
console.log('Status:', report.alignment_status);
|
|
69
|
+
console.log('Score:', report.score);
|
|
70
|
+
console.log('Misaligned:', report.misaligned_tests.length);
|
|
71
|
+
}
|
|
72
|
+
// Set exitCode instead of process.exit() so buffered stdout (notably the
|
|
73
|
+
// --json document) flushes fully before the process terminates (#392 review).
|
|
74
|
+
process.exitCode = report.alignment_status === 'PASS' ? 0 : 1;
|
package/lib/detect-deps.js
CHANGED
|
@@ -270,12 +270,88 @@ function getSkillsDirs(platform) {
|
|
|
270
270
|
return profile.skillsDirs;
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Resolve a bash that can actually run the hooks.
|
|
275
|
+
*
|
|
276
|
+
* On Windows, `bash` on PATH is ambiguous: WSL installs C:\Windows\System32\
|
|
277
|
+
* bash.exe (a Linux interpreter whose filesystem view does not contain the
|
|
278
|
+
* Windows temp dirs the tooling passes to it), and its argv handling silently
|
|
279
|
+
* drops the positional arguments a `-c` script expects ($1 empty → scripts
|
|
280
|
+
* source nothing and "succeed" vacuously). Git for Windows ships the MSYS bash
|
|
281
|
+
* the hooks are written for. Probe candidates with an argv round-trip plus an
|
|
282
|
+
* uname check — only an MSYS/MINGW/CYGWIN bash counts on win32; a plain
|
|
283
|
+
* "bash --version" success proves nothing.
|
|
284
|
+
* @returns {string|null}
|
|
285
|
+
*/
|
|
286
|
+
function probeWindowsBash() {
|
|
287
|
+
const { spawnSync } = require('child_process');
|
|
288
|
+
// `where` runs through the same module so tests can fake discovery + probe.
|
|
289
|
+
const { execSync: localExecSync } = require('child_process');
|
|
290
|
+
return resolveWindowsBashCandidates(spawnSync, localExecSync);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Candidate list + probe loop split out for behavior tests: a fake spawner can
|
|
294
|
+
// simulate the WSL stub (echoes Linux / drops $1) vs an MSYS bash without any
|
|
295
|
+
// real interpreter on PATH (Delphi walkthrough R1, MN-06).
|
|
296
|
+
function resolveWindowsBashCandidates(spawnProbe, whereExecSync) {
|
|
297
|
+
const _execSync = whereExecSync || execSync;
|
|
298
|
+
const candidates = [];
|
|
299
|
+
try {
|
|
300
|
+
const found = _execSync('where bash', { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
|
|
301
|
+
for (const line of found.split(/\r?\n/)) if (line.trim()) candidates.push(line.trim());
|
|
302
|
+
} catch { /* where unavailable */ }
|
|
303
|
+
candidates.push(
|
|
304
|
+
'C:\\Program Files\\Git\\bin\\bash.exe',
|
|
305
|
+
'C:\\Program Files\\Git\\usr\\bin\\bash.exe',
|
|
306
|
+
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
307
|
+
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
|
308
|
+
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
309
|
+
'C:\\cygwin64\\bin\\bash.exe'
|
|
310
|
+
);
|
|
311
|
+
for (const candidate of candidates) {
|
|
312
|
+
try {
|
|
313
|
+
const r = spawnProbe(candidate, ['-c', 'echo "[$1]"; uname -s', 'sh', 'XPGATE_ARGV_PROBE'], {
|
|
314
|
+
encoding: 'utf8',
|
|
315
|
+
timeout: 5000,
|
|
316
|
+
windowsHide: true,
|
|
317
|
+
});
|
|
318
|
+
const out = String((r && r.stdout) || '');
|
|
319
|
+
if (out.includes('[XPGATE_ARGV_PROBE]') && /(MINGW|MSYS|CYGWIN)/i.test(out)) {
|
|
320
|
+
return candidate;
|
|
321
|
+
}
|
|
322
|
+
} catch { /* next candidate */ }
|
|
323
|
+
}
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
273
327
|
/**
|
|
274
328
|
* Check if bash is available on the system.
|
|
275
329
|
* XP-Gate hooks are bash scripts — Windows users need Git Bash installed.
|
|
276
330
|
* @returns {{ok: boolean, path?: string, message?: string}}
|
|
277
331
|
*/
|
|
278
332
|
function checkBash() {
|
|
333
|
+
// On Windows resolve explicitly: PATH may lead with the WSL System32 stub,
|
|
334
|
+
// which cannot execute these hooks (wrong filesystem view, dropped $1).
|
|
335
|
+
if (process.platform === 'win32') {
|
|
336
|
+
const winBash = probeWindowsBash();
|
|
337
|
+
if (winBash) {
|
|
338
|
+
let version = 'unknown';
|
|
339
|
+
try {
|
|
340
|
+
// spawnSync with discrete argv — never interpolate a discovered path
|
|
341
|
+
// into a shell string (a PATH dir name may carry cmd metacharacters).
|
|
342
|
+
const result = spawnSync(winBash, ['--version'], { encoding: 'utf8', timeout: 5000 });
|
|
343
|
+
const versionMatch = String(result.stdout || '').match(/version\s+([^\s]+)/);
|
|
344
|
+
if (versionMatch) version = versionMatch[1];
|
|
345
|
+
} catch { /* keep unknown */ }
|
|
346
|
+
return { ok: true, path: winBash, version };
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
ok: false,
|
|
350
|
+
message: 'bash not found. Windows users must install [Git for Windows](https://git-scm.com/download/win).\n' +
|
|
351
|
+
' During installation, ensure "Git Bash Here" is checked — this adds bash.exe to PATH.\n' +
|
|
352
|
+
' After installation, restart your terminal and run `npm install` again.'
|
|
353
|
+
};
|
|
354
|
+
}
|
|
279
355
|
try {
|
|
280
356
|
// Try 'bash' first (works on Linux/macOS and when Git Bash is in PATH)
|
|
281
357
|
const result = execSync('bash --version', {
|
|
@@ -289,17 +365,11 @@ function checkBash() {
|
|
|
289
365
|
version: versionMatch ? versionMatch[1] : 'unknown'
|
|
290
366
|
};
|
|
291
367
|
} catch (e) {
|
|
292
|
-
//
|
|
368
|
+
// POSIX fallback: common absolute locations
|
|
293
369
|
const bashPaths = [
|
|
294
|
-
|
|
295
|
-
'
|
|
296
|
-
'
|
|
297
|
-
'C:\\Program Files (x86)\\Git\\bin\\bash.exe',
|
|
298
|
-
'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe',
|
|
299
|
-
// MSYS2
|
|
300
|
-
'C:\\msys64\\usr\\bin\\bash.exe',
|
|
301
|
-
// Cygwin
|
|
302
|
-
'C:\\cygwin64\\bin\\bash.exe',
|
|
370
|
+
'/bin/bash',
|
|
371
|
+
'/usr/bin/bash',
|
|
372
|
+
'/usr/local/bin/bash',
|
|
303
373
|
];
|
|
304
374
|
|
|
305
375
|
for (const bashPath of bashPaths) {
|
|
@@ -319,15 +389,7 @@ function checkBash() {
|
|
|
319
389
|
}
|
|
320
390
|
}
|
|
321
391
|
|
|
322
|
-
// Windows
|
|
323
|
-
if (process.platform === 'win32') {
|
|
324
|
-
return {
|
|
325
|
-
ok: false,
|
|
326
|
-
message: 'bash not found. Windows users must install [Git for Windows](https://git-scm.com/download/win).\n' +
|
|
327
|
-
' During installation, ensure "Git Bash Here" is checked — this adds bash.exe to PATH.\n' +
|
|
328
|
-
' After installation, restart your terminal and run `npm install` again.'
|
|
329
|
-
};
|
|
330
|
-
}
|
|
392
|
+
// Windows is handled above; this fallback only runs on POSIX now.
|
|
331
393
|
|
|
332
394
|
return {
|
|
333
395
|
ok: false,
|
|
@@ -505,6 +567,7 @@ function compareVersions(a, b) {
|
|
|
505
567
|
module.exports = {
|
|
506
568
|
checkDeps,
|
|
507
569
|
checkBash,
|
|
570
|
+
resolveWindowsBashCandidates,
|
|
508
571
|
autoInstallDeps,
|
|
509
572
|
detectPlatform,
|
|
510
573
|
getSkillsDirs,
|
package/lib/gate-runner.js
CHANGED
|
@@ -18,15 +18,22 @@ const path = require('node:path');
|
|
|
18
18
|
* behaves exactly as it does inside the commit hook.
|
|
19
19
|
*/
|
|
20
20
|
function runGateAdapter(scriptPath, cwd = process.cwd()) {
|
|
21
|
+
let bashCommand = 'bash';
|
|
21
22
|
if (process.platform === 'win32') {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
// PATH may lead with the WSL System32 stub, which cannot run these hooks
|
|
24
|
+
// (Linux filesystem view, and `-c` scripts receive an empty $1 through it).
|
|
25
|
+
// checkBash probes for an MSYS/MINGW bash that round-trips argv correctly.
|
|
26
|
+
const { checkBash } = require('./detect-deps.js');
|
|
27
|
+
const bash = checkBash();
|
|
28
|
+
if (!bash.ok) {
|
|
25
29
|
console.log('⚠️ This gate requires bash to run shell scripts.');
|
|
26
|
-
|
|
30
|
+
// No WSL suggestion: these hooks need Git Bash specifically — the WSL
|
|
31
|
+
// bash is rejected by the probe above (Linux filesystem view, dropped $1).
|
|
32
|
+
console.log(' On Windows, install Git for Windows (https://git-scm.com/download/win).');
|
|
27
33
|
console.log(' Alternatively, install the required tool directly and run the gate again.');
|
|
28
34
|
return;
|
|
29
35
|
}
|
|
36
|
+
bashCommand = bash.path;
|
|
30
37
|
}
|
|
31
38
|
const scriptDir = path.dirname(scriptPath);
|
|
32
39
|
const commonScript = [
|
|
@@ -62,7 +69,7 @@ function runGateAdapter(scriptPath, cwd = process.cwd()) {
|
|
|
62
69
|
// execFileSync avoids an extra /bin/sh layer, so bash receives the script as a
|
|
63
70
|
// single argv arg (no shell-metacharacter injection through the path).
|
|
64
71
|
execFileSync(
|
|
65
|
-
|
|
72
|
+
bashCommand,
|
|
66
73
|
['--noprofile', '--norc', '-c', wrap, 'xp-gate', commonScript, nowMsScript, scriptPath, process.execPath],
|
|
67
74
|
{ stdio: 'inherit', cwd }
|
|
68
75
|
);
|
package/lib/sprint-status.js
CHANGED
|
@@ -345,8 +345,11 @@ async function handleSprintStatus(args = []) {
|
|
|
345
345
|
|
|
346
346
|
if (dirIdx >= 0 && dirIdx + 1 < args.length) {
|
|
347
347
|
searchDir = path.resolve(args[dirIdx + 1]);
|
|
348
|
-
// Path traversal protection: must be under cwd
|
|
349
|
-
|
|
348
|
+
// Path traversal protection: must be under cwd. Use path.relative instead
|
|
349
|
+
// of a prefix match so a sibling like `<cwd>2` cannot bypass the check
|
|
350
|
+
// (#392-#5).
|
|
351
|
+
const rel = path.relative(process.cwd(), searchDir);
|
|
352
|
+
if (rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
350
353
|
console.error('Error: --dir path must be under current working directory');
|
|
351
354
|
return 1;
|
|
352
355
|
}
|
package/lib/test-alignment.ts
CHANGED
|
@@ -230,28 +230,124 @@ function countAssertions(content: string): number {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/**
|
|
233
|
-
*
|
|
234
|
-
*
|
|
233
|
+
* Given a content string and the index where the test callback arrow/function
|
|
234
|
+
* begins (the position of `() =>` or `function() {`), scan forward with brace
|
|
235
|
+
* matching to return the full callback body (including the outer braces).
|
|
236
|
+
* Skips single/double quoted strings, backtick template literals, and line/block
|
|
237
|
+
* comments so braces inside them do not throw off matching.
|
|
238
|
+
*/
|
|
239
|
+
function extractCallbackBody(content: string, callbackStart: number): string {
|
|
240
|
+
// Find the opening brace of the callback body after `() => {` / `function() {`.
|
|
241
|
+
let i = callbackStart;
|
|
242
|
+
let inLineComment = false;
|
|
243
|
+
let inBlockComment = false;
|
|
244
|
+
let inSingle = false;
|
|
245
|
+
let inDouble = false;
|
|
246
|
+
let inTemplate = false;
|
|
247
|
+
let depth = 0;
|
|
248
|
+
let started = false;
|
|
249
|
+
let openIdx = -1;
|
|
250
|
+
|
|
251
|
+
while (i < content.length) {
|
|
252
|
+
const c = content[i];
|
|
253
|
+
const n = content[i + 1];
|
|
254
|
+
|
|
255
|
+
if (inLineComment) {
|
|
256
|
+
if (c === '\n') inLineComment = false;
|
|
257
|
+
i++;
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (inBlockComment) {
|
|
261
|
+
if (c === '*' && n === '/') { inBlockComment = false; i += 2; continue; }
|
|
262
|
+
i++;
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (inSingle) {
|
|
266
|
+
if (c === '\\') { i += 2; continue; }
|
|
267
|
+
if (c === '\'') inSingle = false;
|
|
268
|
+
i++;
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
if (inDouble) {
|
|
272
|
+
if (c === '\\') { i += 2; continue; }
|
|
273
|
+
if (c === '"') inDouble = false;
|
|
274
|
+
i++;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (inTemplate) {
|
|
278
|
+
if (c === '\\') { i += 2; continue; }
|
|
279
|
+
if (c === '`') inTemplate = false;
|
|
280
|
+
// Template `${}` interpolation can contain braces; treat interpolation
|
|
281
|
+
// content conservatively by not toggling depth on braces inside it.
|
|
282
|
+
i++;
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (c === '/' && n === '/') { inLineComment = true; i += 2; continue; }
|
|
287
|
+
if (c === '/' && n === '*') { inBlockComment = true; i += 2; continue; }
|
|
288
|
+
if (c === '\'') { inSingle = true; i++; continue; }
|
|
289
|
+
if (c === '"') { inDouble = true; i++; continue; }
|
|
290
|
+
if (c === '`') { inTemplate = true; i++; continue; }
|
|
291
|
+
|
|
292
|
+
if (c === '{') {
|
|
293
|
+
if (!started) { started = true; openIdx = i; depth = 1; }
|
|
294
|
+
else depth++;
|
|
295
|
+
i++;
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
if (c === '}') {
|
|
299
|
+
depth--;
|
|
300
|
+
if (depth === 0) {
|
|
301
|
+
return content.slice(openIdx, i + 1);
|
|
302
|
+
}
|
|
303
|
+
i++;
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
i++;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// No balanced body found — the declaration had no callback body (e.g. a
|
|
311
|
+
// `test.todo('name');` line). Return an empty body so it counts 0 assertions.
|
|
312
|
+
return '';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Extract JSDoc block preceding a test/it/describe declaration, along with the
|
|
317
|
+
* *function body* of the callback (so assertion counting is per-test, not
|
|
318
|
+
* per-file). Handles .skip(), .todo(), .only() variants and backtick template
|
|
319
|
+
* literals.
|
|
320
|
+
*
|
|
321
|
+
* Known limitation (pre-existing, not introduced by #392): `test.each([...])`
|
|
322
|
+
* parameterised forms are NOT matched, because the argument list is a bracketed
|
|
323
|
+
* array literal and the name string follows it without a comma. This means such
|
|
324
|
+
* tests are omitted from assertion counting. Coverage determination itself is
|
|
325
|
+
* driven by `@covers` tags, so this only affects the avg-assertions data-validity
|
|
326
|
+
* heuristic, never AC coverage. Tracked separately from #392.
|
|
235
327
|
*/
|
|
236
328
|
function extractJSDocBeforeTest(content: string): Array<{ jsdoc: string; testBody: string; testName: string }> {
|
|
237
329
|
const results: Array<{ jsdoc: string; testBody: string; testName: string }> = [];
|
|
238
330
|
|
|
239
|
-
// Match: /** JSDOC */ (test|it|describe)[.modifier]( [args], )'name',
|
|
240
|
-
// `.each` variants have an extra `()` before the test name string
|
|
241
|
-
|
|
331
|
+
// Match: /** JSDOC */ (test|it|describe)[.modifier]( [args], )'name', ( [params] ) => {
|
|
332
|
+
// `.each` variants have an extra `()` before the test name string.
|
|
333
|
+
// Group 4 (bodyStart) points at the callback open paren so we can brace-match
|
|
334
|
+
// the real function body.
|
|
335
|
+
const testPattern = /\/\*\*([\s\S]*?)\*\/(?:[\s\n]*)(?:test|it|describe)(?:\.(?:each|skip|todo|only))?\s*\([^)]*\)\s*,\s*\(\s*(?:['"]([^'"]+)['"]|`([^`]*)`)\s*\)?\s*,?\s*(?:\([^)]*\)\s*=>)?\s*/g;
|
|
242
336
|
|
|
243
337
|
let match: RegExpExecArray | null;
|
|
244
338
|
while ((match = testPattern.exec(content)) !== null) {
|
|
245
339
|
const jsdoc = match[1]!;
|
|
246
340
|
const testName = match[2] || match[3] || '';
|
|
247
341
|
if (testName) {
|
|
248
|
-
const
|
|
342
|
+
const body = extractCallbackBody(content, match.index + match[0].length);
|
|
343
|
+
// JSDoc + declaration + function body gives a self-contained test body.
|
|
344
|
+
const testBody = content.slice(match.index, match.index + match[0].length) + body;
|
|
249
345
|
results.push({ jsdoc, testBody, testName });
|
|
250
346
|
}
|
|
251
347
|
}
|
|
252
348
|
|
|
253
349
|
// Second pass: match standard test/it/describe without .each
|
|
254
|
-
const simplePattern = /\/\*\*([\s\S]*?)\*\/(?:[\s\n]*)(?:test|it|describe)(?:\.(?:skip|todo|only))?\s*\(\s*(?:['"]([^'"]+)['"]|`([^`]*)`)\s*[
|
|
350
|
+
const simplePattern = /\/\*\*([\s\S]*?)\*\/(?:[\s\n]*)(?:test|it|describe)(?:\.(?:skip|todo|only))?\s*\(\s*(?:['"]([^'"]+)['"]|`([^`]*)`)\s*\)?\s*,?\s*(?:\([^)]*\)\s*=>)?\s*/g;
|
|
255
351
|
|
|
256
352
|
while ((match = simplePattern.exec(content)) !== null) {
|
|
257
353
|
const jsdoc = match[1]!;
|
|
@@ -260,7 +356,8 @@ function extractJSDocBeforeTest(content: string): Array<{ jsdoc: string; testBod
|
|
|
260
356
|
// Don't duplicate if already found in first pass
|
|
261
357
|
const alreadyFound = results.some(r => r.jsdoc === jsdoc);
|
|
262
358
|
if (!alreadyFound) {
|
|
263
|
-
const
|
|
359
|
+
const body = extractCallbackBody(content, match.index + match[0].length);
|
|
360
|
+
const testBody = content.slice(match.index, match.index + match[0].length) + body;
|
|
264
361
|
results.push({ jsdoc, testBody, testName });
|
|
265
362
|
}
|
|
266
363
|
}
|
|
@@ -269,6 +366,27 @@ function extractJSDocBeforeTest(content: string): Array<{ jsdoc: string; testBod
|
|
|
269
366
|
return results;
|
|
270
367
|
}
|
|
271
368
|
|
|
369
|
+
/**
|
|
370
|
+
* Extract a Python test function body: from the `def test_xxx():` line up to
|
|
371
|
+
* (but not including) the next top-level `def ` line or end of file. This keeps
|
|
372
|
+
* assertion counting scoped to the individual test.
|
|
373
|
+
*/
|
|
374
|
+
function extractPythonTestBody(content: string, defIndex: number): string {
|
|
375
|
+
const nextDef = content.indexOf('\ndef ', defIndex);
|
|
376
|
+
const endIdx = nextDef === -1 ? content.length : nextDef;
|
|
377
|
+
return content.slice(defIndex, endIdx);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Extract a Go test function body via brace matching starting at the opening
|
|
382
|
+
* brace that follows `func Test_xxx()`. Returns the full `{ ... }` block.
|
|
383
|
+
*/
|
|
384
|
+
function extractGoTestBody(content: string, funcIndex: number): string {
|
|
385
|
+
const braceIdx = content.indexOf('{', funcIndex);
|
|
386
|
+
if (braceIdx === -1) return '';
|
|
387
|
+
return extractCallbackBody(content, braceIdx);
|
|
388
|
+
}
|
|
389
|
+
|
|
272
390
|
function pushTestWithCovers(
|
|
273
391
|
tests: TestCase[],
|
|
274
392
|
testPath: string,
|
|
@@ -276,6 +394,7 @@ function pushTestWithCovers(
|
|
|
276
394
|
reqId: string,
|
|
277
395
|
intent: string | undefined,
|
|
278
396
|
fileContent: string,
|
|
397
|
+
bodyContent: string,
|
|
279
398
|
totalAssertionsRef: { value: number },
|
|
280
399
|
) {
|
|
281
400
|
const coversRegex = /@covers\s+(.+)/g;
|
|
@@ -285,7 +404,9 @@ function pushTestWithCovers(
|
|
|
285
404
|
covers.push(...coversMatch[1]!.split(',').map(s => s.trim()).filter(c => /^AC-[A-Z0-9]+-\d+-\d+$/.test(c)));
|
|
286
405
|
}
|
|
287
406
|
|
|
288
|
-
|
|
407
|
+
// Count assertions against the *per-test body*, not the whole file, so
|
|
408
|
+
// totalAssertions is not inflated by fileCount x perFileCount (#392-#1).
|
|
409
|
+
const assertions = countAssertions(bodyContent);
|
|
289
410
|
totalAssertionsRef.value += assertions;
|
|
290
411
|
|
|
291
412
|
tests.push({
|
|
@@ -319,7 +440,7 @@ export function parseTestFiles(testPaths: string[]): TestMap {
|
|
|
319
440
|
// Filter: only valid AC IDs (AC-XXX-NNN-NN pattern)
|
|
320
441
|
const covers = coversRaw.filter(c => /^AC-[A-Z0-9]+-\d+-\d+$/.test(c));
|
|
321
442
|
|
|
322
|
-
const assertions = countAssertions(
|
|
443
|
+
const assertions = countAssertions(m.testBody);
|
|
323
444
|
totalAssertionsObj.value += assertions;
|
|
324
445
|
|
|
325
446
|
tests.push({
|
|
@@ -336,7 +457,8 @@ export function parseTestFiles(testPaths: string[]): TestMap {
|
|
|
336
457
|
const pyTestPattern = /(# @test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+)[\s\S]*?)(?:# @intent\s+(.+?)\n)?def\s+(test_\w+)/g;
|
|
337
458
|
let m: RegExpExecArray | null;
|
|
338
459
|
while ((m = pyTestPattern.exec(content)) !== null) {
|
|
339
|
-
|
|
460
|
+
const body = extractPythonTestBody(content, m.index + m[0].lastIndexOf('def '));
|
|
461
|
+
pushTestWithCovers(tests, testPath, m[4]!, m[2]!, m[3]?.trim(), content, body, totalAssertionsObj);
|
|
340
462
|
}
|
|
341
463
|
} else if (ext === '.go') {
|
|
342
464
|
const goTestPattern = /(\/\/ @test\s+(REQ-[A-Z0-9]+-[A-Z0-9]+))\s*[\s\S]*?func\s+(Test_\w+)/g;
|
|
@@ -344,7 +466,9 @@ export function parseTestFiles(testPaths: string[]): TestMap {
|
|
|
344
466
|
while ((m = goTestPattern.exec(content)) !== null) {
|
|
345
467
|
const block = content.slice(Math.max(0, m.index - 500), m.index);
|
|
346
468
|
const intentMatch = block.match(/\/\/ @intent\s+(.+)/);
|
|
347
|
-
|
|
469
|
+
const funcStart = m.index + m[0].lastIndexOf('func ');
|
|
470
|
+
const body = extractGoTestBody(content, funcStart);
|
|
471
|
+
pushTestWithCovers(tests, testPath, m[3]!, m[2]!, intentMatch?.[1]?.trim(), content, body, totalAssertionsObj);
|
|
348
472
|
}
|
|
349
473
|
}
|
|
350
474
|
}
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.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/mutation/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MUTATION KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 12 pre-commit gates (Gate 0-11), 8 pre-push gates, Sprint Flow (6 phases), and Delphi multi-expert review (>=90% consensus).",
|
|
6
6
|
"author": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/plugins/dsh/README.md
CHANGED
|
@@ -185,6 +185,6 @@ npm run build:dsh-plugin # = node scripts/build-plugin.mjs --platform dsh
|
|
|
185
185
|
## 注意
|
|
186
186
|
|
|
187
187
|
- `lib/` 与 `skills/` 是**构建产物**(已 gitignore),不提交。
|
|
188
|
-
- `scripts/prepack.cjs`
|
|
188
|
+
- `scripts/prepack.cjs` 的 skill 列表为**动态发现**(读取仓库 `skills/`),不硬编码。
|
|
189
189
|
- `plugins/dsh/package.json` 的 peerDeps 仍 pin `0.1.1-rc.2`,但实测运行在 `0.2.0-rc.2` 上
|
|
190
190
|
`defineTool` / `TOOL_ABORTED` / `HarnessError` 签名未变,无需改写。
|
package/plugins/dsh/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.27.0",
|
|
4
4
|
"description": "XP-Gate deterministic quality gates + AI workflow skills as a DeepSeek Harness (DSH) native plugin: gate-check / gate-principles / gate-arch tools + 12 SKILL.md skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* plugins/opencode/scripts/prepack.cjs).
|
|
11
11
|
*
|
|
12
12
|
* The skill list is DISCOVERED, not hardcoded: a hardcoded list silently drifts
|
|
13
|
-
* from the repo (
|
|
14
|
-
*
|
|
13
|
+
* from the repo (a hardcoded set previously omitted one shipped skill, breaking
|
|
14
|
+
* parity) — see #448.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
const fs = require('fs');
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, exactly 3 experts with distinct executable model IDs). Supports design, requirements, and code-walkthrough modes.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/SPRINT-FLOW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
**6-phase** development pipeline (v2.0 compact redesign, Issue #290): PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE. Phase 3/6 BUILD default build mode is **ralph-loop** (REQ-level iteration, 40-67% token savings vs parallel). HARD-GATE between DESIGN (2/6) and BUILD (3/6): design must pass Delphi review (≥90% consensus) before any coding.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-16
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 429f73c5
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.
|
|
6
|
+
**Version:** 0.27.0.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|