@boyingliu01/xp-gate 0.35.17 → 0.35.18
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/gate-12-file-hygiene.sh +26 -4
- package/gate-13-compiler-strictness.sh +12 -0
- package/gates/gate-3.ts +77 -21
- package/gates/gate-8.ts +39 -26
- package/gates/gate-9.ts +4 -4
- package/hooks/gate-12-file-hygiene.sh +26 -4
- package/hooks/gate-13-compiler-strictness.sh +12 -0
- package/hooks/pre-commit +75 -11
- package/hooks/pre-push +11 -2
- package/lib/__tests__/doctor.test.js +9 -0
- package/lib/__tests__/gate-numbering.test.js +52 -0
- package/lib/__tests__/gate-runner.test.js +59 -14
- package/lib/__tests__/shared-utils.test.js +47 -0
- package/lib/gate-runner.js +98 -18
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/shared-utils.js +27 -1
- 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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/package.json +1 -1
- package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
- package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
- package/plugins/dsh/src/delphi-findings.test.ts +32 -6
- package/plugins/dsh/src/exit-code.test.ts +6 -0
- package/plugins/opencode/package.json +1 -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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- 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/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
- package/principles/AGENTS.md +2 -2
- package/scripts/__tests__/sync-package-content.test.js +114 -46
- package/skills/delphi-review/AGENTS.md +2 -2
- package/skills/sprint-flow/AGENTS.md +2 -2
- package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
- package/skills/sprint-flow/references/definition-of-done.md +47 -0
- package/skills/sprint-flow/references/phase-4-verify.md +16 -0
- package/skills/sprint-flow/references/phase-5-ship.md +14 -0
- package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-686
|
|
3
|
+
* @intent 验证 CLI 的门禁编号与钩子一致——钩子把 9 号门叫 Build integrity、
|
|
4
|
+
* 10 号门叫 SAST,而 CLI 注册表把两者对调了;用户按报错提示单跑对应门
|
|
5
|
+
* 会跑到另一道门上去(#677)
|
|
6
|
+
* @covers AC-686-01, AC-686-02, AC-686-03
|
|
7
|
+
*
|
|
8
|
+
* 为什么必须直接断言注册表:这个错误的表现是「跑起来了、报告绿了」。
|
|
9
|
+
* `xp-gate check --gates 9` 在编号错位时不会报错,它跑的是 semgrep 并如实报告
|
|
10
|
+
* semgrep 的结果——只有拿它和钩子里的门名对照才能看出跑错了门。
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { GATE_REGISTRY, resolveAlias } = require('../gate-runner');
|
|
17
|
+
|
|
18
|
+
const REPO_ROOT = path.join(__dirname, '../../../..');
|
|
19
|
+
|
|
20
|
+
describe('gate numbering matches the hook (#677)', () => {
|
|
21
|
+
it('AC-686-01: gate 9 is Build Integrity and gate 10 is SAST Security', () => {
|
|
22
|
+
expect(GATE_REGISTRY['9'].name).toBe('Build Integrity');
|
|
23
|
+
expect(GATE_REGISTRY['10'].name).toBe('SAST Security');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('AC-686-02: the hook agrees — its own output names 9 as Build integrity', () => {
|
|
27
|
+
// Reads the hook rather than restating the constant: if the hook ever
|
|
28
|
+
// renumbers again, this fails and points at which side moved.
|
|
29
|
+
const hook = fs.readFileSync(path.join(REPO_ROOT, 'githooks/pre-commit'), 'utf8');
|
|
30
|
+
expect(hook).toMatch(/→ Gate 9: Build integrity/);
|
|
31
|
+
expect(hook).toMatch(/GATE 10: Semgrep SAST/);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('AC-686-03: aliases resolve to the gate the user meant', () => {
|
|
35
|
+
expect(resolveAlias('sast')).toBe('10');
|
|
36
|
+
expect(resolveAlias('semgrep')).toBe('10');
|
|
37
|
+
expect(resolveAlias('build')).toBe('9');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('AC-686-03: gates 12 and 13 are registered instead of "Unknown gate(s)"', () => {
|
|
41
|
+
// Both block commits in the hook. Answering "Unknown gate" for a gate that just
|
|
42
|
+
// blocked the user is the same category of wrong as running the wrong one.
|
|
43
|
+
expect(GATE_REGISTRY['12'].name).toBe('File Hygiene');
|
|
44
|
+
expect(GATE_REGISTRY['13'].name).toBe('Compiler Strictness');
|
|
45
|
+
// They are registered as hook-only, with a stated reason -- not as runnable
|
|
46
|
+
// stubs that would answer PASS without doing the work.
|
|
47
|
+
expect(GATE_REGISTRY['12'].preCommitOnly).toBe(true);
|
|
48
|
+
expect(GATE_REGISTRY['13'].preCommitOnly).toBe(true);
|
|
49
|
+
expect(GATE_REGISTRY['12'].reason).toBeTruthy();
|
|
50
|
+
expect(GATE_REGISTRY['13'].reason).toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -9,6 +9,7 @@ const fs = require('node:fs');
|
|
|
9
9
|
const path = require('node:path');
|
|
10
10
|
const os = require('node:os');
|
|
11
11
|
const { execFileSync, spawnSync } = require('node:child_process');
|
|
12
|
+
const { findConsumerGateVariant } = require('../gate-runner');
|
|
12
13
|
|
|
13
14
|
let tmpProject;
|
|
14
15
|
let tmpAdapters;
|
|
@@ -25,15 +26,26 @@ afterEach(() => {
|
|
|
25
26
|
}
|
|
26
27
|
});
|
|
27
28
|
|
|
29
|
+
// Fixture writers run in every `beforeAll`; swallowing their failure would turn a
|
|
30
|
+
// broken fixture into "the gate under test misbehaved", which is the opposite of
|
|
31
|
+
// what the assertion is checking. Report which file could not be written.
|
|
32
|
+
function writeFixture(filePath, content) {
|
|
33
|
+
try {
|
|
34
|
+
fs.writeFileSync(filePath, content);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw new Error(`Cannot write test fixture ${filePath}: ${err.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
28
40
|
function writeAdapterCommon() {
|
|
29
|
-
|
|
41
|
+
writeFixture(
|
|
30
42
|
path.join(tmpAdapters, 'adapter-common.sh'),
|
|
31
43
|
'detect_project_lang() { echo "typescript"; }'
|
|
32
44
|
);
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
function writeGateFragment(fragmentLines) {
|
|
36
|
-
|
|
48
|
+
writeFixture(
|
|
37
49
|
path.join(tmpAdapters, 'gate-9.sh'),
|
|
38
50
|
['# gate-9 fragment', ...fragmentLines, 'exit 0'].join('\n')
|
|
39
51
|
);
|
|
@@ -55,14 +67,21 @@ function hostilePathSegment() {
|
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
function initializeGitProject() {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
70
|
+
try {
|
|
71
|
+
execFileSync('git', ['init', '-q'], { cwd: tmpProject });
|
|
72
|
+
execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpProject });
|
|
73
|
+
execFileSync('git', ['config', 'user.name', 'XP-Gate Test'], { cwd: tmpProject });
|
|
74
|
+
writeFixture(path.join(tmpProject, 'tracked.ts'), 'const value = 1;\n');
|
|
75
|
+
execFileSync('git', ['add', 'tracked.ts'], { cwd: tmpProject });
|
|
76
|
+
execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'initial'], {
|
|
77
|
+
cwd: tmpProject,
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
// Without this the failure surfaces as a downstream assertion on the gate's
|
|
81
|
+
// output. Name the setup step instead: a repo fixture that never initialised
|
|
82
|
+
// is not a gate defect.
|
|
83
|
+
throw new Error(`Cannot initialise the fixture git repo at ${tmpProject}: ${err.message}`);
|
|
84
|
+
}
|
|
66
85
|
}
|
|
67
86
|
|
|
68
87
|
// Runs runGateAdapter in a child node process (worker threads forbid process
|
|
@@ -188,7 +207,7 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
188
207
|
].join('\n')
|
|
189
208
|
);
|
|
190
209
|
|
|
191
|
-
const result = spawnSync(process.execPath, [cliPath, 'check', '
|
|
210
|
+
const result = spawnSync(process.execPath, [cliPath, 'check', '10'], {
|
|
192
211
|
cwd: tmpProject,
|
|
193
212
|
encoding: 'utf8',
|
|
194
213
|
timeout: 30000,
|
|
@@ -225,7 +244,7 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
225
244
|
|
|
226
245
|
const result = spawnSync(
|
|
227
246
|
process.execPath,
|
|
228
|
-
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '
|
|
247
|
+
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '10', targetProject],
|
|
229
248
|
{
|
|
230
249
|
cwd: callerProject,
|
|
231
250
|
encoding: 'utf8',
|
|
@@ -283,7 +302,7 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
283
302
|
|
|
284
303
|
const result = spawnSync(
|
|
285
304
|
process.execPath,
|
|
286
|
-
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '
|
|
305
|
+
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '10'],
|
|
287
306
|
{ cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
|
|
288
307
|
);
|
|
289
308
|
|
|
@@ -301,10 +320,36 @@ describe('gate-runner.js — runGateAdapter', () => {
|
|
|
301
320
|
|
|
302
321
|
const result = spawnSync(
|
|
303
322
|
process.execPath,
|
|
304
|
-
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '
|
|
323
|
+
[path.join(packageFixture, 'bin', 'xp-gate.js'), 'check', '10', targetProject],
|
|
305
324
|
{ cwd: tmpProject, encoding: 'utf8', timeout: 30000 }
|
|
306
325
|
);
|
|
307
326
|
|
|
308
327
|
expect(result.status).toBe(1);
|
|
309
328
|
}, 30000);
|
|
329
|
+
|
|
330
|
+
// The mirror image of the tests above, asserted on the resolver rather than on
|
|
331
|
+
// CLI output. A project that declares no variant must resolve to nothing, so
|
|
332
|
+
// the built-in implementation still runs.
|
|
333
|
+
//
|
|
334
|
+
// Asserted here, not through `check 9`: BOTH implementations print "Semgrep"
|
|
335
|
+
// (githooks/gate-9.sh says it five times too), and a bash fallback that finds
|
|
336
|
+
// no script prints neither banner, so every stdout-based assertion passed even
|
|
337
|
+
// with the TypeScript module hard-disabled -- the guard proved nothing.
|
|
338
|
+
it('resolves no variant when the project declares none (REQ-standalone-gates-05)', () => {
|
|
339
|
+
const plainProject = path.join(tmpProject, 'plain-target');
|
|
340
|
+
fs.mkdirSync(plainProject, { recursive: true });
|
|
341
|
+
|
|
342
|
+
expect(findConsumerGateVariant('gate-9', plainProject)).toBe(null);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it('resolves the declared variant when the project has one (REQ-standalone-gates-05)', () => {
|
|
346
|
+
const variantProject = path.join(tmpProject, 'variant-target');
|
|
347
|
+
const gatesDir = path.join(variantProject, 'githooks', 'gates');
|
|
348
|
+
fs.mkdirSync(gatesDir, { recursive: true });
|
|
349
|
+
const variantPath = path.join(gatesDir, 'gate-9-project-choice.sh');
|
|
350
|
+
fs.writeFileSync(variantPath, 'exit 0\n');
|
|
351
|
+
|
|
352
|
+
const resolved = findConsumerGateVariant('gate-9', variantProject);
|
|
353
|
+
expect(resolved).toBe(variantPath);
|
|
354
|
+
});
|
|
310
355
|
});
|
|
@@ -130,4 +130,51 @@ describe('shared-utils', () => {
|
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
describe('copyDirRecursive (#640 concurrency)', () => {
|
|
134
|
+
// The mirrored trees this walks are rewritten in place by
|
|
135
|
+
// scripts/sync-package-content.js. A concurrent init enumerates the
|
|
136
|
+
// directory, then finds an entry gone -- ENOENT on a file that reappears
|
|
137
|
+
// microseconds later. That must not abort the install.
|
|
138
|
+
it('skips a file that disappears between enumeration and copy', () => {
|
|
139
|
+
const src = path.join(tmpRoot, 'src');
|
|
140
|
+
const dest = path.join(tmpRoot, 'dest');
|
|
141
|
+
fs.mkdirSync(src, { recursive: true });
|
|
142
|
+
fs.writeFileSync(path.join(src, 'stable.sh'), 'stable');
|
|
143
|
+
fs.writeFileSync(path.join(src, 'vanishing.sh'), 'vanishing');
|
|
144
|
+
|
|
145
|
+
const realReadFileSync = fs.readFileSync;
|
|
146
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation((p, ...rest) => {
|
|
147
|
+
if (String(p).endsWith('vanishing.sh')) {
|
|
148
|
+
const err = new Error('ENOENT: no such file or directory');
|
|
149
|
+
err.code = 'ENOENT';
|
|
150
|
+
throw err;
|
|
151
|
+
}
|
|
152
|
+
return realReadFileSync(p, ...rest);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const { copyDirRecursive } = require('../shared-utils');
|
|
156
|
+
expect(() => copyDirRecursive(src, dest)).not.toThrow();
|
|
157
|
+
expect(fs.existsSync(path.join(dest, 'stable.sh'))).toBe(true);
|
|
158
|
+
expect(fs.existsSync(path.join(dest, 'vanishing.sh'))).toBe(false);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('still surfaces a real read error instead of silently skipping', () => {
|
|
162
|
+
// Only ENOENT means "being replaced". A permission or disk error is a real
|
|
163
|
+
// failure, and swallowing it would ship a half-installed hook set.
|
|
164
|
+
const src = path.join(tmpRoot, 'src2');
|
|
165
|
+
const dest = path.join(tmpRoot, 'dest2');
|
|
166
|
+
fs.mkdirSync(src, { recursive: true });
|
|
167
|
+
fs.writeFileSync(path.join(src, 'locked.sh'), 'locked');
|
|
168
|
+
|
|
169
|
+
vi.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
170
|
+
const err = new Error('EACCES: permission denied');
|
|
171
|
+
err.code = 'EACCES';
|
|
172
|
+
throw err;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const { copyDirRecursive } = require('../shared-utils');
|
|
176
|
+
expect(() => copyDirRecursive(src, dest)).toThrow(/EACCES/);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
133
180
|
});
|
package/lib/gate-runner.js
CHANGED
|
@@ -361,16 +361,69 @@ function resolveNpxInvocation(args) {
|
|
|
361
361
|
return ['npx', args];
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* A project-local gate variant, if the project declares one.
|
|
366
|
+
*
|
|
367
|
+
* `<consumer cwd>/githooks/gates/gate-<n>-<name>.sh` is where a PROJECT puts its
|
|
368
|
+
* own variant of a gate: the hyphenated name is the point, because it lets one
|
|
369
|
+
* project keep several variants where a flat `gate-<n>.sh` cannot express the
|
|
370
|
+
* choice (#672, restored by 7b43666a). `xp-gate init` never creates that
|
|
371
|
+
* directory, so a stock project simply falls through to the built-in
|
|
372
|
+
* implementation and nothing changes for it.
|
|
373
|
+
*
|
|
374
|
+
* This is deliberately NOT the same as `<project>/src/gates/<gate>.ts`, which
|
|
375
|
+
* `findTsGateEntry` refuses to read (#672): that path let the party being
|
|
376
|
+
* checked supply the implementation, so `process.exit(0)` passed every gate.
|
|
377
|
+
* Declaring a variant under `githooks/gates/` is an explicit, named opt-in;
|
|
378
|
+
* dropping a file into `src/gates/` is not.
|
|
379
|
+
*
|
|
380
|
+
* @param {string} gateModule e.g. 'gate-9'
|
|
381
|
+
* @param {string} cwd project root to search
|
|
382
|
+
* @returns {string|null}
|
|
383
|
+
*/
|
|
384
|
+
function findConsumerGateVariant(gateModule, cwd) {
|
|
385
|
+
const gateNum = gateModule.replace('gate-', '');
|
|
386
|
+
// Strictly the project's own declaration. `resolveGateScript` additionally
|
|
387
|
+
// probes the flat `githooks/gate-<n>.sh` and the global install
|
|
388
|
+
// (~/.config/xp-gate/adapters/); treating either as "the project declared a
|
|
389
|
+
// variant" would hand every gate to whichever bash copy happens to be
|
|
390
|
+
// installed -- on a machine with a global install that is *always*. A variant
|
|
391
|
+
// is a file the project deliberately named, so only `githooks/gates/` counts.
|
|
392
|
+
const gatesDir = path.join(cwd, 'githooks', 'gates');
|
|
393
|
+
const declared = matchGateFileIn(gatesDir, gateNum);
|
|
394
|
+
return declared ? path.join(gatesDir, declared) : null;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Which implementation runs this gate: a project-declared variant, the built-in
|
|
399
|
+
* TypeScript module, or a bash fallback.
|
|
400
|
+
*
|
|
401
|
+
* Split out of `runTsGate` so that function stays about running the gate. The
|
|
402
|
+
* precedence is a contract with two sides -- `githooks/gates/` is the project's
|
|
403
|
+
* opt-in, and the built-in module must remain reachable when nobody opted in --
|
|
404
|
+
* so it is named here rather than inlined in the runner.
|
|
405
|
+
*
|
|
406
|
+
* @returns {{kind: 'variant', script: string} | {kind: 'module', entry: string} | {kind: 'fallback', script: string} | {kind: 'none'}}
|
|
407
|
+
*/
|
|
408
|
+
function resolveGateImplementation(gateModule, targetCwd) {
|
|
409
|
+
const variant = findConsumerGateVariant(gateModule, targetCwd);
|
|
410
|
+
if (variant) return { kind: 'variant', script: variant };
|
|
411
|
+
|
|
412
|
+
const entry = findTsGateEntry(gateModule);
|
|
413
|
+
if (entry) return { kind: 'module', entry };
|
|
414
|
+
|
|
415
|
+
const script = resolveGateScript(gateModule.replace('gate-', ''), targetCwd);
|
|
416
|
+
return script ? { kind: 'fallback', script } : { kind: 'none' };
|
|
417
|
+
}
|
|
418
|
+
|
|
364
419
|
function runTsGate(gateModule, targetPath, opts = {}) {
|
|
365
420
|
const targetCwd = resolveGateCwd(targetPath);
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return runGateAdapter(script, targetCwd, opts);
|
|
373
|
-
}
|
|
421
|
+
const impl = resolveGateImplementation(gateModule, targetCwd);
|
|
422
|
+
|
|
423
|
+
if (impl.kind === 'variant' || impl.kind === 'fallback') {
|
|
424
|
+
return runGateAdapter(impl.script, targetCwd, opts);
|
|
425
|
+
}
|
|
426
|
+
if (impl.kind === 'none') {
|
|
374
427
|
// #618: nothing ran. Before this, the function fell through to `return 0`
|
|
375
428
|
// and the --all summary counted the gate as passed.
|
|
376
429
|
console.log(`Gate ${gateModule}: TypeScript module not found and no bash fallback available.`);
|
|
@@ -378,7 +431,7 @@ function runTsGate(gateModule, targetPath, opts = {}) {
|
|
|
378
431
|
return GATE_NOT_RUN;
|
|
379
432
|
}
|
|
380
433
|
|
|
381
|
-
const args = ['-y', 'tsx', entry, '--run'];
|
|
434
|
+
const args = ['-y', 'tsx', impl.entry, '--run'];
|
|
382
435
|
if (targetPath) args.push('--cwd', targetPath);
|
|
383
436
|
|
|
384
437
|
// spawnSync reports a spawn failure (npx missing) on `error`, throws only on a
|
|
@@ -508,21 +561,28 @@ const GATE_REGISTRY = {
|
|
|
508
561
|
return runTsGate('gate-8', targetPath);
|
|
509
562
|
},
|
|
510
563
|
},
|
|
564
|
+
// Numbering follows the HOOK, which is where gates actually execute (#677).
|
|
565
|
+
// These two were swapped here: the CLI called 9 "SAST" and 10 "Build Integrity"
|
|
566
|
+
// while the hook runs 9=Build integrity and 10=Semgrep SAST. A user reading
|
|
567
|
+
// "❌ BLOCKED - Gate 9: Build integrity check failed" in their commit output and
|
|
568
|
+
// then running `xp-gate check --gates 9` got semgrep -- a different gate, reported
|
|
569
|
+
// green. The hook is the majority implementation and the one the error message
|
|
570
|
+
// names, so its numbering wins.
|
|
511
571
|
'9': {
|
|
572
|
+
name: 'Build Integrity',
|
|
573
|
+
description: 'TypeScript compilation, npm pack, and import validation',
|
|
574
|
+
aliases: ['build', '9'],
|
|
575
|
+
preCommitOnly: true,
|
|
576
|
+
reason: 'Build integrity checks require language detection and build tool context',
|
|
577
|
+
},
|
|
578
|
+
'10': {
|
|
512
579
|
name: 'SAST Security',
|
|
513
580
|
description: 'semgrep static application security testing',
|
|
514
|
-
aliases: ['sast', 'semgrep', '
|
|
581
|
+
aliases: ['sast', 'semgrep', '10'],
|
|
515
582
|
run: (targetPath) => {
|
|
516
583
|
return runTsGate('gate-9', targetPath);
|
|
517
584
|
},
|
|
518
585
|
},
|
|
519
|
-
'10': {
|
|
520
|
-
name: 'Build Integrity',
|
|
521
|
-
description: 'TypeScript compilation, npm pack, and import validation',
|
|
522
|
-
aliases: ['build', '10'],
|
|
523
|
-
preCommitOnly: true,
|
|
524
|
-
reason: 'Build integrity checks require language detection and build tool context',
|
|
525
|
-
},
|
|
526
586
|
'11': {
|
|
527
587
|
name: 'Sprint Flow Enforcement',
|
|
528
588
|
description: 'Sprint state and delphi-review validation',
|
|
@@ -530,6 +590,26 @@ const GATE_REGISTRY = {
|
|
|
530
590
|
preCommitOnly: true,
|
|
531
591
|
reason: 'Requires sprint state (.sprint-state/) and git branch context',
|
|
532
592
|
},
|
|
593
|
+
// Gates 12 and 13 block commits in the hook but were absent here entirely, so
|
|
594
|
+
// `xp-gate check --gates 12` answered "Unknown gate(s)" for a gate that had just
|
|
595
|
+
// blocked the user (#677). Registered with an explicit reason rather than
|
|
596
|
+
// pretending to re-implement them: both need the staged-diff and per-language
|
|
597
|
+
// build context the hook assembles, and a half-correct reimplementation that
|
|
598
|
+
// answered PASS would be worse than saying so.
|
|
599
|
+
'12': {
|
|
600
|
+
name: 'File Hygiene',
|
|
601
|
+
description: 'conflict markers (blocking) + YAML/JSON syntax (blocking) + whitespace warnings',
|
|
602
|
+
aliases: ['hygiene', 'file-hygiene', '12'],
|
|
603
|
+
preCommitOnly: true,
|
|
604
|
+
reason: 'File hygiene inspects the staged diff, which only exists inside the hook',
|
|
605
|
+
},
|
|
606
|
+
'13': {
|
|
607
|
+
name: 'Compiler Strictness',
|
|
608
|
+
description: 'gcc/g++ -Wall -Wextra, javac -Xlint, Maven/Gradle warnings on changed files',
|
|
609
|
+
aliases: ['compiler', 'strictness', '13'],
|
|
610
|
+
preCommitOnly: true,
|
|
611
|
+
reason: 'Compiler strictness needs the changed-file set and a discovered toolchain',
|
|
612
|
+
},
|
|
533
613
|
'python-health': {
|
|
534
614
|
name: 'Python Environment Health',
|
|
535
615
|
description: 'Comprehensive Python environment diagnostics',
|
|
@@ -718,4 +798,4 @@ async function runGate(gateId, targetPath, opts = {}) {
|
|
|
718
798
|
return 1;
|
|
719
799
|
}
|
|
720
800
|
|
|
721
|
-
module.exports = { GATE_REGISTRY, GATE_NOT_RUN, getGateInfo, getAllGates, runGate, resolveAlias, getAliases, runGateAdapter, findTsGateEntry, resolveGateScript, SOURCE_FILE_EXTENSIONS, resolveAllModeChangedFiles, describeEmptyChangedFiles, describeChangedFilesScope };
|
|
801
|
+
module.exports = { findConsumerGateVariant, GATE_REGISTRY, GATE_NOT_RUN, getGateInfo, getAllGates, runGate, resolveAlias, getAliases, runGateAdapter, findTsGateEntry, resolveGateScript, SOURCE_FILE_EXTENSIONS, resolveAllModeChangedFiles, describeEmptyChangedFiles, describeChangedFilesScope };
|
package/lib/shared-utils.js
CHANGED
|
@@ -8,6 +8,19 @@ const path = require('path');
|
|
|
8
8
|
/**
|
|
9
9
|
* Recursively copy a directory.
|
|
10
10
|
* Pure function: only uses fs/path params, no global config.
|
|
11
|
+
*
|
|
12
|
+
* A file that disappears between `readdirSync` and `copyFileSync` is skipped
|
|
13
|
+
* rather than crashing the whole copy. That is not defensive padding: the
|
|
14
|
+
* mirrored trees this walks (`hooks/`, `adapters/`) are rewritten in place by
|
|
15
|
+
* `scripts/sync-package-content.js`, which unlinks and recreates each entry. A
|
|
16
|
+
* concurrent `xp-gate init` (or a test suite that runs init while another test
|
|
17
|
+
* invokes the sync) enumerates the directory, then finds the entry gone --
|
|
18
|
+
* `ENOENT` on a file that will exist again microseconds later. Aborting the
|
|
19
|
+
* install there would report a broken package for a transient race.
|
|
20
|
+
*
|
|
21
|
+
* Copies the CONTENT instead of `copyFileSync` so the read and the write both
|
|
22
|
+
* see a consistent snapshot, and only ENOENT is tolerated -- permission and
|
|
23
|
+
* disk errors still surface, because those are real failures.
|
|
11
24
|
*/
|
|
12
25
|
function copyDirRecursive(src, dest) {
|
|
13
26
|
fs.mkdirSync(dest, { recursive: true });
|
|
@@ -18,11 +31,24 @@ function copyDirRecursive(src, dest) {
|
|
|
18
31
|
if (entry.isDirectory()) {
|
|
19
32
|
copyDirRecursive(srcPath, destPath);
|
|
20
33
|
} else {
|
|
21
|
-
|
|
34
|
+
copyFileContents(srcPath, destPath);
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
}
|
|
25
38
|
|
|
39
|
+
/** Copy one file, treating "vanished mid-copy" as a skip. @returns {boolean} whether it was copied */
|
|
40
|
+
function copyFileContents(srcPath, destPath) {
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = fs.readFileSync(srcPath);
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === 'ENOENT') return false;
|
|
46
|
+
throw err;
|
|
47
|
+
}
|
|
48
|
+
fs.writeFileSync(destPath, data);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
function readXpGateConfig() {
|
|
27
53
|
const cfgPath = path.join(require('os').homedir(), '.xp-gate', 'config.json');
|
|
28
54
|
try {
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 97e3c2ec
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.18.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-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 97e3c2ec
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.18.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.35.
|
|
3
|
+
"version": "0.35.18",
|
|
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-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 97e3c2ec
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.18.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-31
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 97e3c2ec
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.18.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.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
# @test REQ-688
|
|
3
|
+
# @intent 验证 sprint-flow 的校验脚本能接受 `xp-gate sprint-init` 自己写出的状态文件
|
|
4
|
+
# ——脚本曾要求 sprint_id/current_phase/phases 这套从未被写入过的键,
|
|
5
|
+
# 对 CLI 刚创建的 sprint 恒判失败(#679)
|
|
6
|
+
# @covers AC-688-01, AC-688-02
|
|
7
|
+
#
|
|
8
|
+
# 为什么用真 sprint-init 而不是手写一个 fixture:缺陷的本质是「校验方与生产方对
|
|
9
|
+
# schema 的认知不一致」。手写 fixture 会把「我认为 CLI 写什么」固化下来,双方一起
|
|
10
|
+
# 错就一起绿。只有让生产方真的产出、校验方真的读,才能发现这类偏差。
|
|
11
|
+
|
|
12
|
+
setup() {
|
|
13
|
+
REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../.." && pwd)"
|
|
14
|
+
VERIFY="$REPO_ROOT/skills/sprint-flow/scripts/verify-sprint-state.sh"
|
|
15
|
+
CLI="$REPO_ROOT/src/npm-package/bin/xp-gate.js"
|
|
16
|
+
WORK="$(mktemp -d)"
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
teardown() {
|
|
20
|
+
rm -rf "$WORK"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@test "AC-688-01: accepts state written by xp-gate sprint-init" {
|
|
24
|
+
cd "$WORK"
|
|
25
|
+
git init -q .
|
|
26
|
+
echo '{}' > package.json
|
|
27
|
+
git add -A
|
|
28
|
+
git -c user.email=t@t -c user.name=t commit -q -m init
|
|
29
|
+
|
|
30
|
+
run node "$CLI" sprint-init "probe task"
|
|
31
|
+
[ -f ".sprint-state/sprint-state.json" ]
|
|
32
|
+
|
|
33
|
+
run bash "$VERIFY"
|
|
34
|
+
[ "$status" -eq 0 ]
|
|
35
|
+
[[ "$output" == *"OK:"* ]]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@test "AC-688-02: still rejects a state file missing real keys" {
|
|
39
|
+
# The fix must not become "accept anything": a file that lost its task or its
|
|
40
|
+
# phase is genuinely broken and the verifier should say so.
|
|
41
|
+
mkdir -p "$WORK/.sprint-state"
|
|
42
|
+
printf '{"id":"s-1","phase":"BUILD"}\n' > "$WORK/.sprint-state/sprint-state.json"
|
|
43
|
+
|
|
44
|
+
cd "$WORK"
|
|
45
|
+
run bash "$VERIFY"
|
|
46
|
+
[ "$status" -eq 1 ]
|
|
47
|
+
[[ "$output" == *"task_description"* ]]
|
|
48
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Definition of Done(统一完成定义)
|
|
2
|
+
|
|
3
|
+
一个 REQ 只有在**全部 10 项**都可指向证据时才算完成。这份清单是 #607 的落地,
|
|
4
|
+
对应指南 §12;它存在的理由是本仓库历史上反复出现「以为完成了,其实没有」:
|
|
5
|
+
|
|
6
|
+
- **#561** 覆盖率阈值形状写错(`Path` 而非 `Partial<Record>`),声明 80% 但整个门禁是空转
|
|
7
|
+
- **#570** 覆盖率检查 fail-open,跑不起来时报告通过
|
|
8
|
+
- **#585** Gate 5c 静默通过
|
|
9
|
+
- **.xp-gate/modules 陈旧副本**(2026-10-19)劫持 Gate 6 数周,hook 报的警告数和独立扫描永远对不上
|
|
10
|
+
|
|
11
|
+
共同点全部是:**某一步从未真正执行,而没有任何东西把这件事说出来。**
|
|
12
|
+
所以这份清单里每一项的判据都是「证据在哪儿」,不是「做没做」。
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 10 项完成定义
|
|
17
|
+
|
|
18
|
+
| # | 条目 | 判据(必须能指出位置) |
|
|
19
|
+
|---|------|------------------------|
|
|
20
|
+
| 1 | **需求可追溯** | `specification.yaml` 有 REQ + AC;测试文件带 `@covers AC-xxx`;`scripts/__tests__/ac-reference-guard.test.cjs` 通过 |
|
|
21
|
+
| 2 | **测试先行** | 实现前存在失败的测试;或说明为何不适用(纯文档/配置) |
|
|
22
|
+
| 3 | **测试真的能失败** | 至少一个变异体/破坏被测试抓住。「测试通过」在从未运行时和「功能正确」无法区分 |
|
|
23
|
+
| 4 | **验证命令已运行** | 记录实际命令与输出,不是「应该是绿的」。CI 命令与本地命令一致 |
|
|
24
|
+
| 5 | **门禁未降级** | 无 `--no-verify`;无新增基线额度来吸收本次改动自身的告警 |
|
|
25
|
+
| 6 | **镜像已同步** | 改了 `githooks/`/`skills/`/`plugins/` → `bash scripts/check-hook-mirror.sh` 输出 PASS |
|
|
26
|
+
| 7 | **不做未经要求的相邻改动** | 每行改动可追溯到本 REQ;顺手发现的无关死代码只提及不删除 |
|
|
27
|
+
| 8 | **已知限制已写明** | 没做的部分、以及为什么没做,写进 issue/提交,而不是留空 |
|
|
28
|
+
| 9 | **文档与实现一致** | 门禁数量、CLI 表、阈值等出现在多处的事实同步更新(见 `AGENTS.md` "Known Drift") |
|
|
29
|
+
| 10 | **可回滚** | 提交粒度单一;出问题时能单独 revert 而不牵连其他改动 |
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 相位落点
|
|
34
|
+
|
|
35
|
+
- **Phase 4 VERIFY**:逐项过 1/2/3/4/5/6,任何一项拿不出证据 → 不得进入 SHIP。
|
|
36
|
+
- **Phase 5 SHIP**:逐项过 7/8/9/10,然后才推送。
|
|
37
|
+
- **Phase 6 CLOSE**:把第 8 项「已知限制」回填到 issue 并关闭。
|
|
38
|
+
|
|
39
|
+
## 常见误判
|
|
40
|
+
|
|
41
|
+
| 看起来完成了 | 实际没有 |
|
|
42
|
+
|---|---|
|
|
43
|
+
| 「测试通过」 | 没跑过会失败的版本,不知道它能不能失败 |
|
|
44
|
+
| 「门禁有配置」 | 配置形状错/键名不对时被静默丢弃,等于没有(#561) |
|
|
45
|
+
| 「工具缺失时跳过」 | 跳过被当成通过;只有 Gate 8 是 fail-closed |
|
|
46
|
+
| 「CI 绿了」 | CI 可能没跑到那一步(`if:` 条件不满足),或跑的是旧副本(#451) |
|
|
47
|
+
| 「注释写了这么做」 | 注释与代码漂移;只有测试能固定行为 |
|
|
@@ -142,6 +142,22 @@ ELSE:
|
|
|
142
142
|
- Web 前端附加: ui-review report + OPTIONAL QA/design-review/benchmark reports
|
|
143
143
|
- 验证通过的 MVP
|
|
144
144
|
|
|
145
|
+
### 完成定义(DoD 第 1–6 项)
|
|
146
|
+
|
|
147
|
+
离开 VERIFY 前逐项过一遍 [`definition-of-done.md`](./definition-of-done.md)。**任何一项拿不出证据 → 不得进入 SHIP。**
|
|
148
|
+
|
|
149
|
+
| # | 条目 | 在 VERIFY 的检查方式 |
|
|
150
|
+
|---|------|----------------------|
|
|
151
|
+
| 1 | 需求可追溯 | `npx vitest run scripts/__tests__/ac-reference-guard.test.cjs` 通过(测试引用的 AC 都在 spec 里) |
|
|
152
|
+
| 2 | 测试先行 | 有 RED 证据(提交历史或记录),或说明为何不适用 |
|
|
153
|
+
| 3 | **测试真的能失败** | 至少证明过一个变异体会被抓住——「测试通过」在从未运行时与「功能正确」无法区分 |
|
|
154
|
+
| 4 | 验证命令已运行 | 记录实际命令 + 输出,不是「应该是绿的」 |
|
|
155
|
+
| 5 | 门禁未降级 | 无 `--no-verify`;无为了本次改动新增的基线额度 |
|
|
156
|
+
| 6 | 镜像已同步 | `bash scripts/check-hook-mirror.sh` → PASS |
|
|
157
|
+
|
|
158
|
+
第 5 项值得单独说明:**加基线额度来吸收自己刚产生的告警,等于把门禁关掉一次。**
|
|
159
|
+
`.warnings-baseline.json` 只应记录「改动之前就存在」的告警。
|
|
160
|
+
|
|
145
161
|
---
|
|
146
162
|
|
|
147
163
|
## Part B: FEEDBACK CAPTURE(反馈获取)
|