@boyingliu01/xp-gate 0.35.0 → 0.35.1
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 +8 -0
- package/hooks/pre-commit +92 -0
- package/lib/__tests__/rework.test.js +127 -0
- package/lib/gate-audit.ts +8 -0
- package/lib/mattpocock-license-verdict.json +1 -1
- package/lib/rework.js +262 -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/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/scripts/__tests__/sync-package-content-isolation.test.js +73 -0
- package/scripts/__tests__/sync-package-content.test.js +109 -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,14 @@ const COMMANDS = {
|
|
|
350
350
|
},
|
|
351
351
|
usage: 'xp-gate retro [--days N] [--all] [--json] [--sprint <id>]'
|
|
352
352
|
},
|
|
353
|
+
'rework': {
|
|
354
|
+
description: 'Report rework: first-pass rate, rounds before landing, top blocking reasons (#651)',
|
|
355
|
+
run: subargs => {
|
|
356
|
+
const { main } = require('../lib/rework.js');
|
|
357
|
+
process.exit(main(subargs));
|
|
358
|
+
},
|
|
359
|
+
usage: 'xp-gate rework [--top N] [--dir <path>] [--json]'
|
|
360
|
+
},
|
|
353
361
|
'crap': {
|
|
354
362
|
description: 'Rank functions by CRAP score (complexity x weak coverage) — advisory, never blocks (#404)',
|
|
355
363
|
run: subargs => {
|
package/hooks/pre-commit
CHANGED
|
@@ -627,6 +627,98 @@ record_gate_audit() {
|
|
|
627
627
|
disown 2>/dev/null || true
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
# ── #651: journal a blocking exit ──────────────────────────────────────────
|
|
631
|
+
#
|
|
632
|
+
# Failure was invisible in this repo's own history: audit.jsonl had 8,258 records
|
|
633
|
+
# and essentially zero gate FAILs, because pre-commit has ~76 `exit 1` blocking
|
|
634
|
+
# paths and record_gate_audit() is only reached AFTER a gate settles. The moment a
|
|
635
|
+
# gate FAILs and exits, the record that would have said so is never written — so
|
|
636
|
+
# `xp-gate retro` and `sprint-status --rework-check` computed rework over a
|
|
637
|
+
# denominator with no failures in it, and rework trended to zero by construction.
|
|
638
|
+
#
|
|
639
|
+
# The trap catches the failure on its way OUT rather than adding a record call to
|
|
640
|
+
# all 76 branches, which would rot the first time someone adds branch 77.
|
|
641
|
+
_BLOCKING_RECORDED=""
|
|
642
|
+
_record_blocking_exit() {
|
|
643
|
+
local rc=$?
|
|
644
|
+
# 0 = success, nothing to journal.
|
|
645
|
+
[ "$rc" -eq 0 ] && return 0
|
|
646
|
+
# One record per run. Without the latch, a gate that already recorded and then
|
|
647
|
+
# exited non-zero would be counted twice, halving the reported first-pass rate.
|
|
648
|
+
[ -n "$_BLOCKING_RECORDED" ] && return 0
|
|
649
|
+
_BLOCKING_RECORDED="1"
|
|
650
|
+
|
|
651
|
+
# Identify the gate that was running. Status vars are NO*-gradeable: any of
|
|
652
|
+
# FAIL/BLOCK means this gate is the reason. Latest failing gate wins, which is
|
|
653
|
+
# the one that actually called exit.
|
|
654
|
+
local _bg_id="" _bg_name="" _bg_status="" _bg_detail=""
|
|
655
|
+
local _i _st_var _st
|
|
656
|
+
for _i in 0 1 2 3 4 5 6 7 8 9 10 11 12 13; do
|
|
657
|
+
_st_var="GATE_${_i}_STATUS"
|
|
658
|
+
_st="${!_st_var:-}"
|
|
659
|
+
case "$_st" in
|
|
660
|
+
FAIL|BLOCK)
|
|
661
|
+
# Latest failing gate wins: it is the one that actually called exit.
|
|
662
|
+
_bg_id="$_i"
|
|
663
|
+
_bg_status="$_st"
|
|
664
|
+
;;
|
|
665
|
+
esac
|
|
666
|
+
done
|
|
667
|
+
local _bg_detail_var="GATE_${_bg_id}_DETAIL"
|
|
668
|
+
_bg_detail="${!_bg_detail_var:-}"
|
|
669
|
+
|
|
670
|
+
# Gate names for the journal. Deliberately short: this runs during shutdown.
|
|
671
|
+
case "$_bg_id" in
|
|
672
|
+
0) _bg_name="Version Consistency" ;;
|
|
673
|
+
1) _bg_name="Code Quality" ;;
|
|
674
|
+
2) _bg_name="Duplicate Code" ;;
|
|
675
|
+
3) _bg_name="Cyclomatic Complexity" ;;
|
|
676
|
+
4) _bg_name="Principles" ;;
|
|
677
|
+
5) _bg_name="Tests + Coverage" ;;
|
|
678
|
+
6) _bg_name="Architecture + Boy Scout" ;;
|
|
679
|
+
7) _bg_name="IaC Security" ;;
|
|
680
|
+
8) _bg_name="Secret Scanning" ;;
|
|
681
|
+
9) _bg_name="Build Integrity" ;;
|
|
682
|
+
10) _bg_name="SAST Security" ;;
|
|
683
|
+
11) _bg_name="Sprint Flow" ;;
|
|
684
|
+
12) _bg_name="File Hygiene" ;;
|
|
685
|
+
13) _bg_name="Compiler Strictness" ;;
|
|
686
|
+
*) _bg_name="unknown" ;;
|
|
687
|
+
esac
|
|
688
|
+
[ -n "$_bg_id" ] || _bg_id="unknown"
|
|
689
|
+
|
|
690
|
+
# The reason must reach the journal or "top-N failing rules" cannot exist.
|
|
691
|
+
# Prefer the gate's own detail (it carries the rule/threshold), else the status.
|
|
692
|
+
local _bg_reason="${_bg_detail:-gate_exit_${rc}}"
|
|
693
|
+
|
|
694
|
+
local _bg_start_var="GATE_${_bg_id}_START"
|
|
695
|
+
local _bg_start="${!_bg_start_var:-}"
|
|
696
|
+
local _bg_dur="null"
|
|
697
|
+
if [ -n "$_bg_start" ]; then
|
|
698
|
+
local _bg_end
|
|
699
|
+
_bg_end=$(now_ms 2>/dev/null || echo "")
|
|
700
|
+
if [ -n "$_bg_end" ]; then
|
|
701
|
+
_bg_dur=$((_bg_end - _bg_start))
|
|
702
|
+
[ "$_bg_dur" -ge 0 ] 2>/dev/null || _bg_dur="null"
|
|
703
|
+
fi
|
|
704
|
+
fi
|
|
705
|
+
|
|
706
|
+
# Fire-and-forget, and never let journaling change the exit code.
|
|
707
|
+
{
|
|
708
|
+
run_tsx "${PROJECT_ROOT_FOR_AUDIT}/src/npm-package/lib/gate-audit.ts" record \
|
|
709
|
+
--gate-id "$_bg_id" \
|
|
710
|
+
--gate-name "$_bg_name" \
|
|
711
|
+
--passed false \
|
|
712
|
+
--issues-found 1 \
|
|
713
|
+
--duration-ms "$_bg_dur" \
|
|
714
|
+
--trigger commit \
|
|
715
|
+
--failure-reason "$_bg_reason" \
|
|
716
|
+
2>/dev/null
|
|
717
|
+
} >/dev/null 2>&1 || true
|
|
718
|
+
return 0
|
|
719
|
+
}
|
|
720
|
+
trap _record_blocking_exit EXIT
|
|
721
|
+
|
|
630
722
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
631
723
|
echo " QUALITY GATES - PRE-COMMIT CHECK"
|
|
632
724
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-651 rework
|
|
3
|
+
* @intent #651 asks for a command that turns the gate journal into the three
|
|
4
|
+
* numbers rework management actually needs:
|
|
5
|
+
*
|
|
6
|
+
* 1. first-pass rate -- how often a round clears the gates on try 1
|
|
7
|
+
* 2. rework round stats -- median / mean / worst rounds per landing
|
|
8
|
+
* 3. top-N failing rules -- which rule_id blocks most often
|
|
9
|
+
*
|
|
10
|
+
* Without (3) the only thing you can do is "try harder". With it, the
|
|
11
|
+
* answer to "why is development slow" becomes a ranked list.
|
|
12
|
+
*
|
|
13
|
+
* Two data hazards decide the design:
|
|
14
|
+
*
|
|
15
|
+
* - Before #651 the journal contained NO failures, so every derived rate
|
|
16
|
+
* was 100% by construction. A command that reports "first-pass rate: 100%"
|
|
17
|
+
* from an empty failure set is worse than no command: it manufactures
|
|
18
|
+
* confidence. So an input with zero FAIL records must report that the
|
|
19
|
+
* figure is NOT MEASURABLE, not that it is perfect.
|
|
20
|
+
*
|
|
21
|
+
* - ~401 historical records carry epoch-sized `duration_ms` (flagged
|
|
22
|
+
* `duration_anomaly`). Any duration aggregate must exclude them rather
|
|
23
|
+
* than average them in -- one of them once produced "average 430 million
|
|
24
|
+
* seconds".
|
|
25
|
+
*
|
|
26
|
+
* These tests exercise the pure aggregation function, so the numbers are
|
|
27
|
+
* pinned independently of file layout.
|
|
28
|
+
* @covers AC-670-59, AC-670-60, AC-670-61, AC-670-62
|
|
29
|
+
*/
|
|
30
|
+
'use strict';
|
|
31
|
+
|
|
32
|
+
const { computeReworkStats } = require('../rework');
|
|
33
|
+
|
|
34
|
+
/** One journal record, defaulting to a pass. */
|
|
35
|
+
function rec(overrides) {
|
|
36
|
+
return {
|
|
37
|
+
timestamp: '2026-10-30T10:00:00.000Z',
|
|
38
|
+
gate_id: '1',
|
|
39
|
+
gate_name: 'Code Quality',
|
|
40
|
+
passed: true,
|
|
41
|
+
issues_found: 0,
|
|
42
|
+
duration_ms: 1000,
|
|
43
|
+
trigger: 'commit',
|
|
44
|
+
...overrides,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('computeReworkStats (#651)', () => {
|
|
49
|
+
it('AC-670-59: reports an unmeasurable first-pass rate when the journal has no failures', () => {
|
|
50
|
+
// This is exactly the pre-#651 journal: thousands of passes, zero fails.
|
|
51
|
+
const entries = Array.from({ length: 50 }, () => rec());
|
|
52
|
+
const stats = computeReworkStats(entries);
|
|
53
|
+
// A rate derived from an empty failure set is not 100% -- it is unknown.
|
|
54
|
+
expect(stats.firstPassRate).toBeNull();
|
|
55
|
+
expect(stats.measurable).toBe(false);
|
|
56
|
+
expect(stats.reason).toMatch(/no failure/i);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('AC-670-60: counts failing rules and ranks them, so the worst rule is actionable', () => {
|
|
60
|
+
const entries = [
|
|
61
|
+
rec({ passed: false, failure_reason: 'long-function' }),
|
|
62
|
+
rec({ passed: false, failure_reason: 'long-function' }),
|
|
63
|
+
rec({ passed: false, failure_reason: 'long-function' }),
|
|
64
|
+
rec({ passed: false, failure_reason: 'tests_failed' }),
|
|
65
|
+
rec(),
|
|
66
|
+
rec(),
|
|
67
|
+
];
|
|
68
|
+
const stats = computeReworkStats(entries);
|
|
69
|
+
expect(stats.measurable).toBe(true);
|
|
70
|
+
expect(stats.topFailures[0]).toEqual({ reason: 'long-function', count: 3 });
|
|
71
|
+
expect(stats.topFailures[1]).toEqual({ reason: 'tests_failed', count: 1 });
|
|
72
|
+
// 4 failures out of 6 attempts -> 33% first-pass.
|
|
73
|
+
expect(stats.firstPassRate).toBeCloseTo(33.3, 1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('AC-670-61: excludes anomaly-flagged durations instead of averaging them in', () => {
|
|
77
|
+
// A real epoch-sized value from the corpus; averaging it in produced
|
|
78
|
+
// "average 430 million seconds" on gate-1.
|
|
79
|
+
const entries = [
|
|
80
|
+
rec({ duration_ms: 1000 }),
|
|
81
|
+
rec({ duration_ms: 3000 }),
|
|
82
|
+
rec({ duration_ms: 1784536915891, duration_anomaly: true }),
|
|
83
|
+
];
|
|
84
|
+
const stats = computeReworkStats(entries);
|
|
85
|
+
expect(stats.duration).not.toBeNull();
|
|
86
|
+
// Median over the two sane values (1000, 3000) is 2000, not ~5.9e11.
|
|
87
|
+
expect(stats.duration.medianMs).toBe(2000);
|
|
88
|
+
expect(stats.duration.samples).toBe(2);
|
|
89
|
+
expect(stats.duration.excludedAnomalies).toBe(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('AC-670-62: a failure with no reason is bucketed, never silently dropped', () => {
|
|
93
|
+
const entries = [
|
|
94
|
+
rec({ passed: false }), // no failure_reason
|
|
95
|
+
rec({ passed: false, failure_reason: '' }), // empty
|
|
96
|
+
rec({ passed: false, failure_reason: 'short-circuit' }),
|
|
97
|
+
];
|
|
98
|
+
const stats = computeReworkStats(entries);
|
|
99
|
+
// Dropping unlabelled failures would understate the failure count and push
|
|
100
|
+
// the first-pass rate UP -- the optimistic direction is the dangerous one.
|
|
101
|
+
expect(stats.failures).toBe(3);
|
|
102
|
+
const unattributed = stats.topFailures.find((f) => /unattributed|unknown/i.test(f.reason));
|
|
103
|
+
expect(unattributed, 'unlabelled failures must appear in the ranking').toBeDefined();
|
|
104
|
+
expect(unattributed.count).toBe(2);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('rework duration sampling (#651)', () => {
|
|
109
|
+
it('AC-670-61: an UNFLAGGED epoch-sized duration is still excluded', () => {
|
|
110
|
+
// Measured on the real journal: 391 records carry duration_anomaly, but 6
|
|
111
|
+
// more epoch-sized values do not. Trusting the flag alone left the mean at
|
|
112
|
+
// 1.3e9 ms -- the "average 430 million seconds" artifact returning through
|
|
113
|
+
// the back door. Plausibility must be checked independently of the flag.
|
|
114
|
+
const entries = [
|
|
115
|
+
rec({ duration_ms: 2000 }),
|
|
116
|
+
rec({ duration_ms: 4000 }),
|
|
117
|
+
rec({ duration_ms: 1785001784992 }), // no duration_anomaly flag
|
|
118
|
+
];
|
|
119
|
+
const stats = computeReworkStats(entries);
|
|
120
|
+
expect(stats.duration).not.toBeNull();
|
|
121
|
+
expect(stats.duration.medianMs).toBe(3000);
|
|
122
|
+
expect(stats.duration.samples).toBe(2);
|
|
123
|
+
// Excluded count covers BOTH flagged and implausible, or the sample size
|
|
124
|
+
// looks closer to the record count than it really is.
|
|
125
|
+
expect(stats.duration.excludedAnomalies).toBe(1);
|
|
126
|
+
});
|
|
127
|
+
});
|
package/lib/gate-audit.ts
CHANGED
|
@@ -41,6 +41,12 @@ export interface GateAuditEntry {
|
|
|
41
41
|
duration_anomaly?: boolean; // Issue #370: set when duration_ms > max_duration_ms
|
|
42
42
|
duration_reason?: 'poisoned_start_ms' | 'clock_skew'; // REQ-B: why duration_ms is null
|
|
43
43
|
detail?: string; // #507: optional gate-specific payload (e.g. effective CCN threshold)
|
|
44
|
+
/**
|
|
45
|
+
* #651: WHY the gate blocked. Without this the journal can only answer "how
|
|
46
|
+
* many failed", never "which rule fails most" — and the second question is the
|
|
47
|
+
* one that drives fixes. Only meaningful when passed=false.
|
|
48
|
+
*/
|
|
49
|
+
failure_reason?: string;
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
// ── Public API ───────────────────────────────────────────────────────────────
|
|
@@ -415,6 +421,8 @@ function runCli(): void {
|
|
|
415
421
|
...(durationReason ? { duration_reason: durationReason as 'poisoned_start_ms' | 'clock_skew' } : {}),
|
|
416
422
|
// #507: only set when provided (omit field otherwise)
|
|
417
423
|
...(opts['detail'] ? { detail: opts['detail'] } : {}),
|
|
424
|
+
// #651: only set when provided, so successful records stay byte-identical.
|
|
425
|
+
...(opts['failure-reason'] ? { failure_reason: String(opts['failure-reason']).slice(0, 300) } : {}),
|
|
418
426
|
};
|
|
419
427
|
// Pre-commit (trigger=commit) is staging for a commit that does not exist
|
|
420
428
|
// yet: park it in the pending sidecar so post-commit can stamp the real
|
package/lib/rework.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `xp-gate rework` — what the gate journal says about rework (#651).
|
|
3
|
+
*
|
|
4
|
+
* `xp-gate retro` already answers "how much did we ship". This answers the
|
|
5
|
+
* narrower and more actionable question: how often does a round of work clear
|
|
6
|
+
* the gates on the first attempt, and which rule stops it most.
|
|
7
|
+
*
|
|
8
|
+
* The reason this needs a dedicated command rather than a `retro` line: before
|
|
9
|
+
* #651 the journal contained NO failure records at all, so a naive rework rate
|
|
10
|
+
* computed from it returned 100% every time. A metric that reads "perfect" when
|
|
11
|
+
* it is actually unmeasured is worse than no metric -- it manufactures
|
|
12
|
+
* confidence. So `measurable: false` is a first-class outcome here.
|
|
13
|
+
*
|
|
14
|
+
* No runtime dependencies: this ships inside the zero-install package.
|
|
15
|
+
*/
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
|
|
21
|
+
const AUDIT_DIR = '.xp-gate';
|
|
22
|
+
const AUDIT_FILE = 'audit.jsonl';
|
|
23
|
+
const PENDING_FILE = 'audit.pending.jsonl';
|
|
24
|
+
|
|
25
|
+
/** Bucket name for failures the journal cannot attribute to a rule. */
|
|
26
|
+
const UNATTRIBUTED = 'unattributed';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read every journal record under `repoRoot`, oldest first.
|
|
30
|
+
*
|
|
31
|
+
* Malformed lines are skipped rather than thrown on: the journal is append-only
|
|
32
|
+
* and a single truncated write must not take the whole report down.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} [repoRoot]
|
|
35
|
+
* @returns {Array<object>}
|
|
36
|
+
*/
|
|
37
|
+
function readJournal(repoRoot = process.cwd()) {
|
|
38
|
+
const dir = path.join(repoRoot, AUDIT_DIR);
|
|
39
|
+
const entries = [];
|
|
40
|
+
for (const file of [AUDIT_FILE, PENDING_FILE]) {
|
|
41
|
+
const full = path.join(dir, file);
|
|
42
|
+
let raw;
|
|
43
|
+
try {
|
|
44
|
+
raw = fs.readFileSync(full, 'utf8');
|
|
45
|
+
} catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (const line of raw.split('\n')) {
|
|
49
|
+
if (!line.trim()) continue;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(line);
|
|
52
|
+
if (parsed && typeof parsed === 'object') entries.push(parsed);
|
|
53
|
+
} catch {
|
|
54
|
+
/* truncated/garbled line -- skip, do not fail the report */
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Median of a numeric array (0 for an empty array). */
|
|
62
|
+
function median(values) {
|
|
63
|
+
if (values.length === 0) return 0;
|
|
64
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
65
|
+
const mid = Math.floor(sorted.length / 2);
|
|
66
|
+
return sorted.length % 2 === 0 ? (sorted[mid - 1] + sorted[mid]) / 2 : sorted[mid];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Mean of a numeric array (0 for an empty array). */
|
|
70
|
+
function mean(values) {
|
|
71
|
+
if (values.length === 0) return 0;
|
|
72
|
+
return values.reduce((a, b) => a + b, 0) / values.length;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Group a flat record list into rework rounds.
|
|
77
|
+
*
|
|
78
|
+
* The journal is a flat stream of gate executions with no round identifier, so a
|
|
79
|
+
* round is reconstructed the only way the data allows: a run of FAIL records
|
|
80
|
+
* terminated by the next PASS. That is exactly "how many attempts before this
|
|
81
|
+
* landed", which is the definition rework needs. A trailing run of failures with
|
|
82
|
+
* no resolving pass is a round that has not landed yet, and is counted as
|
|
83
|
+
* `unresolved` rather than being dropped.
|
|
84
|
+
*
|
|
85
|
+
* @param {Array<object>} entries
|
|
86
|
+
*/
|
|
87
|
+
function groupRounds(entries) {
|
|
88
|
+
const rounds = [];
|
|
89
|
+
let current = 0;
|
|
90
|
+
for (const entry of entries) {
|
|
91
|
+
if (entry.passed) {
|
|
92
|
+
if (current > 0) rounds.push(current);
|
|
93
|
+
current = 0;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
current += 1;
|
|
97
|
+
}
|
|
98
|
+
return { resolved: rounds, unresolved: current };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Aggregate the journal into rework metrics.
|
|
103
|
+
*
|
|
104
|
+
* @param {Array<object>} entries journal records
|
|
105
|
+
* @returns {object} see AC-670-59..62
|
|
106
|
+
*/
|
|
107
|
+
function computeReworkStats(entries) {
|
|
108
|
+
const list = Array.isArray(entries) ? entries : [];
|
|
109
|
+
const totals = list.length;
|
|
110
|
+
const failures = list.filter((e) => e.passed === false).length;
|
|
111
|
+
const passes = totals - failures;
|
|
112
|
+
|
|
113
|
+
// ── Failures by reason ────────────────────────────────────────────────────
|
|
114
|
+
// Unlabelled failures are bucketed, never dropped: dropping them would lower
|
|
115
|
+
// the failure count and push the first-pass rate UP, which is the optimistic
|
|
116
|
+
// direction and therefore the dangerous one.
|
|
117
|
+
const byReason = new Map();
|
|
118
|
+
for (const entry of list) {
|
|
119
|
+
if (entry.passed !== false) continue;
|
|
120
|
+
const raw = typeof entry.failure_reason === 'string' ? entry.failure_reason.trim() : '';
|
|
121
|
+
const key = raw === '' ? UNATTRIBUTED : raw;
|
|
122
|
+
byReason.set(key, (byReason.get(key) || 0) + 1);
|
|
123
|
+
}
|
|
124
|
+
const topFailures = [...byReason.entries()]
|
|
125
|
+
.map(([reason, count]) => ({ reason, count }))
|
|
126
|
+
.sort((a, b) => b.count - a.count || a.reason.localeCompare(b.reason));
|
|
127
|
+
|
|
128
|
+
// ── Durations ─────────────────────────────────────────────────────────────
|
|
129
|
+
// Two independent guards, because the flag alone is not enough:
|
|
130
|
+
//
|
|
131
|
+
// 1. honour `duration_anomaly` -- the writer's own verdict;
|
|
132
|
+
// 2. reject implausible magnitudes. Measured on the real journal: 391 records
|
|
133
|
+
// carry the flag, but 6 MORE epoch-sized values (up to 1.78e12) do not,
|
|
134
|
+
// so trusting the flag alone left the mean at 1.3e9 ms -- the very
|
|
135
|
+
// "average 430 million seconds" artifact this is supposed to kill.
|
|
136
|
+
//
|
|
137
|
+
// The ceiling is deliberately generous (1 hour): a real gate run that long is
|
|
138
|
+
// pathological but conceivable, while an epoch stamp is ~1.78e12 and cannot be
|
|
139
|
+
// mistaken for anything else.
|
|
140
|
+
const MAX_PLAUSIBLE_DURATION_MS = 60 * 60 * 1000;
|
|
141
|
+
const isPlausible = (e) => typeof e.duration_ms === 'number'
|
|
142
|
+
&& Number.isFinite(e.duration_ms)
|
|
143
|
+
&& e.duration_ms >= 0
|
|
144
|
+
&& e.duration_ms <= MAX_PLAUSIBLE_DURATION_MS
|
|
145
|
+
&& e.duration_anomaly !== true;
|
|
146
|
+
const sane = list.filter(isPlausible);
|
|
147
|
+
const duration = sane.length > 0
|
|
148
|
+
? {
|
|
149
|
+
medianMs: median(sane.map((e) => e.duration_ms)),
|
|
150
|
+
meanMs: Math.round(mean(sane.map((e) => e.duration_ms))),
|
|
151
|
+
samples: sane.length,
|
|
152
|
+
// Every record that did NOT contribute -- flagged or implausible. Reporting
|
|
153
|
+
// only the flagged count would understate the exclusion and make the
|
|
154
|
+
// sample size look closer to the record count than it is.
|
|
155
|
+
excludedAnomalies: list.filter((e) => typeof e.duration_ms === 'number' && !isPlausible(e)).length,
|
|
156
|
+
}
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
// ── Rounds ────────────────────────────────────────────────────────────────
|
|
160
|
+
const rounds = groupRounds(list);
|
|
161
|
+
const reworkRounds = rounds.resolved;
|
|
162
|
+
|
|
163
|
+
// ── First-pass rate ───────────────────────────────────────────────────────
|
|
164
|
+
// Undefined without failures. A rate over an empty failure set is not 100%.
|
|
165
|
+
const measurable = failures > 0;
|
|
166
|
+
const firstPassRate = measurable ? Math.round((passes / totals) * 1000) / 10 : null;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
measurable,
|
|
170
|
+
reason: measurable ? null : 'no failure records in the journal -- first-pass rate is not measurable (pre-#651 journals only recorded passes)',
|
|
171
|
+
totalRecords: totals,
|
|
172
|
+
passes,
|
|
173
|
+
failures,
|
|
174
|
+
firstPassRate,
|
|
175
|
+
rounds: {
|
|
176
|
+
resolved: reworkRounds.length,
|
|
177
|
+
unresolved: rounds.unresolved > 0,
|
|
178
|
+
medianRework: median(reworkRounds),
|
|
179
|
+
meanRework: Math.round(mean(reworkRounds) * 100) / 100,
|
|
180
|
+
worstRework: reworkRounds.length > 0 ? Math.max(...reworkRounds) : 0,
|
|
181
|
+
},
|
|
182
|
+
topFailures,
|
|
183
|
+
duration,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Render a stats object for the terminal. */
|
|
188
|
+
function formatReworkReport(stats, { limit = 10 } = {}) {
|
|
189
|
+
const lines = [];
|
|
190
|
+
lines.push('');
|
|
191
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
192
|
+
lines.push(' REWORK REPORT');
|
|
193
|
+
lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
194
|
+
lines.push('');
|
|
195
|
+
lines.push(` Journal records: ${stats.totalRecords} (${stats.passes} pass, ${stats.failures} fail)`);
|
|
196
|
+
|
|
197
|
+
if (!stats.measurable) {
|
|
198
|
+
// Say WHY, and say it loudly: a silent 100% would be a lie.
|
|
199
|
+
lines.push('');
|
|
200
|
+
lines.push(' ⚠️ First-pass rate: NOT MEASURABLE');
|
|
201
|
+
lines.push(` ${stats.reason}`);
|
|
202
|
+
} else {
|
|
203
|
+
lines.push(` First-pass rate: ${stats.firstPassRate}%`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (stats.rounds.resolved > 0) {
|
|
207
|
+
lines.push('');
|
|
208
|
+
lines.push(` Rework rounds (failures before a landing): ${stats.rounds.resolved}`);
|
|
209
|
+
lines.push(` median ${stats.rounds.medianRework} / mean ${stats.rounds.meanRework} / worst ${stats.rounds.worstRework}`);
|
|
210
|
+
if (stats.rounds.unresolved) {
|
|
211
|
+
lines.push(' (a trailing run of failures has not landed yet)');
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (stats.topFailures.length > 0) {
|
|
216
|
+
lines.push('');
|
|
217
|
+
lines.push(` Top ${limit} blocking reasons:`);
|
|
218
|
+
for (const { reason, count } of stats.topFailures.slice(0, limit)) {
|
|
219
|
+
lines.push(` ${String(count).padStart(5)} ${reason}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (stats.duration) {
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(` Gate duration: median ${stats.duration.medianMs} ms / mean ${stats.duration.meanMs} ms (${stats.duration.samples} samples`);
|
|
226
|
+
lines.push(` ${stats.duration.excludedAnomalies} anomaly record(s) excluded)`);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
lines.push('');
|
|
230
|
+
return lines.join('\n');
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* CLI entry.
|
|
235
|
+
* @param {string[]} argv args after `rework`
|
|
236
|
+
*/
|
|
237
|
+
function main(argv = []) {
|
|
238
|
+
const repoRootIdx = argv.indexOf('--dir');
|
|
239
|
+
const repoRoot = repoRootIdx !== -1 && argv[repoRootIdx + 1] ? argv[repoRootIdx + 1] : process.cwd();
|
|
240
|
+
const limitIdx = argv.indexOf('--top');
|
|
241
|
+
const limit = limitIdx !== -1 && argv[limitIdx + 1] ? parseInt(argv[limitIdx + 1], 10) : 10;
|
|
242
|
+
|
|
243
|
+
const stats = computeReworkStats(readJournal(repoRoot));
|
|
244
|
+
if (argv.includes('--json')) {
|
|
245
|
+
process.stdout.write(`${JSON.stringify(stats, null, 2)}\n`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
process.stdout.write(`${formatReworkReport(stats, { limit })}\n`);
|
|
249
|
+
return 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
module.exports = {
|
|
253
|
+
readJournal,
|
|
254
|
+
computeReworkStats,
|
|
255
|
+
formatReworkReport,
|
|
256
|
+
main,
|
|
257
|
+
UNATTRIBUTED,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
if (require.main === module) {
|
|
261
|
+
process.exitCode = main(process.argv.slice(2));
|
|
262
|
+
}
|
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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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.1",
|
|
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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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.1",
|
|
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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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.1",
|
|
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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test REQ-645
|
|
3
|
+
* @intent `sync-package-content.test.js` used the REAL repository root as the
|
|
4
|
+
* spawn cwd:
|
|
5
|
+
*
|
|
6
|
+
* const WORKTREE_PATH = path.resolve(__dirname, '..', '..', '..', '..');
|
|
7
|
+
*
|
|
8
|
+
* Four levels up from `src/npm-package/scripts/__tests__/` is the repo
|
|
9
|
+
* root, not a temp dir. The tests then ran the real sync script with
|
|
10
|
+
* `cwd: WORKTREE_PATH`, so the script's writes landed in the repository
|
|
11
|
+
* instead of a sandbox. CI's Gate 5 failed at `vitest-worktree-guard`:
|
|
12
|
+
*
|
|
13
|
+
* Work tree polluted by the test run — 1 path(s) the run introduced:
|
|
14
|
+
* M src/npm-package/mock-policy/gate-m3.ts
|
|
15
|
+
*
|
|
16
|
+
* The reason this is easy to miss: the pollution is CONDITIONAL. The
|
|
17
|
+
* script's writes are idempotent, so when the mirror already matches the
|
|
18
|
+
* source nothing changes and the run looks clean. It only dirties the tree
|
|
19
|
+
* when the checked-out commit happens to carry a stale mirror — which is
|
|
20
|
+
* exactly when CI is checking out a commit that the fix was supposed to
|
|
21
|
+
* validate. So it presents as "flaky, just re-run it", and re-running
|
|
22
|
+
* hides the cause while leaving the latent defect in place.
|
|
23
|
+
*
|
|
24
|
+
* The guard catching it is good, but it catches the CONSEQUENCE. This file
|
|
25
|
+
* pins the CAUSE: a test may not write into the repository.
|
|
26
|
+
* @covers AC-670-54, AC-670-55
|
|
27
|
+
*/
|
|
28
|
+
'use strict';
|
|
29
|
+
|
|
30
|
+
const fs = require('node:fs');
|
|
31
|
+
const path = require('node:path');
|
|
32
|
+
|
|
33
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
34
|
+
const TEST_FILE = path.join(
|
|
35
|
+
REPO_ROOT, 'src', 'npm-package', 'scripts', '__tests__', 'sync-package-content.test.js',
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
function readSuite() {
|
|
39
|
+
return fs.readFileSync(TEST_FILE, 'utf8');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('sync-package-content tests must not write into the repository (#645)', () => {
|
|
43
|
+
it('AC-670-54: no test uses the repo root as a spawn cwd', () => {
|
|
44
|
+
const source = readSuite();
|
|
45
|
+
// The exact offending expression. `path.resolve(__dirname, '..' x4)` from the
|
|
46
|
+
// test file IS the repo root -- there is no temp dir anywhere in that chain.
|
|
47
|
+
const offenders = source
|
|
48
|
+
.split('\n')
|
|
49
|
+
.map((line, i) => ({ line: line.trim(), n: i + 1 }))
|
|
50
|
+
.filter((x) => /WORKTREE_PATH\s*=\s*path\.resolve\(__dirname/.test(x.line));
|
|
51
|
+
expect(
|
|
52
|
+
offenders,
|
|
53
|
+
`tests still anchor on the real repo root:\n${offenders.map((o) => ` :${o.n} ${o.line}`).join('\n')}`,
|
|
54
|
+
).toEqual([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('AC-670-55: the sync tests run against a temp worktree, not the checkout', () => {
|
|
58
|
+
const source = readSuite();
|
|
59
|
+
// Whatever the variable is called, the cwd handed to a spawned sync must be
|
|
60
|
+
// one the test CREATED. mkdtemp is the only way to guarantee that: a path
|
|
61
|
+
// built from __dirname is by construction inside the repository.
|
|
62
|
+
expect(source, 'suite must create a throwaway worktree').toMatch(/mkdtemp/);
|
|
63
|
+
// Every spawnSync `cwd:` must point at that temp root, never at the checkout.
|
|
64
|
+
const cwdLines = source
|
|
65
|
+
.split('\n')
|
|
66
|
+
.filter((line) => /^\s*cwd:\s*/.test(line))
|
|
67
|
+
.map((line) => line.trim());
|
|
68
|
+
expect(cwdLines.length, 'expected at least one spawn cwd').toBeGreaterThan(0);
|
|
69
|
+
for (const line of cwdLines) {
|
|
70
|
+
expect(line, `spawn cwd escapes the sandbox: ${line}`).not.toMatch(/cwd:\s*REPO_ROOT/);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -27,6 +27,37 @@ function writeDoc(filePath, content) {
|
|
|
27
27
|
fs.writeFileSync(filePath, content, 'utf8');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Copy the canonical source trees a sync run READS into `destRoot` (#645).
|
|
32
|
+
*
|
|
33
|
+
* The sync script mirrors githooks/, skills/, plugins/ and every src/ module into
|
|
34
|
+
* the package. Seeding the whole of `src/` except the package itself keeps this
|
|
35
|
+
* list from rotting: syncModules() gains a module, this helper needs no edit.
|
|
36
|
+
* Only reads from the real checkout -- nothing is ever written back to it.
|
|
37
|
+
*/
|
|
38
|
+
function seedCanonicalSources(realRoot, destRoot) {
|
|
39
|
+
const real = realRoot || path.resolve(__dirname, '..', '..', '..', '..');
|
|
40
|
+
const dest = destRoot;
|
|
41
|
+
for (const tree of ['githooks', 'skills', 'plugins']) {
|
|
42
|
+
const from = path.join(real, tree);
|
|
43
|
+
if (fs.existsSync(from)) fs.cpSync(from, path.join(dest, tree), { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
const srcRoot = path.join(real, 'src');
|
|
46
|
+
for (const name of fs.readdirSync(srcRoot)) {
|
|
47
|
+
// The package itself is the DESTINATION of the sync, not a canonical source;
|
|
48
|
+
// copying it would let the test compare a mirror against a stale mirror.
|
|
49
|
+
if (name === 'npm-package') continue;
|
|
50
|
+
const from = path.join(srcRoot, name);
|
|
51
|
+
if (fs.statSync(from).isDirectory()) {
|
|
52
|
+
fs.cpSync(from, path.join(dest, 'src', name), { recursive: true });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
fs.mkdirSync(path.join(dest, 'src', 'npm-package', 'scripts'), { recursive: true });
|
|
56
|
+
for (const rel of ['scripts/sync-package-content.js', 'package.json']) {
|
|
57
|
+
fs.copyFileSync(path.join(srcRoot, 'npm-package', rel), path.join(dest, 'src', 'npm-package', rel));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
30
61
|
describe('checkDocsDrift', () => {
|
|
31
62
|
let tempDir;
|
|
32
63
|
let preCommitPath;
|
|
@@ -269,12 +300,56 @@ describe('syncGateScripts', () => {
|
|
|
269
300
|
* @covers AC-450-01, AC-450-02, AC-450-03, AC-450-04
|
|
270
301
|
*/
|
|
271
302
|
describe('syncHooks', () => {
|
|
272
|
-
|
|
303
|
+
// #645: run the sync against a THROWAWAY worktree, never the checkout.
|
|
304
|
+
//
|
|
305
|
+
// This block used to spawn the real sync script with
|
|
306
|
+
// cwd: path.resolve(__dirname, '..', '..', '..', '..')
|
|
307
|
+
// -- four levels up from src/npm-package/scripts/__tests__ is the REPOSITORY
|
|
308
|
+
// ROOT, not a temp dir -- so the script's writes landed in the repo and CI's
|
|
309
|
+
// Gate 5 failed at vitest-worktree-guard with
|
|
310
|
+
// Work tree polluted by the test run: M src/npm-package/mock-policy/gate-m3.ts
|
|
311
|
+
//
|
|
312
|
+
// The pollution was CONDITIONAL (the writes are idempotent, so it only dirties
|
|
313
|
+
// the tree when the checked-out commit carries a stale mirror), which made it
|
|
314
|
+
// look flaky and encouraged "just re-run it" -- hiding the cause. The guard
|
|
315
|
+
// catches the consequence; a temp cwd removes the cause.
|
|
316
|
+
const REAL_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
317
|
+
const TMP_WORKTREE = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-sync-hooks-'));
|
|
318
|
+
const WORKTREE_PATH = TMP_WORKTREE;
|
|
273
319
|
const PKG_ROOT = path.join(WORKTREE_PATH, 'src', 'npm-package');
|
|
274
320
|
const HOOKS_DIR = path.join(PKG_ROOT, 'hooks');
|
|
275
321
|
const syncScript = path.join(WORKTREE_PATH, 'src', 'npm-package', 'scripts', 'sync-package-content.js');
|
|
276
322
|
const nodeExe = process.execPath;
|
|
277
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Lay out just enough of the repo shape inside the temp root for the sync script
|
|
326
|
+
* to read its canonical sources and write its mirrors. Copies only the trees the
|
|
327
|
+
* script touches: githooks/ (the hook source of truth) and the packages it
|
|
328
|
+
* mirrors into. The REAL checkout is only ever READ.
|
|
329
|
+
*/
|
|
330
|
+
function seedTempWorktree() {
|
|
331
|
+
seedCanonicalSources(REAL_REPO_ROOT, WORKTREE_PATH);
|
|
332
|
+
fs.mkdirSync(path.join(PKG_ROOT, 'scripts'), { recursive: true });
|
|
333
|
+
fs.copyFileSync(
|
|
334
|
+
path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'scripts', 'sync-package-content.js'),
|
|
335
|
+
syncScript,
|
|
336
|
+
);
|
|
337
|
+
// The script resolves its package root relative to its own location, so it
|
|
338
|
+
// must sit at <temp>/src/npm-package/scripts/ exactly as in the real tree.
|
|
339
|
+
fs.copyFileSync(
|
|
340
|
+
path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'package.json'),
|
|
341
|
+
path.join(PKG_ROOT, 'package.json'),
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
beforeAll(() => {
|
|
346
|
+
seedTempWorktree();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
afterAll(() => {
|
|
350
|
+
fs.rmSync(TMP_WORKTREE, { recursive: true, force: true });
|
|
351
|
+
});
|
|
352
|
+
|
|
278
353
|
// Ensure hooks are re-synced once before tests run; share the result.
|
|
279
354
|
let syncOk = false;
|
|
280
355
|
beforeAll(() => {
|
|
@@ -473,9 +548,41 @@ describe('checkAdapterDrift', () => {
|
|
|
473
548
|
const { spawnSync } = require('child_process');
|
|
474
549
|
|
|
475
550
|
describe('sync-package-content subprocess invocation', () => {
|
|
476
|
-
|
|
551
|
+
// #645: same rule as the syncHooks block above -- a test may not write into the
|
|
552
|
+
// repository. `path.resolve(__dirname, '..', '..', '..', '..')` IS the repo root,
|
|
553
|
+
// so spawning the sync script with it as cwd copied mirrors over the checkout and
|
|
554
|
+
// CI's Gate 5 failed at vitest-worktree-guard.
|
|
555
|
+
//
|
|
556
|
+
// This block additionally DELETES package directories to prove the sync
|
|
557
|
+
// recreates them, which is precisely the operation that must never touch a real
|
|
558
|
+
// checkout. Everything therefore runs inside a throwaway tree seeded from the
|
|
559
|
+
// real sources (read-only).
|
|
560
|
+
const REAL_REPO_ROOT = path.resolve(__dirname, '..', '..', '..', '..');
|
|
561
|
+
const TMP_WORKTREE = fs.mkdtempSync(path.join(os.tmpdir(), 'xp-gate-sync-sub-'));
|
|
562
|
+
const WORKTREE_PATH = TMP_WORKTREE;
|
|
477
563
|
const PKG_ROOT = path.join(WORKTREE_PATH, 'src', 'npm-package');
|
|
478
564
|
const REPO_ROOT = path.join(WORKTREE_PATH);
|
|
565
|
+
|
|
566
|
+
function seedTempWorktree() {
|
|
567
|
+
seedCanonicalSources(REAL_REPO_ROOT, WORKTREE_PATH);
|
|
568
|
+
fs.mkdirSync(path.join(PKG_ROOT, 'scripts'), { recursive: true });
|
|
569
|
+
fs.copyFileSync(
|
|
570
|
+
path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'scripts', 'sync-package-content.js'),
|
|
571
|
+
path.join(PKG_ROOT, 'scripts', 'sync-package-content.js'),
|
|
572
|
+
);
|
|
573
|
+
fs.copyFileSync(
|
|
574
|
+
path.join(REAL_REPO_ROOT, 'src', 'npm-package', 'package.json'),
|
|
575
|
+
path.join(PKG_ROOT, 'package.json'),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
beforeAll(() => {
|
|
580
|
+
seedTempWorktree();
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
afterAll(() => {
|
|
584
|
+
fs.rmSync(TMP_WORKTREE, { recursive: true, force: true });
|
|
585
|
+
});
|
|
479
586
|
const CORE_SKILLS = [
|
|
480
587
|
'admin-template-guidelines',
|
|
481
588
|
'delphi-review',
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
|
|
2
2
|
|
|
3
3
|
**Generated:** 2026-10-30
|
|
4
|
-
**Commit:**
|
|
4
|
+
**Commit:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.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:** 2ce96f13
|
|
5
5
|
**Branch:** main
|
|
6
|
-
**Version:** 0.35.
|
|
6
|
+
**Version:** 0.35.1.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|