@boyingliu01/xp-gate 0.35.6 → 0.35.8
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 +60 -0
- package/hooks/lib/assertion-quality.sh +47 -0
- package/hooks/pre-commit +131 -21
- package/lib/__tests__/assertion-debt.test.js +99 -0
- package/lib/__tests__/gate-rules.test.js +58 -0
- package/lib/__tests__/mirror-manifest.test.js +64 -0
- package/lib/assertion-debt.js +173 -0
- package/lib/gate-rules.js +149 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/mirror-manifest.js +112 -0
- 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/references/phase-3-build.md +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/references/phase-3-build.md +14 -1
- 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/sprint-flow/references/phase-3-build.md +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/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/sprint-flow/references/phase-3-build.md +14 -1
- package/skills/test-specification-alignment/AGENTS.md +2 -2
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xp-gate audit assertions` — make pre-existing assertion debt visible (#668).
|
|
3
|
+
*
|
|
4
|
+
* Gate 5d deliberately checks only the files in the diff: a full-repo scan on
|
|
5
|
+
* every commit is too slow. That has an expensive side effect. Pre-existing debt
|
|
6
|
+
* blocks unrelated changes — the author who fixed `writeReport()`'s error handling
|
|
7
|
+
* was stopped by four long-standing `expect(true).toBe(true)` lines in a file they
|
|
8
|
+
* had merely touched — and the block reads as "this change introduced a problem".
|
|
9
|
+
*
|
|
10
|
+
* The worst consequence is not that one false attribution. It is what it does to
|
|
11
|
+
* behaviour: noise in the attribution makes people reach for
|
|
12
|
+
* `@assertion-justified` instead of fixing the test. A gate that teaches people to
|
|
13
|
+
* annotate their way past it is worse than no gate. Hence a batch work queue.
|
|
14
|
+
*
|
|
15
|
+
* Scope: only the two shapes Gate 5d BLOCKS (tautological, empty catch). Weak-only
|
|
16
|
+
* assertions and mock echoes WARN there because each can be legitimate; putting them
|
|
17
|
+
* in a "debt to clear" list would invent work.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
const fs = require('node:fs');
|
|
22
|
+
const path = require('node:path');
|
|
23
|
+
|
|
24
|
+
/** Directories that never contain hand-written assertions worth auditing. */
|
|
25
|
+
const SKIP_DIRS = new Set([
|
|
26
|
+
'node_modules',
|
|
27
|
+
'.git',
|
|
28
|
+
'dist',
|
|
29
|
+
'build',
|
|
30
|
+
'coverage',
|
|
31
|
+
'.stryker-tmp',
|
|
32
|
+
'.agent-tmp',
|
|
33
|
+
// Build output mirrored from src/ — listing it again would double-count debt and
|
|
34
|
+
// send a batch fixer to a file that regenerates.
|
|
35
|
+
'.xp-gate',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
/** Test-file patterns across the ecosystems XP-Gate supports. */
|
|
39
|
+
function isTestFile(name) {
|
|
40
|
+
return (
|
|
41
|
+
/\.(test|spec)\.[cm]?[jt]sx?$/.test(name) ||
|
|
42
|
+
/(^|\/)(__tests__)\//.test(name) ||
|
|
43
|
+
/\.test\.bats$/.test(name) ||
|
|
44
|
+
/^test_.*\.py$/.test(name) ||
|
|
45
|
+
/_test\.(go|rs|py)$/.test(name)
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Tautological assertions: the assertion cannot fail.
|
|
51
|
+
*
|
|
52
|
+
* Mirrors `classify_tautological_assertions` in githooks/lib/assertion-quality.sh.
|
|
53
|
+
* The two implementations must agree, because a debt list that disagrees with the
|
|
54
|
+
* gate it exists to serve sends people to fix lines that are not blocking and miss
|
|
55
|
+
* the ones that are.
|
|
56
|
+
*/
|
|
57
|
+
const TAUTOLOGY_PATTERNS = [
|
|
58
|
+
// (a) identical literal on both sides.
|
|
59
|
+
{
|
|
60
|
+
re: /expect\s*\(\s*(true|false|null|undefined|-?\d+|"[^"]*"|'[^']*')\s*\)\s*\.\s*to(?:Be|Equal|StrictEqual)\s*\(\s*\1\s*\)/,
|
|
61
|
+
},
|
|
62
|
+
// (b) a literal tested against the truthiness it already has.
|
|
63
|
+
{ re: /expect\s*\(\s*(?:true|1|-1|"[^"]+"|'[^']+')\s*\)\s*\.\s*toBeTruthy\s*\(/ },
|
|
64
|
+
{ re: /expect\s*\(\s*(?:false|0|null|undefined|""|'')\s*\)\s*\.\s*toBeFalsy\s*\(/ },
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
/** An empty catch body: the failure it guards is discarded. */
|
|
68
|
+
const EMPTY_CATCH_RE = /catch\s*(?:\([^)]*\))?\s*\{\s*\}/;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Is this line a fixture rather than a real assertion?
|
|
72
|
+
*
|
|
73
|
+
* A `.bats` test that exercises the Gate 5d classifier necessarily contains
|
|
74
|
+
* `'expect(true).toBe(true);'` as a quoted INPUT. Reporting it as debt would send a
|
|
75
|
+
* batch fixer to delete the very strings that pin the classifier — turning the
|
|
76
|
+
* "fix debt" instruction into an instruction to remove test coverage.
|
|
77
|
+
*
|
|
78
|
+
* The distinguishing feature is quoting: in a fixture the shape is wrapped in
|
|
79
|
+
* quotes and usually sits inside a list/assignment, where a real assertion is bare
|
|
80
|
+
* executable code.
|
|
81
|
+
*/
|
|
82
|
+
function isFixtureLine(trimmed) {
|
|
83
|
+
// A leading quote before the assertion shape: `'expect(...)...'` or "expect(...)".
|
|
84
|
+
if (/^['"`]/.test(trimmed)) return true;
|
|
85
|
+
// Assertion shape appearing inside a quoted string on the line.
|
|
86
|
+
if (/['"][^'"`]*expect\s*\(/.test(trimmed)) return true;
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Classify one source line, or null when it is fine. */
|
|
91
|
+
function classifyLine(text) {
|
|
92
|
+
const trimmed = text.trim();
|
|
93
|
+
if (trimmed === '') return null;
|
|
94
|
+
// A commented-out assertion is not an assertion.
|
|
95
|
+
if (/^(\/\/|\*|\/\*|#)/.test(trimmed)) return null;
|
|
96
|
+
if (isFixtureLine(trimmed)) return null;
|
|
97
|
+
for (const { re } of TAUTOLOGY_PATTERNS) {
|
|
98
|
+
if (re.test(trimmed)) return 'tautological';
|
|
99
|
+
}
|
|
100
|
+
if (EMPTY_CATCH_RE.test(trimmed)) return 'swallowed-error';
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Find debt in one file's text. */
|
|
105
|
+
function scanSource(text, file) {
|
|
106
|
+
const findings = [];
|
|
107
|
+
const lines = text.split(/\r?\n/);
|
|
108
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
109
|
+
const kind = classifyLine(lines[i]);
|
|
110
|
+
if (kind) findings.push({ line: i + 1, kind, text: lines[i].trim() });
|
|
111
|
+
}
|
|
112
|
+
return findings.length > 0 ? [{ file, findings }] : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Walk the tree collecting test files, bounded to avoid node_modules. */
|
|
116
|
+
function listTestFiles(root, out = []) {
|
|
117
|
+
let entries;
|
|
118
|
+
try {
|
|
119
|
+
entries = fs.readdirSync(root, { withFileTypes: true });
|
|
120
|
+
} catch {
|
|
121
|
+
return out;
|
|
122
|
+
}
|
|
123
|
+
if (!Array.isArray(entries)) return out;
|
|
124
|
+
for (const entry of entries) {
|
|
125
|
+
if (entry.isDirectory()) {
|
|
126
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
127
|
+
listTestFiles(path.join(root, entry.name), out);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (isTestFile(entry.name)) out.push(path.join(root, entry.name));
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Scan for assertion debt.
|
|
137
|
+
*
|
|
138
|
+
* @param {{cwd?: string, source?: string, file?: string}} opts
|
|
139
|
+
* @returns {Array<{file: string, findings: Array<{line: number, kind: string, text: string}>}>}
|
|
140
|
+
*/
|
|
141
|
+
function scanForAssertionDebt(opts = {}) {
|
|
142
|
+
if (typeof opts.source === 'string') {
|
|
143
|
+
return scanSource(opts.source, opts.file || '<inline>');
|
|
144
|
+
}
|
|
145
|
+
const root = opts.cwd || process.cwd();
|
|
146
|
+
const results = [];
|
|
147
|
+
for (const abs of listTestFiles(root)) {
|
|
148
|
+
let text;
|
|
149
|
+
try {
|
|
150
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
151
|
+
} catch {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
155
|
+
results.push(...scanSource(text, rel));
|
|
156
|
+
}
|
|
157
|
+
return results.sort((a, b) => b.findings.length - a.findings.length || a.file.localeCompare(b.file));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Totals for a debt run: per-kind counts plus the file count to batch over. */
|
|
161
|
+
function summarizeDebt(results) {
|
|
162
|
+
const byKind = { tautological: 0, 'swallowed-error': 0 };
|
|
163
|
+
let total = 0;
|
|
164
|
+
for (const entry of results) {
|
|
165
|
+
for (const finding of entry.findings) {
|
|
166
|
+
byKind[finding.kind] = (byKind[finding.kind] || 0) + 1;
|
|
167
|
+
total += 1;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return { files: results.length, total, byKind };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { classifyLine, scanForAssertionDebt, summarizeDebt, isTestFile };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect the gate thresholds that actually block a commit, from the files that
|
|
3
|
+
* define them (#652).
|
|
4
|
+
*
|
|
5
|
+
* Why generated rather than hand-written: BUILD-phase docs used to say "there are
|
|
6
|
+
* gates 0-11" without saying what the numbers are. The numbers live in
|
|
7
|
+
* `.principlesrc`, `jscpd.conf.json`, `.ccn-baseline.json` and the hook itself. A
|
|
8
|
+
* hand-copied table is correct on the day it is written and wrong on the day
|
|
9
|
+
* someone tunes a threshold — and a self-check list that lies is worse than none,
|
|
10
|
+
* because it teaches the wrong rule and then contradicts it at commit time.
|
|
11
|
+
*
|
|
12
|
+
* Every rule therefore carries an `evidence` pointer, so "the limit is 50" can be
|
|
13
|
+
* checked or argued with instead of being indistinguishable from a guess.
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
/** Read and parse a JSON file, or null when absent/unreadable. */
|
|
21
|
+
function readJson(file) {
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalise severity spellings.
|
|
31
|
+
*
|
|
32
|
+
* The config files and the hook do not agree: `.principlesrc` says `warning`,
|
|
33
|
+
* Gate 3's boundary is `warn`, and both mean the same thing. Carrying two spellings
|
|
34
|
+
* into one list would make it look like two different severities exist.
|
|
35
|
+
*/
|
|
36
|
+
function normaliseSeverity(value) {
|
|
37
|
+
if (value === 'warning') return 'warn';
|
|
38
|
+
if (value === 'block' || value === 'warn' || value === 'info') return value;
|
|
39
|
+
return 'warn';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Flatten `.principlesrc` rules into entries with source + evidence. */
|
|
43
|
+
function principlesRules(cwd) {
|
|
44
|
+
const file = path.join(cwd, '.principlesrc');
|
|
45
|
+
const cfg = readJson(file);
|
|
46
|
+
if (!cfg || !cfg.rules) return [];
|
|
47
|
+
const out = [];
|
|
48
|
+
for (const group of Object.keys(cfg.rules)) {
|
|
49
|
+
for (const [name, rule] of Object.entries(cfg.rules[group] || {})) {
|
|
50
|
+
out.push({
|
|
51
|
+
id: `${group}.${name}`,
|
|
52
|
+
source: 'principles',
|
|
53
|
+
severity: normaliseSeverity(rule.severity),
|
|
54
|
+
// Not every rule is threshold-shaped (e.g. unused-imports); report the
|
|
55
|
+
// one that exists rather than inventing a 0.
|
|
56
|
+
threshold: typeof rule.threshold === 'number' ? rule.threshold : null,
|
|
57
|
+
evidence: '.principlesrc',
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Gate 2's duplicate-code rule.
|
|
66
|
+
*
|
|
67
|
+
* `githooks/pre-commit` prints "jscpd found duplicated code (warning, not blocking
|
|
68
|
+
* by default)" -- this gate reports, it does not block. The BUILD-phase doc used to
|
|
69
|
+
* claim "jscpd similarity >5% BLOCK", which sent people to refactor for a verdict
|
|
70
|
+
* that never fires. A generated list must record what the hook does, not what the
|
|
71
|
+
* old prose said.
|
|
72
|
+
*/
|
|
73
|
+
function jscpdRules(cwd) {
|
|
74
|
+
const cfg = readJson(path.join(cwd, 'jscpd.conf.json'));
|
|
75
|
+
// jscpd itself has no similarity threshold in this project's config and the hook
|
|
76
|
+
// never compares a percentage. Reporting null says "no limit declared" instead of
|
|
77
|
+
// inventing a number the hook does not enforce.
|
|
78
|
+
const threshold = cfg && typeof cfg.threshold === 'number' ? cfg.threshold : null;
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
id: 'duplication.jscpd',
|
|
82
|
+
source: 'jscpd',
|
|
83
|
+
severity: 'info',
|
|
84
|
+
threshold,
|
|
85
|
+
evidence: 'githooks/pre-commit (Gate 2, advisory)',
|
|
86
|
+
},
|
|
87
|
+
];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Gate 3 warn/block boundaries, which the hook itself owns. */
|
|
91
|
+
function ccnRules(cwd) {
|
|
92
|
+
return [
|
|
93
|
+
{ id: 'ccn.warn', source: 'ccn', severity: 'warn', threshold: 5, evidence: 'githooks/pre-commit (Gate 3)' },
|
|
94
|
+
{ id: 'ccn.block', source: 'ccn', severity: 'block', threshold: 10, evidence: 'githooks/pre-commit (Gate 3)' },
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Boy Scout's structural limits, read from wherever they are configured. */
|
|
99
|
+
function boyScoutRules(cwd) {
|
|
100
|
+
const out = [
|
|
101
|
+
{
|
|
102
|
+
id: 'boy-scout.new-file',
|
|
103
|
+
source: 'boy-scout',
|
|
104
|
+
severity: 'block',
|
|
105
|
+
threshold: 0,
|
|
106
|
+
evidence: 'src/principles/boy-scout.ts',
|
|
107
|
+
},
|
|
108
|
+
];
|
|
109
|
+
// The per-function line and nesting limits are principles rules; when
|
|
110
|
+
// .principlesrc exists they are already listed by principlesRules, so only add
|
|
111
|
+
// here when it does not (so the BUILD list never silently loses them).
|
|
112
|
+
const cfg = readJson(path.join(cwd, '.principlesrc'));
|
|
113
|
+
if (!cfg || !cfg.rules || !cfg.rules['clean-code']) {
|
|
114
|
+
out.push(
|
|
115
|
+
{ id: 'clean-code.long-function', source: 'boy-scout', severity: 'warn', threshold: 50, evidence: 'defaults' },
|
|
116
|
+
{ id: 'clean-code.deep-nesting', source: 'boy-scout', severity: 'warn', threshold: 4, evidence: 'defaults' },
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Collect every rule that can block or warn a commit.
|
|
124
|
+
*
|
|
125
|
+
* @param {{cwd?: string}} opts
|
|
126
|
+
* @returns {Array<{id: string, source: string, severity: string, threshold: number|null, evidence: string}>}
|
|
127
|
+
*/
|
|
128
|
+
function collectGateRules(opts = {}) {
|
|
129
|
+
const cwd = opts.cwd || process.cwd();
|
|
130
|
+
const rules = [...principlesRules(cwd), ...jscpdRules(cwd), ...ccnRules(cwd), ...boyScoutRules(cwd)];
|
|
131
|
+
// Stable order: blocking first, then warn/info, so a reader stops at what hurts.
|
|
132
|
+
const rank = { block: 0, warn: 1, info: 2 };
|
|
133
|
+
return rules.sort(
|
|
134
|
+
(a, b) => (rank[a.severity] ?? 3) - (rank[b.severity] ?? 3) || a.id.localeCompare(b.id),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Render the checklist as markdown with an evidence column. */
|
|
139
|
+
function renderGateRules(rules) {
|
|
140
|
+
const lines = ['| Rule | Severity | Limit | Evidence |', '|---|---|---|---|'];
|
|
141
|
+
for (const rule of rules) {
|
|
142
|
+
lines.push(
|
|
143
|
+
`| \`${rule.id}\` | ${rule.severity} | ${rule.threshold === null ? '—' : rule.threshold} | \`${rule.evidence}\` |`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return lines.join('\n');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = { collectGateRules, renderGateRules };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror manifest: which canonical sources must be byte-identical in the npm package
|
|
3
|
+
* (#654).
|
|
4
|
+
*
|
|
5
|
+
* `githooks/**` and `plugins/*/src/**` are canonical; `src/npm-package/**` holds
|
|
6
|
+
* byte-identical mirrors that get published. Editing a mirror only means the next
|
|
7
|
+
* prepack silently regenerates it — v0.27.3 shipped exactly that bug.
|
|
8
|
+
*
|
|
9
|
+
* `AGENTS.md` states the rule clearly and it still gets violated ~58 times, because
|
|
10
|
+
* the constraint is *structural* (which file is source, which is mirror, who depends
|
|
11
|
+
* on whom), not stylistic. Knowing the rule does not remove the need to track the
|
|
12
|
+
* mapping across a multi-file edit; that is tooling's job.
|
|
13
|
+
*
|
|
14
|
+
* Hence this module: the mapping is data, and a canonical file missing from the
|
|
15
|
+
* mapping is an error rather than a silent no-op.
|
|
16
|
+
*/
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const path = require('node:path');
|
|
21
|
+
|
|
22
|
+
/** Directories (repo-relative) whose files mirror into the package. */
|
|
23
|
+
const MIRRORED_ROOTS = [
|
|
24
|
+
{ from: 'githooks', to: 'src/npm-package/hooks' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
/** Individual canonical files that mirror into a different location. */
|
|
28
|
+
const INDIVIDUAL_PAIRS = [
|
|
29
|
+
{ source: 'githooks/pre-commit', mirror: 'src/npm-package/hooks/pre-commit' },
|
|
30
|
+
{ source: 'githooks/pre-push', mirror: 'src/npm-package/hooks/pre-push' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/** Paths inside a mirrored root that are NOT mirrored (tests, docs). */
|
|
34
|
+
const EXCLUDED_PREFIXES = ['__tests__/', 'node_modules/', '.git/'];
|
|
35
|
+
|
|
36
|
+
function toPosix(p) {
|
|
37
|
+
return p.split(path.sep).join('/');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Recursively list files under `dir`, repo-relative and forward-slashed. */
|
|
41
|
+
function listFiles(root, dir, out = []) {
|
|
42
|
+
let entries;
|
|
43
|
+
try {
|
|
44
|
+
entries = fs.readdirSync(path.join(root, dir), { withFileTypes: true });
|
|
45
|
+
} catch {
|
|
46
|
+
return out;
|
|
47
|
+
}
|
|
48
|
+
if (!Array.isArray(entries)) return out;
|
|
49
|
+
for (const entry of entries) {
|
|
50
|
+
const rel = toPosix(path.join(dir, entry.name));
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (EXCLUDED_PREFIXES.some((p) => `${entry.name}/` === p)) continue;
|
|
53
|
+
listFiles(root, rel, out);
|
|
54
|
+
} else {
|
|
55
|
+
out.push(rel);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Expand the declared mapping into concrete source→mirror pairs.
|
|
63
|
+
*
|
|
64
|
+
* @param {{cwd?: string}} opts
|
|
65
|
+
* @returns {Array<{source: string, mirror: string}>}
|
|
66
|
+
*/
|
|
67
|
+
function declareSources(opts = {}) {
|
|
68
|
+
const cwd = opts.cwd || process.cwd();
|
|
69
|
+
const pairs = [...INDIVIDUAL_PAIRS];
|
|
70
|
+
const seen = new Set(pairs.map((p) => p.source));
|
|
71
|
+
for (const root of MIRRORED_ROOTS) {
|
|
72
|
+
for (const file of listFiles(cwd, root.from)) {
|
|
73
|
+
if (seen.has(file)) continue;
|
|
74
|
+
const rest = file.slice(root.from.length + 1);
|
|
75
|
+
if (EXCLUDED_PREFIXES.some((prefix) => rest.startsWith(prefix))) continue;
|
|
76
|
+
pairs.push({ source: file, mirror: `${root.to}/${rest}` });
|
|
77
|
+
seen.add(file);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return pairs.sort((a, b) => a.source.localeCompare(b.source));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Canonical files that exist but appear in no mapping entry.
|
|
85
|
+
*
|
|
86
|
+
* This is the check that turns "someone forgot to register java.sh" from a
|
|
87
|
+
* publish-time rollback into a red test at commit time. Silence here is the failure
|
|
88
|
+
* mode worth preventing: a file that is never mirrored looks fine locally and
|
|
89
|
+
* disappears from the published package.
|
|
90
|
+
*
|
|
91
|
+
* @param {{cwd?: string, files?: string[], declared?: Array<{source: string}>}} opts
|
|
92
|
+
* @returns {string[]}
|
|
93
|
+
*/
|
|
94
|
+
function findUnlistedSources(opts = {}) {
|
|
95
|
+
const cwd = opts.cwd || process.cwd();
|
|
96
|
+
const declared = opts.declared || declareSources({ cwd });
|
|
97
|
+
const declaredSet = new Set(declared.map((entry) => entry.source));
|
|
98
|
+
const present = opts.files || listFiles(cwd, 'githooks');
|
|
99
|
+
return present
|
|
100
|
+
.filter((file) => !EXCLUDED_PREFIXES.some((prefix) => file.slice('githooks/'.length).startsWith(prefix)))
|
|
101
|
+
.filter((file) => !declaredSet.has(file))
|
|
102
|
+
.sort();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Human-readable drift report; empty array means clean. */
|
|
106
|
+
function describeUnlisted(unlisted) {
|
|
107
|
+
return unlisted.map(
|
|
108
|
+
(file) => `${file} is a canonical source with no mirror entry — the published package will not carry it`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { declareSources, findUnlistedSources, describeUnlisted, MIRRORED_ROOTS };
|
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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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.8",
|
|
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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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.
|
|
@@ -34,12 +34,25 @@
|
|
|
34
34
|
|---|------|------------------------------|----------|
|
|
35
35
|
| 1 | **Gate 3 CCN** | 单函数圈复杂度 **>10 BLOCK**、**>5 WARN** | 拆函数 / 早返回 / 抽策略表;`xp-gate check 3 <file>` |
|
|
36
36
|
| 2 | **Gate 6 Boy Scout** | **新文件 0 warning**;修改文件 warning 数不得高于 `.warnings-baseline.json`;**单函数 ≤50 行**;**嵌套 ≤4 层** | 新文件当"零容忍"写;改动前 `xp-gate check 6` 看基线 |
|
|
37
|
-
| 3 | **Gate 2 重复代码** | jscpd
|
|
37
|
+
| 3 | **Gate 2 重复代码** | jscpd 报告重复**仅提示,不阻断** | 抽公共函数,别复制粘贴;`xp-gate check 2 <dir>` |
|
|
38
38
|
| 4 | **Gate 4 Principles** | 14 条 Clean Code/SOLID 规则,任一 error/warning 即拦(按 `.principlesrc`) | `xp-gate check 4 <file>`;避免超长参数表、空 catch、`as any` |
|
|
39
39
|
| 5 | **Gate 1 Lint** | 适配器 lint 任一 error | 提交前跑一次项目 lint;不要留未用变量/导入 |
|
|
40
40
|
| 6 | **Gate 5 测试** | 测试全绿 + 覆盖率 ≥80%(新 .ts/.tsx 无测试直接 BLOCK) | 新代码先写测试(TDD);`npx vitest run <file>` |
|
|
41
41
|
| 7 | **mirror/sync 纪律** | 改 `githooks/` 或 `skills/` 后必须 `node src/npm-package/scripts/sync-package-content.js --fix` 且 `bash scripts/check-hook-mirror.sh` 报 PASS | 忘了同步会在 pre-push 的 mirror 校验被打回 |
|
|
42
42
|
|
|
43
|
+
**权威清单用 `xp-gate gate-rules` 现取,不要背上面这张表。** 上表是手抄的,阈值真正定义在
|
|
44
|
+
`.principlesrc`、`jscpd.conf.json` 和 `githooks/pre-commit` 里。`xp-gate gate-rules` 从那些
|
|
45
|
+
文件生成一份带**出处**的清单:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
xp-gate gate-rules # markdown 表:规则 / 严重度 / 阈值 / 出处
|
|
49
|
+
xp-gate gate-rules --json # 机器可读
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
为什么必须生成而不是手写:这张表原本写"jscpd 相似度 >5% BLOCK",而 Gate 2 实际只是提示、
|
|
53
|
+
从不阻断——有人会去为一个永不触发的裁定重构代码。**一个说谎的自检清单比没有清单更糟**,
|
|
54
|
+
它会让人按错误规则写,然后在提交时被告知另一套。改阈值时只需改配置,清单自动跟上。
|
|
55
|
+
|
|
43
56
|
**一条命令抢先看**:`xp-gate check . --changed --brief` 只跑"本次 diff 会触发的门禁",
|
|
44
57
|
一屏出 PASS/SKIP/BLOCK,目标 <5 s。把它放在 ralph-loop 每个 REQ 的 step 0,先看红不红,
|
|
45
58
|
再决定是否继续写——而不是写到一半才被 `git commit` 拦下。
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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.8",
|
|
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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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.
|
|
@@ -34,12 +34,25 @@
|
|
|
34
34
|
|---|------|------------------------------|----------|
|
|
35
35
|
| 1 | **Gate 3 CCN** | 单函数圈复杂度 **>10 BLOCK**、**>5 WARN** | 拆函数 / 早返回 / 抽策略表;`xp-gate check 3 <file>` |
|
|
36
36
|
| 2 | **Gate 6 Boy Scout** | **新文件 0 warning**;修改文件 warning 数不得高于 `.warnings-baseline.json`;**单函数 ≤50 行**;**嵌套 ≤4 层** | 新文件当"零容忍"写;改动前 `xp-gate check 6` 看基线 |
|
|
37
|
-
| 3 | **Gate 2 重复代码** | jscpd
|
|
37
|
+
| 3 | **Gate 2 重复代码** | jscpd 报告重复**仅提示,不阻断** | 抽公共函数,别复制粘贴;`xp-gate check 2 <dir>` |
|
|
38
38
|
| 4 | **Gate 4 Principles** | 14 条 Clean Code/SOLID 规则,任一 error/warning 即拦(按 `.principlesrc`) | `xp-gate check 4 <file>`;避免超长参数表、空 catch、`as any` |
|
|
39
39
|
| 5 | **Gate 1 Lint** | 适配器 lint 任一 error | 提交前跑一次项目 lint;不要留未用变量/导入 |
|
|
40
40
|
| 6 | **Gate 5 测试** | 测试全绿 + 覆盖率 ≥80%(新 .ts/.tsx 无测试直接 BLOCK) | 新代码先写测试(TDD);`npx vitest run <file>` |
|
|
41
41
|
| 7 | **mirror/sync 纪律** | 改 `githooks/` 或 `skills/` 后必须 `node src/npm-package/scripts/sync-package-content.js --fix` 且 `bash scripts/check-hook-mirror.sh` 报 PASS | 忘了同步会在 pre-push 的 mirror 校验被打回 |
|
|
42
42
|
|
|
43
|
+
**权威清单用 `xp-gate gate-rules` 现取,不要背上面这张表。** 上表是手抄的,阈值真正定义在
|
|
44
|
+
`.principlesrc`、`jscpd.conf.json` 和 `githooks/pre-commit` 里。`xp-gate gate-rules` 从那些
|
|
45
|
+
文件生成一份带**出处**的清单:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
xp-gate gate-rules # markdown 表:规则 / 严重度 / 阈值 / 出处
|
|
49
|
+
xp-gate gate-rules --json # 机器可读
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
为什么必须生成而不是手写:这张表原本写"jscpd 相似度 >5% BLOCK",而 Gate 2 实际只是提示、
|
|
53
|
+
从不阻断——有人会去为一个永不触发的裁定重构代码。**一个说谎的自检清单比没有清单更糟**,
|
|
54
|
+
它会让人按错误规则写,然后在提交时被告知另一套。改阈值时只需改配置,清单自动跟上。
|
|
55
|
+
|
|
43
56
|
**一条命令抢先看**:`xp-gate check . --changed --brief` 只跑"本次 diff 会触发的门禁",
|
|
44
57
|
一屏出 PASS/SKIP/BLOCK,目标 <5 s。把它放在 ralph-loop 每个 REQ 的 step 0,先看红不红,
|
|
45
58
|
再决定是否继续写——而不是写到一半才被 `git commit` 拦下。
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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:** b3549c25
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.8.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.
|