@boyingliu01/xp-gate 0.35.16 → 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.
Files changed (75) hide show
  1. package/gate-12-file-hygiene.sh +26 -4
  2. package/gate-13-compiler-strictness.sh +12 -0
  3. package/gates/gate-3.ts +77 -21
  4. package/gates/gate-8.ts +39 -26
  5. package/gates/gate-9.ts +4 -4
  6. package/hooks/gate-12-file-hygiene.sh +26 -4
  7. package/hooks/gate-13-compiler-strictness.sh +12 -0
  8. package/hooks/pre-commit +82 -12
  9. package/hooks/pre-push +11 -2
  10. package/lib/__tests__/doctor.test.js +9 -0
  11. package/lib/__tests__/gate-numbering.test.js +52 -0
  12. package/lib/__tests__/gate-runner.test.js +59 -14
  13. package/lib/__tests__/shared-utils.test.js +47 -0
  14. package/lib/gate-runner.js +98 -18
  15. package/lib/mattpocock-license-verdict.json +1 -1
  16. package/lib/shared-utils.js +27 -1
  17. package/mock-policy/AGENTS.md +2 -2
  18. package/mutation/AGENTS.md +2 -2
  19. package/mutation/runners/mutmut-runner.ts +165 -70
  20. package/package.json +1 -1
  21. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  22. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  23. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  24. package/plugins/claude-code/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  25. package/plugins/claude-code/skills/sprint-flow/references/definition-of-done.md +47 -0
  26. package/plugins/claude-code/skills/sprint-flow/references/phase-4-verify.md +16 -0
  27. package/plugins/claude-code/skills/sprint-flow/references/phase-5-ship.md +14 -0
  28. package/plugins/claude-code/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  29. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  30. package/plugins/dsh/package.json +1 -1
  31. package/plugins/dsh/skills/delphi-review/AGENTS.md +2 -2
  32. package/plugins/dsh/skills/sprint-flow/AGENTS.md +2 -2
  33. package/plugins/dsh/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  34. package/plugins/dsh/skills/sprint-flow/references/definition-of-done.md +47 -0
  35. package/plugins/dsh/skills/sprint-flow/references/phase-4-verify.md +16 -0
  36. package/plugins/dsh/skills/sprint-flow/references/phase-5-ship.md +14 -0
  37. package/plugins/dsh/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  38. package/plugins/dsh/skills/test-specification-alignment/AGENTS.md +2 -2
  39. package/plugins/dsh/src/delphi-findings.test.ts +32 -6
  40. package/plugins/dsh/src/exit-code.test.ts +6 -0
  41. package/plugins/opencode/package.json +1 -1
  42. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  43. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  44. package/plugins/opencode/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  45. package/plugins/opencode/skills/sprint-flow/references/definition-of-done.md +47 -0
  46. package/plugins/opencode/skills/sprint-flow/references/phase-4-verify.md +16 -0
  47. package/plugins/opencode/skills/sprint-flow/references/phase-5-ship.md +14 -0
  48. package/plugins/opencode/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  49. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  50. package/plugins/qoder/plugin.json +1 -1
  51. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  52. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  53. package/plugins/qoder/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  54. package/plugins/qoder/skills/sprint-flow/references/definition-of-done.md +47 -0
  55. package/plugins/qoder/skills/sprint-flow/references/phase-4-verify.md +16 -0
  56. package/plugins/qoder/skills/sprint-flow/references/phase-5-ship.md +14 -0
  57. package/plugins/qoder/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  58. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  59. package/principles/AGENTS.md +2 -2
  60. package/principles/adapters/import-clause.ts +57 -0
  61. package/principles/adapters/member-scan.ts +73 -0
  62. package/principles/adapters/position-analysis.ts +43 -0
  63. package/principles/adapters/quote-state.ts +80 -0
  64. package/principles/adapters/typescript.ts +57 -116
  65. package/principles/rules/clean-code/god-class.ts +37 -14
  66. package/principles/rules/solid/dip.ts +15 -2
  67. package/scripts/__tests__/sync-package-content.test.js +114 -46
  68. package/skills/delphi-review/AGENTS.md +2 -2
  69. package/skills/sprint-flow/AGENTS.md +2 -2
  70. package/skills/sprint-flow/__tests__/verify-sprint-state.test.bats +48 -0
  71. package/skills/sprint-flow/references/definition-of-done.md +47 -0
  72. package/skills/sprint-flow/references/phase-4-verify.md +16 -0
  73. package/skills/sprint-flow/references/phase-5-ship.md +14 -0
  74. package/skills/sprint-flow/scripts/verify-sprint-state.sh +14 -1
  75. 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
- fs.writeFileSync(
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
- fs.writeFileSync(
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
- execFileSync('git', ['init', '-q'], { cwd: tmpProject });
59
- execFileSync('git', ['config', 'user.email', 'test@example.com'], { cwd: tmpProject });
60
- execFileSync('git', ['config', 'user.name', 'XP-Gate Test'], { cwd: tmpProject });
61
- fs.writeFileSync(path.join(tmpProject, 'tracked.ts'), 'const value = 1;\n');
62
- execFileSync('git', ['add', 'tracked.ts'], { cwd: tmpProject });
63
- execFileSync('git', ['-c', 'core.hooksPath=/dev/null', 'commit', '-qm', 'initial'], {
64
- cwd: tmpProject,
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', '9'], {
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', '9', targetProject],
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', '9'],
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', '9', targetProject],
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
  });
@@ -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 entry = findTsGateEntry(gateModule);
367
- if (!entry) {
368
- // Fallback to bash script
369
- const gateNum = gateModule.replace('gate-', '');
370
- const script = resolveGateScript(gateNum, targetCwd);
371
- if (script) {
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', '9'],
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 };
@@ -2,6 +2,6 @@
2
2
  "verdict": "PASS",
3
3
  "license_type": "MIT",
4
4
  "sha256": "0e7ac423bf2c6e223b7c5b156f8cf72da49d748e56a1641402c31f22ad07dbb5",
5
- "checked_at": "2026-10-31T05:05:10.442Z",
5
+ "checked_at": "2026-10-31T16:53:19.066Z",
6
6
  "source": "network"
7
7
  }
@@ -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
- fs.copyFileSync(srcPath, destPath);
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 {
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 97e3c2ec
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
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.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-10-31
4
- **Commit:** 3aa988e7
4
+ **Commit:** 97e3c2ec
5
5
  **Branch:** main
6
- **Version:** 0.35.16.0
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.