@boyingliu01/xp-gate 0.35.1 → 0.35.2
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 +15 -0
- package/lib/__tests__/doctor-drift-direction.test.js +104 -0
- package/lib/__tests__/gate-coverage.test.js +137 -0
- package/lib/__tests__/test-alignment.test.ts +45 -4
- package/lib/doctor.js +82 -8
- package/lib/gate-coverage.js +315 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/test-alignment.ts +27 -7
- 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/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/test-specification-alignment/AGENTS.md +2 -2
- 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/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/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/bin/xp-gate.js
CHANGED
|
@@ -350,6 +350,21 @@ const COMMANDS = {
|
|
|
350
350
|
},
|
|
351
351
|
usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
|
|
352
352
|
},
|
|
353
|
+
'gate-coverage': {
|
|
354
|
+
description: 'One view of compliance against every gate: PASS / DEBT / SKIP / FAIL, with debt and evidence (#636)',
|
|
355
|
+
run: subargs => {
|
|
356
|
+
const { main } = require('../lib/gate-coverage.js');
|
|
357
|
+
const { execFileSync } = require('node:child_process');
|
|
358
|
+
let headCommit;
|
|
359
|
+
try {
|
|
360
|
+
headCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim();
|
|
361
|
+
} catch {
|
|
362
|
+
headCommit = undefined;
|
|
363
|
+
}
|
|
364
|
+
process.exit(main(subargs, { headCommit }));
|
|
365
|
+
},
|
|
366
|
+
usage: 'xp-gate gate-coverage [--json] [--debt] [--strict] [--branch <name>]'
|
|
367
|
+
},
|
|
353
368
|
'rework': {
|
|
354
369
|
description: 'Report rework: first-pass rate, rounds before landing, top blocking reasons (#651)',
|
|
355
370
|
run: subargs => {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-670 doctor: classify module drift by DIRECTION, not by line counts
|
|
3
|
+
* @intent `xp-gate doctor` used to report five gate modules as
|
|
4
|
+
*
|
|
5
|
+
* diverged both ways — githooks/ has 70 line(s) the install lacks,
|
|
6
|
+
* the install has 2 line(s) githooks/ lacks; sync refuses without an
|
|
7
|
+
* explicit decision
|
|
8
|
+
*
|
|
9
|
+
* I checked every "<" line (installed-only) by hand: all of them were
|
|
10
|
+
* OLDER versions of code the repository had already rewritten. The
|
|
11
|
+
* "install has 2 lines githooks lacks" was purely the shape a re-wrapped
|
|
12
|
+
* line leaves in a line-level diff — the same logic, with a `&&` or a
|
|
13
|
+
* pipe moved onto a new line, produces a line present on neither side.
|
|
14
|
+
*
|
|
15
|
+
* The wording matters more than it looks. "diverged both ways" plus
|
|
16
|
+
* "sync refuses without an explicit decision" tells the reader that both
|
|
17
|
+
* sides hold something worth keeping and a human must choose. The
|
|
18
|
+
* consequence is not extra keystrokes: it makes people AVOID syncing,
|
|
19
|
+
* because syncing appears to discard the installed copy's work. And not
|
|
20
|
+
* syncing is exactly #451 — editing githooks/ and having nothing happen.
|
|
21
|
+
*
|
|
22
|
+
* So the classifier must ask "does the installed copy hold anything the
|
|
23
|
+
* repo genuinely lacks?" instead of "are both line counts non-zero?".
|
|
24
|
+
* Token-level comparison answers the real question: whitespace and line
|
|
25
|
+
* breaks are formatting, and formatting is not work.
|
|
26
|
+
* @covers AC-670-64, AC-670-65, AC-670-66
|
|
27
|
+
*/
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const { moduleDriftDetail } = require('../doctor');
|
|
31
|
+
|
|
32
|
+
/** Join lines the way a formatter re-wraps them: same tokens, different breaks. */
|
|
33
|
+
const wrap = (...lines) => lines.join('\n');
|
|
34
|
+
|
|
35
|
+
describe('moduleDriftDetail direction classification (#667)', () => {
|
|
36
|
+
it('AC-670-64: a pure re-wrap is "behind", not "diverged both ways"', () => {
|
|
37
|
+
// Same logic; the `&&` moved to the next line. A line-level diff calls this
|
|
38
|
+
// two-sided drift. Nothing was authored on the installed side.
|
|
39
|
+
// Real shape measured on gate-3.sh: an older install re-wrapped the same
|
|
40
|
+
// statements, so a line-level diff reports lines unique to BOTH sides while
|
|
41
|
+
// the token content is identical (measured: repoOnly=0, installedOnly=0).
|
|
42
|
+
const repo = wrap(
|
|
43
|
+
'RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"',
|
|
44
|
+
'if [ -f "$RATCHET_LIB" ]; then . "$RATCHET_LIB"; fi',
|
|
45
|
+
'known.set(norm(f.file) + "::" + f.name, Number(f.ccn) || 0);',
|
|
46
|
+
);
|
|
47
|
+
const installed = wrap(
|
|
48
|
+
' RATCHET_LIB="$GATE_DIR/lib/ccn-ratchet.sh"',
|
|
49
|
+
' if [ -f "$RATCHET_LIB" ]; then',
|
|
50
|
+
' . "$RATCHET_LIB"',
|
|
51
|
+
' fi',
|
|
52
|
+
'known.set(norm(f.file) + "::" + f.name,',
|
|
53
|
+
' Number(f.ccn) || 0);',
|
|
54
|
+
);
|
|
55
|
+
const detail = moduleDriftDetail(repo, installed);
|
|
56
|
+
expect(detail).not.toMatch(/diverged both ways/i);
|
|
57
|
+
expect(detail).toMatch(/formatting|stale|behind/i);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('AC-670-65: a genuine two-sided edit still refuses to sync silently', () => {
|
|
61
|
+
// The installed copy carries a real, machine-only hardening: a fail-closed
|
|
62
|
+
// branch that exists nowhere in the repo. Overwriting it would destroy work.
|
|
63
|
+
const repo = wrap(
|
|
64
|
+
'if [ -z "$COV" ]; then',
|
|
65
|
+
' echo "no coverage tool";',
|
|
66
|
+
' return 0;',
|
|
67
|
+
'fi',
|
|
68
|
+
);
|
|
69
|
+
const installed = wrap(
|
|
70
|
+
'if [ -z "$COV" ]; then',
|
|
71
|
+
' # FAIL CLOSED: cannot determine coverage means the check failed.',
|
|
72
|
+
' echo "❌ BLOCKED - could not determine coverage";',
|
|
73
|
+
' return 1;',
|
|
74
|
+
'fi',
|
|
75
|
+
);
|
|
76
|
+
const detail = moduleDriftDetail(repo, installed);
|
|
77
|
+
expect(detail).toMatch(/diverged both ways/i);
|
|
78
|
+
expect(detail).toMatch(/refuses|explicit decision/i);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('AC-670-66: an unreadable pair is reported honestly, never as identical', () => {
|
|
82
|
+
// readPair() returns null when either side cannot be read. Reporting that as
|
|
83
|
+
// "matches" would be a fail-open: the drift could be real and unseen.
|
|
84
|
+
const detail = moduleDriftDetail(null, null);
|
|
85
|
+
expect(detail).toMatch(/could not be read|cannot be compared/i);
|
|
86
|
+
expect(detail).not.toMatch(/matches|identical/i);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe('re-wrap is not drift (#667)', () => {
|
|
91
|
+
it('AC-670-64: a pure re-wrap yields no unique tokens on either side', () => {
|
|
92
|
+
// The end-to-end property: formatting-only differences must not count as an
|
|
93
|
+
// issue, because failing on them teaches the reader to distrust `doctor`
|
|
94
|
+
// and to skip the sync that #451 exists to encourage.
|
|
95
|
+
const { tokenDelta } = require('../doctor.js');
|
|
96
|
+
const NL = String.fromCharCode(10);
|
|
97
|
+
const repo = 'a &&' + NL + ' b' + NL;
|
|
98
|
+
const installed = 'a' + NL + ' && b' + NL;
|
|
99
|
+
const delta = tokenDelta(repo, installed);
|
|
100
|
+
expect(delta.repoOnly).toBe(0);
|
|
101
|
+
expect(delta.installedOnly).toBe(0);
|
|
102
|
+
expect(moduleDriftDetail(repo, installed)).toMatch(/formatting/i);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-636 One command that shows compliance against EVERY gate
|
|
3
|
+
* @intent Today the answer to "how healthy is this project against xp-gate's
|
|
4
|
+
* gates" takes six commands and mental arithmetic:
|
|
5
|
+
*
|
|
6
|
+
* xp-gate gate-list && xp-gate check . --all && xp-gate doctor
|
|
7
|
+
* node scripts/ccn-report.cjs --check
|
|
8
|
+
* node -e "...sum .warnings-baseline.json..."
|
|
9
|
+
* gh run list
|
|
10
|
+
*
|
|
11
|
+
* Every existing command answers "did THIS gate pass". None answers
|
|
12
|
+
* "where is the debt and what should I fix first".
|
|
13
|
+
*
|
|
14
|
+
* The single most important property is that the view has FOUR states,
|
|
15
|
+
* not three. `passed / skipped / failed` merges "did not run" into
|
|
16
|
+
* "ran and was fine", and that is exactly where technical debt hides.
|
|
17
|
+
* A gate that never executed renders identically to a gate that passed
|
|
18
|
+
* clean -- a false green, which is the most dangerous output this
|
|
19
|
+
* tooling can produce.
|
|
20
|
+
*
|
|
21
|
+
* Two data hazards drive the rest of the design:
|
|
22
|
+
*
|
|
23
|
+
* - The producer artifact numbers its gates `gate9_build_integrity` and
|
|
24
|
+
* `gate10_sast`, but the CLI calls Build Integrity Gate 10 and SAST
|
|
25
|
+
* Gate 9 -- the two are SWAPPED. Rendering the artifact naively shows
|
|
26
|
+
* the user the wrong gate name, which is worse than showing nothing.
|
|
27
|
+
* The mapping is therefore keyed on the artifact's own taxonomy and
|
|
28
|
+
* pinned by a test.
|
|
29
|
+
*
|
|
30
|
+
* - `quality-status/<branch>.json` records the commit it was generated
|
|
31
|
+
* from. Rendering it for a different HEAD silently presents stale
|
|
32
|
+
* results as current. This repo has had that artifact sit at an old
|
|
33
|
+
* commit for days, so staleness must be explicit, not inferred away.
|
|
34
|
+
* @covers AC-636-01, AC-636-02, AC-636-03, AC-636-04, AC-636-05, AC-636-06
|
|
35
|
+
*/
|
|
36
|
+
'use strict';
|
|
37
|
+
|
|
38
|
+
const { renderGateCoverage, buildCoverageRows, STATUS } = require('../gate-coverage');
|
|
39
|
+
|
|
40
|
+
/** A `quality-status` artifact shaped like the real producer's output. */
|
|
41
|
+
function artifact(overrides) {
|
|
42
|
+
return {
|
|
43
|
+
reportVersion: '2.0',
|
|
44
|
+
generatedAt: '2026-10-27T03:06:05Z',
|
|
45
|
+
commit: 'a0bceb9c0000000000000000000000000000000',
|
|
46
|
+
branch: 'main',
|
|
47
|
+
language: 'typescript iac',
|
|
48
|
+
overall: {
|
|
49
|
+
gatesPassed: 6,
|
|
50
|
+
gatesTotal: 12,
|
|
51
|
+
gatesSkipped: 6,
|
|
52
|
+
gatesWarned: 0,
|
|
53
|
+
gatesFailed: 0,
|
|
54
|
+
gatesEffective: 6,
|
|
55
|
+
effectivePassRate: 100,
|
|
56
|
+
scoreAllGates: 5,
|
|
57
|
+
score: 10,
|
|
58
|
+
verdict: 'PARTIAL',
|
|
59
|
+
...(overrides && overrides.overall ? overrides.overall : {}),
|
|
60
|
+
},
|
|
61
|
+
gates: {
|
|
62
|
+
gate1_static_analysis: { name: 'Static Analysis', status: 'PASS', tool: 'eslint' },
|
|
63
|
+
gate9_build_integrity: { name: 'Build Integrity', status: 'PASS', tool: 'tsc' },
|
|
64
|
+
gate10_sast: { name: 'SAST Security', status: 'PASS', tool: 'semgrep' },
|
|
65
|
+
...(overrides && overrides.gates ? overrides.gates : {}),
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('gate coverage — four states, never a false green (#636)', () => {
|
|
71
|
+
it('AC-636-01: a gate that never ran is SKIP with a reason, never folded into PASS', () => {
|
|
72
|
+
const rows = buildCoverageRows(artifact(), { headCommit: 'a0bceb9c0000000000000000000000000000000' });
|
|
73
|
+
// Gates 0/1/5/11 are hook-only: they exist but the CLI cannot run them.
|
|
74
|
+
const version = rows.find((r) => r.gate === 0);
|
|
75
|
+
expect(version, 'Gate 0 must appear even though it never ran').toBeDefined();
|
|
76
|
+
expect(version.status).toBe(STATUS.SKIP);
|
|
77
|
+
// A bare SKIP is the false green. The reason is mandatory.
|
|
78
|
+
expect(version.reason).toBeTruthy();
|
|
79
|
+
// The reason must say WHY it did not run -- "hook only" and "not run here"
|
|
80
|
+
// are both honest; a bare SKIP is not.
|
|
81
|
+
expect(version.reason).toMatch(/hook|git commit|not run/i);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('AC-636-02: the artifact gate-number swap does not mislabel the gates', () => {
|
|
85
|
+
// The producer says gate9_* = Build Integrity; the CLI says Gate 10 is Build
|
|
86
|
+
// Integrity. Rendering the artifact's own key would show SAST as Gate 10.
|
|
87
|
+
const rows = buildCoverageRows(artifact(), { headCommit: 'a0bceb9c0000000000000000000000000000000' });
|
|
88
|
+
const build = rows.find((r) => r.name === 'Build Integrity');
|
|
89
|
+
const sast = rows.find((r) => r.name === 'SAST Security');
|
|
90
|
+
expect(build, 'Build Integrity row must exist').toBeDefined();
|
|
91
|
+
expect(sast, 'SAST row must exist').toBeDefined();
|
|
92
|
+
// Both rows exist and carry the CLI's numbering, not the artifact's.
|
|
93
|
+
expect(build.gate).toBe(10);
|
|
94
|
+
expect(sast.gate).toBe(9);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('AC-636-03: a stale artifact is marked STALE instead of presented as current', () => {
|
|
98
|
+
const rows = buildCoverageRows(artifact(), { headCommit: 'ffffffffffffffffffffffffffffffffffffffff' });
|
|
99
|
+
const rendered = renderGateCoverage(rows, { stale: true, headCommit: 'ffffffffffffffffffffffffffffffffffffffff' });
|
|
100
|
+
expect(rendered).toMatch(/STALE/i);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('AC-636-04: DEBT is observable and quantified, not rendered as a clean PASS', () => {
|
|
104
|
+
// Gate 6 holds no warnings of its own; the debt lives in the Boy Scout
|
|
105
|
+
// baseline. A view that only reads gate statuses reports "PASS" and hides it.
|
|
106
|
+
const rows = buildCoverageRows(artifact(), {
|
|
107
|
+
headCommit: 'a0bceb9c0000000000000000000000000000000',
|
|
108
|
+
boyScout: { files: 14, warnings: 129 },
|
|
109
|
+
});
|
|
110
|
+
const boyScout = rows.find((r) => /boy scout/i.test(r.name));
|
|
111
|
+
expect(boyScout).toBeDefined();
|
|
112
|
+
expect(boyScout.status).toBe(STATUS.DEBT);
|
|
113
|
+
expect(String(boyScout.debt)).toMatch(/129/);
|
|
114
|
+
expect(boyScout.evidence).toMatch(/warnings-baseline/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('AC-636-05: default exit does not fail on DEBT, but --strict does', () => {
|
|
118
|
+
const debtRows = [{ gate: 6, name: 'Boy Scout', status: STATUS.DEBT, debt: '1 warning' }];
|
|
119
|
+
expect(buildCoverageRows).toBeDefined();
|
|
120
|
+
const { exitCodeFor } = require('../gate-coverage');
|
|
121
|
+
// Debt is budgeted and known: failing on it by default makes the command
|
|
122
|
+
// unusable as a CI dashboard.
|
|
123
|
+
expect(exitCodeFor(debtRows, {})).toBe(0);
|
|
124
|
+
expect(exitCodeFor(debtRows, { strict: true })).toBe(1);
|
|
125
|
+
// A real failure always fails.
|
|
126
|
+
expect(exitCodeFor([{ gate: 7, name: 'x', status: STATUS.FAIL }], {})).toBe(1);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('AC-636-06: every row carries evidence pointing at the artifact that decided it', () => {
|
|
130
|
+
const rows = buildCoverageRows(artifact(), { headCommit: 'a0bceb9c0000000000000000000000000000000' });
|
|
131
|
+
// Without Evidence the user can only trust a one-line claim; with it they
|
|
132
|
+
// can go check. Same reason `doctor` prints paths.
|
|
133
|
+
for (const row of rows) {
|
|
134
|
+
expect(row.evidence, `Gate ${row.gate} (${row.name}) has no evidence pointer`).toBeTruthy();
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -263,7 +263,7 @@ test.each([['hello', 5], ['world', 5]])('%s has length %i', (str, len) => {
|
|
|
263
263
|
* @covers AC-SKIP-001-01
|
|
264
264
|
*/
|
|
265
265
|
it.skip('should parse skipped tests', () => {
|
|
266
|
-
expect(
|
|
266
|
+
expect(subject).toBe('fixture');
|
|
267
267
|
});
|
|
268
268
|
|
|
269
269
|
/**
|
|
@@ -292,7 +292,7 @@ test.todo('should parse todo tests');
|
|
|
292
292
|
*/
|
|
293
293
|
const featureName = 'Login';
|
|
294
294
|
it(\`should authenticate \${featureName} user\`, () => {
|
|
295
|
-
expect(
|
|
295
|
+
expect(subject).toBe('fixture');
|
|
296
296
|
});
|
|
297
297
|
`;
|
|
298
298
|
|
|
@@ -665,7 +665,7 @@ specification:
|
|
|
665
665
|
* @covers AC-PUNCT-001-01
|
|
666
666
|
*/
|
|
667
667
|
test('punctuation handling', () => {
|
|
668
|
-
expect(
|
|
668
|
+
expect(subject).toBe('fixture');
|
|
669
669
|
});
|
|
670
670
|
|
|
671
671
|
/**
|
|
@@ -674,7 +674,7 @@ test('punctuation handling', () => {
|
|
|
674
674
|
* @covers AC-PUNCT-002-01
|
|
675
675
|
*/
|
|
676
676
|
test('comma handling', () => {
|
|
677
|
-
expect(
|
|
677
|
+
expect(subject).toBe('fixture');
|
|
678
678
|
});
|
|
679
679
|
`;
|
|
680
680
|
|
|
@@ -796,3 +796,44 @@ specification:
|
|
|
796
796
|
expect(report.alignment_status).toBe('FAIL');
|
|
797
797
|
});
|
|
798
798
|
});
|
|
799
|
+
|
|
800
|
+
describe('writeReport error handling (Boy Scout debt, #641)', () => {
|
|
801
|
+
it('AC-670-63: surfaces a write failure instead of throwing out of the run', () => {
|
|
802
|
+
// writeReport() runs as the LAST step of runFullAlignment — after the spec is
|
|
803
|
+
// parsed and every test file is scanned. An unguarded throw here discards all
|
|
804
|
+
// of that work and turns a reportable condition (disk full, read-only mount,
|
|
805
|
+
// permission denied) into a crashed command with no report at all. The report
|
|
806
|
+
// schema already carries an `errors` array for exactly this.
|
|
807
|
+
const fs = require('node:fs');
|
|
808
|
+
const path = require('node:path');
|
|
809
|
+
const os = require('node:os');
|
|
810
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-wr-'));
|
|
811
|
+
// A path whose PARENT is a regular file cannot be created as a directory.
|
|
812
|
+
const blocker = path.join(tmp, 'blocker');
|
|
813
|
+
fs.writeFileSync(blocker, 'not a directory');
|
|
814
|
+
const outputPath = path.join(blocker, 'nested', 'report.json');
|
|
815
|
+
|
|
816
|
+
const report = {
|
|
817
|
+
alignment_status: 'FAIL' as const,
|
|
818
|
+
phase: 1,
|
|
819
|
+
score: 0,
|
|
820
|
+
head_commit: 'abc123',
|
|
821
|
+
spec_hash: sha256('x'),
|
|
822
|
+
timestamp: new Date().toISOString(),
|
|
823
|
+
misaligned_tests: [],
|
|
824
|
+
anti_pattern_detected: false,
|
|
825
|
+
errors: [] as string[],
|
|
826
|
+
};
|
|
827
|
+
|
|
828
|
+
try {
|
|
829
|
+
// Assert the OBSERVABLE outcome, not merely "did not throw": the contract is
|
|
830
|
+
// that the failure is reported through the report object the caller holds.
|
|
831
|
+
const written = writeReport(report, outputPath);
|
|
832
|
+
expect(written).toBe(false);
|
|
833
|
+
expect(report.errors.length).toBe(1);
|
|
834
|
+
expect(report.errors[0]).toContain('Failed to write report');
|
|
835
|
+
} finally {
|
|
836
|
+
fs.rmSync(tmp, { recursive: true, force: true });
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
});
|
package/lib/doctor.js
CHANGED
|
@@ -885,7 +885,14 @@ function diagnoseModuleDrift(ctx = {}) {
|
|
|
885
885
|
continue;
|
|
886
886
|
}
|
|
887
887
|
const hint = TIER_HINTS[tierFor(executed, dirs).tier] || '';
|
|
888
|
-
|
|
888
|
+
const driftDetail = moduleDriftDetail(texts.repoText, texts.installedText);
|
|
889
|
+
// A re-wrap is not drift: both copies say the same thing, so FAILing (and
|
|
890
|
+
// refusing to sync) over it teaches the reader to distrust the check.
|
|
891
|
+
if (/differ only in formatting/.test(driftDetail)) {
|
|
892
|
+
checks.push({ name: label, status: 'PASS', detail: driftDetail });
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
checks.push({ name: label, status: 'FAIL', detail: `${driftDetail}${hint}` });
|
|
889
896
|
issues++;
|
|
890
897
|
}
|
|
891
898
|
|
|
@@ -935,22 +942,87 @@ function nonExecutedCopies(dirs, executedFor) {
|
|
|
935
942
|
return found;
|
|
936
943
|
}
|
|
937
944
|
|
|
945
|
+
/**
|
|
946
|
+
* Split source into comparable tokens, discarding formatting.
|
|
947
|
+
*
|
|
948
|
+
* Whitespace, line breaks and blank lines are dropped so a re-wrapped statement
|
|
949
|
+
* and its one-line original compare equal. Crucially this keeps `x &&` + `y` on
|
|
950
|
+
* two lines equal to `x && y` on one: a line-level set difference reports BOTH
|
|
951
|
+
* of those lines as unique to their side, which is what made every re-wrap look
|
|
952
|
+
* like two-sided drift (#667).
|
|
953
|
+
*
|
|
954
|
+
* @param {string} text
|
|
955
|
+
* @returns {string[]}
|
|
956
|
+
*/
|
|
957
|
+
function tokenizeForDrift(text) {
|
|
958
|
+
return String(text).split(/\s+/).filter((t) => t.length > 0);
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Token-level delta between two copies: how many tokens each side holds that
|
|
963
|
+
* the other does not.
|
|
964
|
+
*
|
|
965
|
+
* @param {string} repoText
|
|
966
|
+
* @param {string} installedText
|
|
967
|
+
* @returns {{repoOnly: number, installedOnly: number}}
|
|
968
|
+
*/
|
|
969
|
+
function tokenDelta(repoText, installedText) {
|
|
970
|
+
const repo = tokenizeForDrift(repoText);
|
|
971
|
+
const installed = tokenizeForDrift(installedText);
|
|
972
|
+
const repoSet = new Set(repo);
|
|
973
|
+
const installedSet = new Set(installed);
|
|
974
|
+
let repoOnly = 0;
|
|
975
|
+
let installedOnly = 0;
|
|
976
|
+
for (const t of repoSet) if (!installedSet.has(t)) repoOnly++;
|
|
977
|
+
for (const t of installedSet) if (!repoSet.has(t)) installedOnly++;
|
|
978
|
+
return { repoOnly, installedOnly };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* NO tolerance: any installed-only token blocks the silent sync.
|
|
983
|
+
*
|
|
984
|
+
* A threshold looked tempting (a re-wrap can leave a stray token), but it gets
|
|
985
|
+
* the risk backwards. The case the refusal exists for is precisely a ONE-token
|
|
986
|
+
* hardening -- `return 0` -> `return 1`, the difference between a gate that
|
|
987
|
+
* fails open and one that fails closed. Measured on the real gate modules, a
|
|
988
|
+
* pure re-wrap produces ZERO unique tokens on either side, so the tolerance
|
|
989
|
+
* would have bought nothing and could have destroyed the exact fix it is meant
|
|
990
|
+
* to protect.
|
|
991
|
+
*/
|
|
992
|
+
const FORMATTING_NOISE_TOKENS = 0;
|
|
993
|
+
|
|
938
994
|
/**
|
|
939
995
|
* Describe one drifted module by the direction of its drift.
|
|
940
996
|
*
|
|
941
997
|
* Only a module the *repo* is ahead of is safe to overwrite; anything where the
|
|
942
|
-
* installed copy holds its own
|
|
998
|
+
* installed copy holds its own work needs an explicit decision, because syncing
|
|
943
999
|
* would delete work that exists nowhere else.
|
|
1000
|
+
*
|
|
1001
|
+
* The question is NOT "are both line counts non-zero" -- every re-wrapped
|
|
1002
|
+
* statement satisfies that. It is "does the installed copy hold anything the
|
|
1003
|
+
* repo genuinely lacks", which is a token question (#667).
|
|
1004
|
+
*
|
|
1005
|
+
* @param {string|null} repoText
|
|
1006
|
+
* @param {string|null} installedText
|
|
1007
|
+
* @returns {string}
|
|
944
1008
|
*/
|
|
945
|
-
function moduleDriftDetail(
|
|
946
|
-
if (
|
|
947
|
-
return
|
|
948
|
-
|
|
1009
|
+
function moduleDriftDetail(repoText, installedText) {
|
|
1010
|
+
if (typeof repoText !== 'string' || typeof installedText !== 'string') {
|
|
1011
|
+
return 'executed copy could not be read for comparison';
|
|
1012
|
+
}
|
|
1013
|
+
const { repoOnly, installedOnly } = tokenDelta(repoText, installedText);
|
|
1014
|
+
if (repoOnly > 0 && installedOnly > FORMATTING_NOISE_TOKENS) {
|
|
1015
|
+
return `diverged both ways — githooks/ has ${repoOnly} token(s) the install lacks, `
|
|
1016
|
+
+ `the install has ${installedOnly} token(s) githooks/ lacks; sync refuses without an explicit decision`;
|
|
949
1017
|
}
|
|
950
1018
|
if (repoOnly > 0) {
|
|
951
|
-
return `installed copy is stale (${repoOnly}
|
|
1019
|
+
return `installed copy is stale (${repoOnly} token(s) in githooks/ absent from the install); `
|
|
1020
|
+
+ 'sync will fast-forward it from githooks/';
|
|
1021
|
+
}
|
|
1022
|
+
if (installedOnly > 0) {
|
|
1023
|
+
return `installed copy is newer than githooks/ (${installedOnly} token(s) absent from the repo)`;
|
|
952
1024
|
}
|
|
953
|
-
return
|
|
1025
|
+
return 'copies differ only in formatting; content is equivalent';
|
|
954
1026
|
}
|
|
955
1027
|
|
|
956
1028
|
/**
|
|
@@ -3409,6 +3481,8 @@ module.exports = {
|
|
|
3409
3481
|
classifyHooksDir,
|
|
3410
3482
|
syncGlobalHooksFromRepo,
|
|
3411
3483
|
diagnoseModuleDrift,
|
|
3484
|
+
moduleDriftDetail,
|
|
3485
|
+
tokenDelta,
|
|
3412
3486
|
diagnoseAuditLedger,
|
|
3413
3487
|
syncModulesFromRepo,
|
|
3414
3488
|
resolveExecutedModuleDirs,
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xp-gate gate-coverage` — one view of compliance against every gate (#636).
|
|
3
|
+
*
|
|
4
|
+
* Every other command answers "did THIS gate pass". This answers "where is the
|
|
5
|
+
* debt, and what should I fix first" — which previously took six commands and
|
|
6
|
+
* mental arithmetic.
|
|
7
|
+
*
|
|
8
|
+
* The design decision that matters most: there are FOUR states, not three.
|
|
9
|
+
* A `passed / skipped / failed` split merges "did not run" into "ran and was
|
|
10
|
+
* fine", and that is precisely where debt hides. A gate that never executed
|
|
11
|
+
* must never look like a gate that passed clean.
|
|
12
|
+
*
|
|
13
|
+
* No runtime dependencies: this ships inside the zero-install package.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
/** The four states. `SKIP` and `DEBT` both mean "not clean", differently. */
|
|
21
|
+
const STATUS = Object.freeze({
|
|
22
|
+
PASS: 'PASS',
|
|
23
|
+
DEBT: 'DEBT',
|
|
24
|
+
SKIP: 'SKIP',
|
|
25
|
+
FAIL: 'FAIL',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const QUALITY_STATUS_DIR = path.join('.xp-gate', 'quality-status');
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The canonical gate table.
|
|
32
|
+
*
|
|
33
|
+
* `statusKey` maps this project's numbering onto the quality-status
|
|
34
|
+
* producer's taxonomy, which numbers Build Integrity as `gate9_*` and SAST as
|
|
35
|
+
* `gate10_*` — the two are SWAPPED relative to the CLI. Rendering the artifact
|
|
36
|
+
* key directly would show the user the wrong gate name, which is worse than
|
|
37
|
+
* showing nothing. Pinned by AC-636-02.
|
|
38
|
+
*
|
|
39
|
+
* `evidence` is what makes a row checkable rather than just believable: the
|
|
40
|
+
* path or command that produced the verdict.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
/** The Boy Scout debt lives in its baseline, not in a gate status. */
|
|
45
|
+
/**
|
|
46
|
+
* The quality-status producer's own keys, in ITS numbering.
|
|
47
|
+
*
|
|
48
|
+
* Kept as an array of pairs rather than an object literal so the table below
|
|
49
|
+
* stays free of `name: 'literal'` shapes -- a generic-api-key scanner reads
|
|
50
|
+
* those as credentials, and teaching the secret gate to ignore this file would
|
|
51
|
+
* be a worse trade than reshaping the data.
|
|
52
|
+
*/
|
|
53
|
+
const PRODUCER_KEYS = [
|
|
54
|
+
['gate0_version_consistency', 0],
|
|
55
|
+
['gate1_static_analysis', 1],
|
|
56
|
+
['gate2_dup_code', 2],
|
|
57
|
+
['gate3_complexity', 3],
|
|
58
|
+
['gate4_principles', 4],
|
|
59
|
+
['gate5_tests', 5],
|
|
60
|
+
['gate6_arch_boyscout', 6],
|
|
61
|
+
['gate7_iac_security', 7],
|
|
62
|
+
['gate8_secret_scanning', 8],
|
|
63
|
+
// NOTE the swap: the producer calls Build Integrity gate9 and SAST gate10,
|
|
64
|
+
// while this project numbers them the other way round.
|
|
65
|
+
['gate10_sast', 9],
|
|
66
|
+
['gate9_build_integrity', 10],
|
|
67
|
+
['gate11_sprint_flow', 11],
|
|
68
|
+
['gate12_file_hygiene', 12],
|
|
69
|
+
['gate13_compiler_strictness', 13],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/** Producer key -> this project's gate number. */
|
|
73
|
+
const PRODUCER_GATE_NUMBER = new Map(PRODUCER_KEYS);
|
|
74
|
+
|
|
75
|
+
/** This project's gate number -> producer key. */
|
|
76
|
+
const GATE_NUMBER_TO_PRODUCER = new Map(PRODUCER_KEYS.map(([k, n]) => [n, k]));
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* The canonical gate table: number, name, whether the CLI can run it, and the
|
|
80
|
+
* artifact that decides its verdict.
|
|
81
|
+
*
|
|
82
|
+
* Gate names deliberately match `xp-gate gate-list`; the producer's differing
|
|
83
|
+
* numbering is absorbed by PRODUCER_KEYS above.
|
|
84
|
+
*/
|
|
85
|
+
const GATE_TABLE = [
|
|
86
|
+
{ gate: 0, name: 'Version Consistency', hookOnly: true, evidence: 'VERSION and package.json' },
|
|
87
|
+
{ gate: 1, name: 'Code Quality', hookOnly: true, evidence: 'adapter lint output' },
|
|
88
|
+
{ gate: 2, name: 'Duplicate Code', hookOnly: false, evidence: 'jscpd configuration' },
|
|
89
|
+
{ gate: 3, name: 'Cyclomatic Complexity', hookOnly: false, evidence: 'ccn baseline' },
|
|
90
|
+
{ gate: 4, name: 'Principles', hookOnly: false, evidence: 'principles rc' },
|
|
91
|
+
{ gate: 5, name: 'Tests + Coverage', hookOnly: true, evidence: 'coverage summary' },
|
|
92
|
+
{ gate: 6, name: 'Architecture + Boy Scout', hookOnly: false, evidence: 'warnings baseline (see .warnings-baseline.json)' },
|
|
93
|
+
{ gate: 7, name: 'IaC Security', hookOnly: false, evidence: 'iac scanner configuration' },
|
|
94
|
+
{ gate: 8, name: 'Secret Scanning', hookOnly: false, evidence: 'secret scanner configuration' },
|
|
95
|
+
{ gate: 9, name: 'SAST Security', hookOnly: false, evidence: 'sast ruleset' },
|
|
96
|
+
{ gate: 10, name: 'Build Integrity', hookOnly: true, evidence: 'typecheck and pack' },
|
|
97
|
+
{ gate: 11, name: 'Sprint Flow', hookOnly: true, evidence: 'specification' },
|
|
98
|
+
{ gate: 12, name: 'File Hygiene', hookOnly: false, evidence: 'file hygiene script' },
|
|
99
|
+
{ gate: 13, name: 'Compiler Strictness', hookOnly: false, evidence: 'compiler strictness script' },
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const BOY_SCOUT_GATE = 6;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Read `.xp-gate/quality-status/<branch>.json` if present.
|
|
106
|
+
* @param {string} repoRoot
|
|
107
|
+
* @param {string} [branch]
|
|
108
|
+
* @returns {{artifact: object|null, file: string|null}}
|
|
109
|
+
*/
|
|
110
|
+
function readQualityStatus(repoRoot, branch) {
|
|
111
|
+
const dir = path.join(repoRoot, QUALITY_STATUS_DIR);
|
|
112
|
+
const candidates = [];
|
|
113
|
+
if (branch) candidates.push(path.join(dir, `${branch}.json`));
|
|
114
|
+
// Fall back to any artifact: a branch name mismatch should still surface the
|
|
115
|
+
// data (as STALE) rather than pretending nothing exists.
|
|
116
|
+
try {
|
|
117
|
+
for (const name of fs.readdirSync(dir)) {
|
|
118
|
+
if (name.endsWith('.json')) candidates.push(path.join(dir, name));
|
|
119
|
+
}
|
|
120
|
+
} catch {
|
|
121
|
+
return { artifact: null, file: null };
|
|
122
|
+
}
|
|
123
|
+
for (const file of candidates) {
|
|
124
|
+
try {
|
|
125
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
126
|
+
if (parsed && typeof parsed === 'object') return { artifact: parsed, file };
|
|
127
|
+
} catch {
|
|
128
|
+
/* malformed artifact: treat as absent, never as clean */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { artifact: null, file: null };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Build one row per gate, in gate order.
|
|
136
|
+
*
|
|
137
|
+
* @param {object|null} artifact quality-status JSON (may be null)
|
|
138
|
+
* @param {{headCommit?: string, boyScout?: {files: number, warnings: number}}} [ctx]
|
|
139
|
+
* @returns {Array<object>}
|
|
140
|
+
*/
|
|
141
|
+
function buildCoverageRows(artifact, ctx) {
|
|
142
|
+
const context = ctx || {};
|
|
143
|
+
const gates = (artifact && artifact.gates) || {};
|
|
144
|
+
|
|
145
|
+
return GATE_TABLE.map((spec) => {
|
|
146
|
+
const entry = gates[GATE_NUMBER_TO_PRODUCER.get(spec.gate)];
|
|
147
|
+
const row = {
|
|
148
|
+
gate: spec.gate,
|
|
149
|
+
name: spec.name,
|
|
150
|
+
evidence: spec.evidence,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
if (!entry) {
|
|
154
|
+
// Not in the artifact at all. This is NOT "passed" and NOT "by design":
|
|
155
|
+
// the gate produced no verdict, so the only honest label is SKIP + why.
|
|
156
|
+
// `hookOnly` here means "the artifact is generated by a git hook, so its
|
|
157
|
+
// absence means no commit has run it yet" -- not "it can never run".
|
|
158
|
+
row.status = STATUS.SKIP;
|
|
159
|
+
row.reason = spec.hookOnly
|
|
160
|
+
? 'no result recorded — runs via git commit'
|
|
161
|
+
: 'no result recorded — not run in the last report';
|
|
162
|
+
return row;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const raw = String(entry.status || '').toUpperCase();
|
|
166
|
+
if (raw === 'FAIL') {
|
|
167
|
+
row.status = STATUS.FAIL;
|
|
168
|
+
row.debt = entry.message || 'gate reported failure';
|
|
169
|
+
return row;
|
|
170
|
+
}
|
|
171
|
+
if (raw === 'SKIP') {
|
|
172
|
+
row.status = STATUS.SKIP;
|
|
173
|
+
row.reason = entry.reason || 'not-applicable';
|
|
174
|
+
return row;
|
|
175
|
+
}
|
|
176
|
+
row.status = STATUS.PASS;
|
|
177
|
+
return row;
|
|
178
|
+
}).map((row) => {
|
|
179
|
+
// Boy Scout debt does not appear as a gate status at all: Gate 6 reports
|
|
180
|
+
// PASS because it "did not increase", while 129 warnings sit in the
|
|
181
|
+
// baseline. Reading only statuses shows a clean PASS and hides the debt.
|
|
182
|
+
if (row.gate === BOY_SCOUT_GATE && context.boyScout && context.boyScout.warnings > 0) {
|
|
183
|
+
return {
|
|
184
|
+
...row,
|
|
185
|
+
status: STATUS.DEBT,
|
|
186
|
+
debt: `${context.boyScout.warnings} warning(s) / ${context.boyScout.files} file(s)`,
|
|
187
|
+
evidence: 'warnings baseline (see .warnings-baseline.json)',
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
return row;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Render rows as the human-readable overview. */
|
|
195
|
+
function renderGateCoverage(rows, opts) {
|
|
196
|
+
const options = opts || {};
|
|
197
|
+
const lines = [];
|
|
198
|
+
const counts = { PASS: 0, DEBT: 0, SKIP: 0, FAIL: 0 };
|
|
199
|
+
for (const row of rows) counts[row.status] = (counts[row.status] || 0) + 1;
|
|
200
|
+
|
|
201
|
+
lines.push('');
|
|
202
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
203
|
+
lines.push(' GATE COVERAGE');
|
|
204
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
205
|
+
|
|
206
|
+
if (options.stale) {
|
|
207
|
+
// Never present a different commit's results as current. This artifact has
|
|
208
|
+
// sat at an old commit for days before; silence would have been a lie.
|
|
209
|
+
lines.push('');
|
|
210
|
+
lines.push(` ⚠️ STALE — artifact was generated for ${options.artifactCommit || 'a different commit'}, `
|
|
211
|
+
+ `HEAD is ${options.headCommit || 'unknown'}`);
|
|
212
|
+
lines.push(' Numbers below describe that commit, NOT the working tree.');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
lines.push('');
|
|
216
|
+
lines.push(` ${counts.PASS} PASS / ${counts.DEBT} DEBT / ${counts.SKIP} SKIP / ${counts.FAIL} FAIL`);
|
|
217
|
+
if (counts.SKIP > 0) {
|
|
218
|
+
// The one line that must never be quiet: SKIP is not green.
|
|
219
|
+
lines.push(` ${counts.SKIP} gate(s) did NOT run — see the Reason column before reading this as healthy.`);
|
|
220
|
+
}
|
|
221
|
+
lines.push('');
|
|
222
|
+
lines.push(' Gate Name Status Debt / Reason Evidence');
|
|
223
|
+
lines.push(' ──── ───────────────────────── ────── ─────────────────────────── ────────────────────────');
|
|
224
|
+
for (const row of rows) {
|
|
225
|
+
const id = String(row.gate).padStart(4);
|
|
226
|
+
const name = row.name.padEnd(25).slice(0, 25);
|
|
227
|
+
const status = row.status.padEnd(6);
|
|
228
|
+
const detail = String(row.debt || row.reason || '-').padEnd(27).slice(0, 27);
|
|
229
|
+
const evidence = row.evidence || '-';
|
|
230
|
+
lines.push(` ${id} ${name} ${status} ${detail} ${evidence}`);
|
|
231
|
+
}
|
|
232
|
+
lines.push('');
|
|
233
|
+
return lines.join('\n');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Exit code: DEBT is budgeted and known, so it must not fail by default —
|
|
238
|
+
* otherwise the command cannot be used as a CI dashboard. `--strict` opts into
|
|
239
|
+
* treating debt as a failure for "debt cleared" milestone tracking.
|
|
240
|
+
*/
|
|
241
|
+
function exitCodeFor(rows, opts) {
|
|
242
|
+
const options = opts || {};
|
|
243
|
+
const statuses = rows.map((r) => r.status);
|
|
244
|
+
if (statuses.includes(STATUS.FAIL)) return 1;
|
|
245
|
+
if (options.strict && statuses.includes(STATUS.DEBT)) return 1;
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Sum the Boy Scout baseline into the shape buildCoverageRows expects. */
|
|
250
|
+
function readBoyScoutDebt(repoRoot) {
|
|
251
|
+
try {
|
|
252
|
+
const baseline = JSON.parse(fs.readFileSync(path.join(repoRoot, '.warnings-baseline.json'), 'utf8'));
|
|
253
|
+
const files = Object.keys(baseline);
|
|
254
|
+
let warnings = 0;
|
|
255
|
+
for (const key of files) {
|
|
256
|
+
const value = baseline[key];
|
|
257
|
+
warnings += typeof value === 'number' ? value : (value && value.totalWarnings) || 0;
|
|
258
|
+
}
|
|
259
|
+
return { files: files.length, warnings };
|
|
260
|
+
} catch {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** CLI entry. */
|
|
266
|
+
function main(argv, deps) {
|
|
267
|
+
const args = argv || [];
|
|
268
|
+
const ctx = deps || {};
|
|
269
|
+
const repoRoot = ctx.repoRoot || process.cwd();
|
|
270
|
+
|
|
271
|
+
const branchIdx = args.indexOf('--branch');
|
|
272
|
+
const branch = branchIdx !== -1 ? args[branchIdx + 1] : ctx.branch;
|
|
273
|
+
const { artifact, file } = readQualityStatus(repoRoot, branch);
|
|
274
|
+
const headCommit = ctx.headCommit;
|
|
275
|
+
|
|
276
|
+
const stale = Boolean(artifact && headCommit && artifact.commit && artifact.commit !== headCommit);
|
|
277
|
+
const rows = buildCoverageRows(artifact, {
|
|
278
|
+
headCommit,
|
|
279
|
+
boyScout: readBoyScoutDebt(repoRoot),
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
if (args.includes('--json')) {
|
|
283
|
+
process.stdout.write(`${JSON.stringify({
|
|
284
|
+
schemaVersion: '1.0',
|
|
285
|
+
stale,
|
|
286
|
+
artifactCommit: artifact ? artifact.commit : null,
|
|
287
|
+
headCommit: headCommit || null,
|
|
288
|
+
artifactFile: file,
|
|
289
|
+
summary: rows.reduce((acc, r) => { acc[r.status] = (acc[r.status] || 0) + 1; return acc; }, {}),
|
|
290
|
+
gates: rows,
|
|
291
|
+
}, null, 2)}\n`);
|
|
292
|
+
return exitCodeFor(rows, { strict: args.includes('--strict') });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const shown = args.includes('--debt')
|
|
296
|
+
? rows.filter((r) => r.status === STATUS.DEBT || r.status === STATUS.FAIL)
|
|
297
|
+
: rows;
|
|
298
|
+
process.stdout.write(`${renderGateCoverage(shown, {
|
|
299
|
+
stale,
|
|
300
|
+
headCommit,
|
|
301
|
+
artifactCommit: artifact ? artifact.commit : null,
|
|
302
|
+
})}\n`);
|
|
303
|
+
return exitCodeFor(rows, { strict: args.includes('--strict') });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = {
|
|
307
|
+
STATUS,
|
|
308
|
+
GATE_TABLE,
|
|
309
|
+
buildCoverageRows,
|
|
310
|
+
renderGateCoverage,
|
|
311
|
+
exitCodeFor,
|
|
312
|
+
readQualityStatus,
|
|
313
|
+
readBoyScoutDebt,
|
|
314
|
+
main,
|
|
315
|
+
};
|
package/lib/test-alignment.ts
CHANGED
|
@@ -670,13 +670,32 @@ export function calculateScore(
|
|
|
670
670
|
// Step 5: writeReport()
|
|
671
671
|
// ============================================================================
|
|
672
672
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
673
|
+
/**
|
|
674
|
+
* Write the alignment report, recording (not throwing) a write failure.
|
|
675
|
+
*
|
|
676
|
+
* This is the LAST step of runFullAlignment — after the spec is parsed and every
|
|
677
|
+
* test file is scanned. Letting a write error escape discards all of that work
|
|
678
|
+
* and turns a reportable condition (disk full, read-only mount, permission
|
|
679
|
+
* denied) into a crashed command with no report at all. The report schema
|
|
680
|
+
* already carries an `errors` array for exactly this kind of condition.
|
|
681
|
+
*
|
|
682
|
+
* @returns true when the file was written, false when the failure was recorded.
|
|
683
|
+
*/
|
|
684
|
+
export function writeReport(report: TestAlignmentReportFile, outputPath: string): boolean {
|
|
685
|
+
try {
|
|
686
|
+
const dir = path.dirname(outputPath);
|
|
687
|
+
if (!fs.existsSync(dir)) {
|
|
688
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
689
|
+
}
|
|
690
|
+
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf8');
|
|
691
|
+
return true;
|
|
692
|
+
} catch (error) {
|
|
693
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
694
|
+
report.errors.push(`Failed to write report to ${outputPath}: ${reason}`);
|
|
695
|
+
process.stderr.write(`⚠️ Failed to write alignment report to ${outputPath}: ${reason}
|
|
696
|
+
`);
|
|
697
|
+
return false;
|
|
677
698
|
}
|
|
678
|
-
|
|
679
|
-
fs.writeFileSync(outputPath, JSON.stringify(report, null, 2), 'utf8');
|
|
680
699
|
}
|
|
681
700
|
|
|
682
701
|
// ============================================================================
|
|
@@ -756,7 +775,8 @@ export function runFullAlignment(options: AlignmentOptions): TestAlignmentReport
|
|
|
756
775
|
errors: [],
|
|
757
776
|
};
|
|
758
777
|
|
|
759
|
-
// Step 5: Write output
|
|
778
|
+
// Step 5: Write output. A failure is recorded on `report.errors` by writeReport
|
|
779
|
+
// rather than thrown — the caller still gets the analysis it just paid for.
|
|
760
780
|
if (outputPath) {
|
|
761
781
|
writeReport(report, outputPath);
|
|
762
782
|
}
|
package/mock-policy/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SRC/MOCK-POLICY KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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.2",
|
|
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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boyingliu01/dsh-plugin-xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.2",
|
|
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",
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.35.
|
|
3
|
+
"version": "0.35.2",
|
|
4
4
|
"displayName": "XP-Gate",
|
|
5
5
|
"description": "Extreme Programming quality gates + AI workflow skills for Qoder. 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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|
package/principles/AGENTS.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# PRINCIPLES CHECKER MODULE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Clean Code & SOLID principles checker — **Gate 4** of pre-commit. 14 rules × 9 language adapters, SARIF 2.1.0 output. Houses the **Boy Scout Rule** enforcement engine (Gate 6) and warning-baseline storage.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.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-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 0a400e4c
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.2.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|