@boyingliu01/xp-gate 0.14.28 → 0.14.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/bin/xp-gate.js +8 -0
  2. package/lib/__tests__/phase-transition.test.js +100 -2
  3. package/lib/__tests__/sprint-audit.test.js +207 -0
  4. package/lib/phase-transition.js +45 -7
  5. package/lib/sprint-audit.js +235 -0
  6. package/mock-policy/AGENTS.md +2 -2
  7. package/mutation/AGENTS.md +2 -2
  8. package/package.json +2 -2
  9. package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
  10. package/plugins/claude-code/skills/delphi-review/AGENTS.md +2 -2
  11. package/plugins/claude-code/skills/sprint-flow/AGENTS.md +2 -2
  12. package/plugins/claude-code/skills/sprint-flow/references/orchestration-rules.md +10 -0
  13. package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +2 -2
  14. package/plugins/opencode/package.json +1 -1
  15. package/plugins/opencode/skills/delphi-review/AGENTS.md +2 -2
  16. package/plugins/opencode/skills/sprint-flow/AGENTS.md +2 -2
  17. package/plugins/opencode/skills/sprint-flow/references/orchestration-rules.md +10 -0
  18. package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +2 -2
  19. package/plugins/qoder/plugin.json +1 -1
  20. package/plugins/qoder/skills/delphi-review/AGENTS.md +2 -2
  21. package/plugins/qoder/skills/sprint-flow/AGENTS.md +2 -2
  22. package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +2 -2
  23. package/principles/AGENTS.md +2 -2
  24. package/skills/delphi-review/AGENTS.md +2 -2
  25. package/skills/sprint-flow/AGENTS.md +2 -2
  26. package/skills/sprint-flow/references/orchestration-rules.md +10 -0
  27. package/skills/test-specification-alignment/AGENTS.md +2 -2
package/bin/xp-gate.js CHANGED
@@ -203,6 +203,14 @@ const COMMANDS = {
203
203
  handlePhaseTransition(subargs).then(code => process.exit(code));
204
204
  },
205
205
  usage: 'xp-gate phase-transition <phase> <status> [--render] [--outputs <json>]'
206
+ },
207
+ 'sprint-audit': {
208
+ description: 'Final sprint completeness audit (Phase 6 CLOSE)',
209
+ run: subargs => {
210
+ const { handleSprintAudit } = require('../lib/sprint-audit.js');
211
+ handleSprintAudit(subargs).then(code => process.exit(code));
212
+ },
213
+ usage: 'xp-gate sprint-audit [--dir <path>] [--json]'
206
214
  }
207
215
  };
208
216
 
@@ -8,11 +8,11 @@
8
8
  * @covers AC-338-04 (--outputs flag records outputs in state)
9
9
  */
10
10
 
11
- import { describe, it, expect, beforeEach, afterEach } from 'vitest';
11
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
12
12
  import fs from 'node:fs';
13
13
  import path from 'node:path';
14
14
  import os from 'node:os';
15
- import { handlePhaseTransition, renderDashboard } from '../phase-transition.js';
15
+ import { handlePhaseTransition, renderDashboard, PHASE_NAMES } from '../phase-transition.js';
16
16
 
17
17
  describe('phase-transition', () => {
18
18
  let tmpDir;
@@ -171,4 +171,102 @@ describe('phase-transition', () => {
171
171
  expect(result).toContain('sprint-min');
172
172
  });
173
173
  });
174
+ describe('Layer 1: Pre-transition gate check', () => {
175
+ it('warns when previous phase not completed', async () => {
176
+ // Set up: Phase 1 completed, then try Phase 3 in_progress (skipping Phase 2)
177
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
178
+
179
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
180
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
181
+ expect(code).toBe(0);
182
+ expect(warnSpy).toHaveBeenCalledWith(
183
+ expect.stringContaining('Phase 2 (DESIGN) not recorded')
184
+ );
185
+ warnSpy.mockRestore();
186
+ });
187
+
188
+ it('does NOT warn when previous phase is completed', async () => {
189
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
190
+ await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
191
+
192
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
193
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
194
+ expect(code).toBe(0);
195
+ expect(warnSpy).not.toHaveBeenCalled();
196
+ warnSpy.mockRestore();
197
+ });
198
+
199
+ it('does NOT warn when previous phase is skipped', async () => {
200
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
201
+ await handlePhaseTransition(['2', 'skipped', '--dir', tmpDir]);
202
+
203
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
204
+ const code = await handlePhaseTransition(['3', 'in_progress', '--dir', tmpDir]);
205
+ expect(code).toBe(0);
206
+ expect(warnSpy).not.toHaveBeenCalled();
207
+ warnSpy.mockRestore();
208
+ });
209
+
210
+ it('skips check when no sprint-state.json exists', async () => {
211
+ // No state file — fresh project
212
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
213
+ const code = await handlePhaseTransition(['2', 'in_progress', '--dir', tmpDir]);
214
+ expect(code).toBe(0);
215
+ expect(warnSpy).not.toHaveBeenCalled();
216
+ warnSpy.mockRestore();
217
+ });
218
+
219
+ it('does NOT check for Phase 1 (no predecessor)', async () => {
220
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
221
+ const code = await handlePhaseTransition(['1', 'in_progress', '--dir', tmpDir]);
222
+ expect(code).toBe(0);
223
+ expect(warnSpy).not.toHaveBeenCalled();
224
+ warnSpy.mockRestore();
225
+ });
226
+
227
+ it('handles phase_history with gaps correctly', async () => {
228
+ // Set up: Phase 1 completed, Phase 3 completed (gap at Phase 2)
229
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
230
+ // Manually add Phase 3 without Phase 2
231
+ const stateFile = path.join(tmpDir, '.sprint-state', 'sprint-state.json');
232
+ const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
233
+ state.phase_history.push({ phase: 3, phase_name: 'BUILD', status: 'completed' });
234
+ state.phase = 3;
235
+ fs.writeFileSync(stateFile, JSON.stringify(state));
236
+
237
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
238
+ const code = await handlePhaseTransition(['4', 'in_progress', '--dir', tmpDir]);
239
+ expect(code).toBe(0);
240
+ // Phase 3 is completed, so no warning for Phase 3→4 transition
241
+ expect(warnSpy).not.toHaveBeenCalled();
242
+ warnSpy.mockRestore();
243
+ });
244
+ });
245
+
246
+ describe('Layer 1.5: Phase 6 auto-trigger', () => {
247
+ it('outputs sprint-audit reminder on Phase 6 completed', async () => {
248
+ // Set up state through Phase 5
249
+ await handlePhaseTransition(['1', 'completed', '--dir', tmpDir]);
250
+ await handlePhaseTransition(['2', 'completed', '--dir', tmpDir]);
251
+ await handlePhaseTransition(['3', 'completed', '--dir', tmpDir]);
252
+ await handlePhaseTransition(['4', 'completed', '--dir', tmpDir]);
253
+ await handlePhaseTransition(['5', 'completed', '--dir', tmpDir]);
254
+
255
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
256
+ const code = await handlePhaseTransition(['6', 'completed', '--dir', tmpDir]);
257
+ expect(code).toBe(0);
258
+ expect(logSpy).toHaveBeenCalledWith(
259
+ expect.stringContaining('sprint-audit')
260
+ );
261
+ logSpy.mockRestore();
262
+ });
263
+ });
264
+
265
+ describe('PHASE_NAMES export', () => {
266
+ it('exports PHASE_NAMES as shared constant', () => {
267
+ expect(PHASE_NAMES).toBeDefined();
268
+ expect(PHASE_NAMES[1]).toBe('PREP');
269
+ expect(PHASE_NAMES[6]).toBe('CLOSE');
270
+ });
271
+ });
174
272
  });
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Tests for sprint-audit CLI command (Layer 2)
3
+ * @test REQ-338 Sprint audit — final completeness check
4
+ * @intent Verify sprint-audit correctly checks phase coverage, artifacts, consistency
5
+ * @covers AC-AUDIT-01 (SKIP when no sprint-state.json)
6
+ * @covers AC-AUDIT-02 (PASS when 6/6 completed)
7
+ * @covers AC-AUDIT-03 (PASS_WITH_WARNINGS when 4-5/6 completed)
8
+ * @covers AC-AUDIT-04 (FAIL when <4/6 completed)
9
+ * @covers AC-AUDIT-05 (FAIL on state inconsistency)
10
+ * @covers AC-AUDIT-06 (--json flag outputs JSON)
11
+ * @covers AC-AUDIT-07 (report persisted to audit-report.json)
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import os from 'node:os';
18
+ import { handleSprintAudit } from '../sprint-audit.js';
19
+
20
+ describe('sprint-audit', () => {
21
+ let tmpDir;
22
+
23
+ beforeEach(() => {
24
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'sprint-audit-test-'));
25
+ });
26
+
27
+ afterEach(() => {
28
+ fs.rmSync(tmpDir, { recursive: true, force: true });
29
+ });
30
+
31
+ function writeState(state) {
32
+ const stateDir = path.join(tmpDir, '.sprint-state');
33
+ fs.mkdirSync(stateDir, { recursive: true });
34
+ fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), JSON.stringify(state));
35
+ }
36
+
37
+ function makeState(overrides = {}) {
38
+ return {
39
+ _schema_version: 1,
40
+ id: 'sprint-test-audit',
41
+ task_description: 'Test audit feature',
42
+ phase: 6,
43
+ status: 'completed',
44
+ started_at: '2026-07-22T10:00:00Z',
45
+ phase_history: [],
46
+ isolation: { branch: 'sprint/test-audit', worktree_path: null },
47
+ outputs: {},
48
+ ...overrides,
49
+ };
50
+ }
51
+
52
+ function makePhaseHistory(completedPhases) {
53
+ const phaseNames = ['PREP', 'DESIGN', 'BUILD', 'VERIFY', 'SHIP', 'CLOSE'];
54
+ return completedPhases.map((p, i) => ({
55
+ phase: p,
56
+ phase_name: phaseNames[p - 1],
57
+ status: 'completed',
58
+ started_at: `2026-07-22T10:${String(i).padStart(2, '0')}:00Z`,
59
+ completed_at: `2026-07-22T10:${String(i).padStart(2, '0')}:30Z`,
60
+ duration_seconds: 30,
61
+ }));
62
+ }
63
+
64
+ describe('handleSprintAudit()', () => {
65
+ it('returns 0 and shows help with --help', async () => {
66
+ const code = await handleSprintAudit(['--help']);
67
+ expect(code).toBe(0);
68
+ });
69
+
70
+ it('returns SKIP (exit 0) when no sprint-state.json exists', async () => {
71
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
72
+ const code = await handleSprintAudit(['--dir', tmpDir]);
73
+ expect(code).toBe(0);
74
+ expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('No sprint-state.json'));
75
+ logSpy.mockRestore();
76
+ });
77
+
78
+ it('returns PASS when 6/6 phases completed', async () => {
79
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
80
+
81
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
82
+ const code = await handleSprintAudit(['--dir', tmpDir]);
83
+ expect(code).toBe(0);
84
+ // Check output contains PASS
85
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
86
+ expect(output).toContain('PASS');
87
+ expect(output).toContain('6/6');
88
+ logSpy.mockRestore();
89
+ });
90
+
91
+ it('returns PASS_WITH_WARNINGS when 5/6 completed', async () => {
92
+ writeState(makeState({ phase: 5, phase_history: makePhaseHistory([1, 2, 3, 4, 5]) }));
93
+
94
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
95
+ const code = await handleSprintAudit(['--dir', tmpDir]);
96
+ expect(code).toBe(0);
97
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
98
+ expect(output).toContain('PASS_WITH_WARNINGS');
99
+ logSpy.mockRestore();
100
+ });
101
+
102
+ it('returns PASS_WITH_WARNINGS when 4/6 completed', async () => {
103
+ writeState(makeState({ phase: 4, phase_history: makePhaseHistory([1, 2, 3, 4]) }));
104
+
105
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
106
+ const code = await handleSprintAudit(['--dir', tmpDir]);
107
+ expect(code).toBe(0);
108
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
109
+ expect(output).toContain('PASS_WITH_WARNINGS');
110
+ logSpy.mockRestore();
111
+ });
112
+
113
+ it('returns FAIL when <4/6 completed', async () => {
114
+ writeState(makeState({ phase: 3, phase_history: makePhaseHistory([1, 2, 3]) }));
115
+
116
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
117
+ const code = await handleSprintAudit(['--dir', tmpDir]);
118
+ expect(code).toBe(1);
119
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
120
+ expect(output).toContain('FAIL');
121
+ logSpy.mockRestore();
122
+ });
123
+
124
+ it('returns FAIL on state inconsistency (phase vs phase_history)', async () => {
125
+ const history = makePhaseHistory([1, 2, 3, 4, 5, 6]);
126
+ writeState(makeState({ phase: 3, phase_history: history })); // phase=3 but last entry is 6
127
+
128
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
129
+ const code = await handleSprintAudit(['--dir', tmpDir]);
130
+ expect(code).toBe(1);
131
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
132
+ expect(output).toContain('State inconsistency');
133
+ logSpy.mockRestore();
134
+ });
135
+
136
+ it('outputs JSON when --json flag is set', async () => {
137
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
138
+
139
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
140
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
141
+ expect(code).toBe(0);
142
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
143
+ const result = JSON.parse(output);
144
+ expect(result.verdict).toBe('PASS');
145
+ expect(result.coverage.completed).toBe(6);
146
+ logSpy.mockRestore();
147
+ });
148
+
149
+ it('counts skipped phases as completed in coverage', async () => {
150
+ const history = makePhaseHistory([1, 2, 3, 4, 5]);
151
+ history.push({
152
+ phase: 6, phase_name: 'CLOSE', status: 'skipped',
153
+ started_at: '2026-07-22T10:05:00Z', completed_at: '2026-07-22T10:05:00Z', duration_seconds: 0,
154
+ });
155
+ writeState(makeState({ phase: 6, phase_history: history }));
156
+
157
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
158
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
159
+ expect(code).toBe(0);
160
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
161
+ const result = JSON.parse(output);
162
+ expect(result.coverage.completed).toBe(6);
163
+ expect(result.verdict).toBe('PASS');
164
+ logSpy.mockRestore();
165
+ });
166
+
167
+ it('warns about missing summary files for completed phases', async () => {
168
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
169
+ // No phase-outputs directory → all summaries missing
170
+
171
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
172
+ const code = await handleSprintAudit(['--dir', tmpDir, '--json']);
173
+ expect(code).toBe(0);
174
+ const output = logSpy.mock.calls.map(c => c[0]).join('\n');
175
+ const result = JSON.parse(output);
176
+ expect(result.warnings.length).toBeGreaterThan(0);
177
+ expect(result.warnings.some(w => w.includes('summary missing'))).toBe(true);
178
+ logSpy.mockRestore();
179
+ });
180
+
181
+ it('persists audit report to audit-report.json', async () => {
182
+ writeState(makeState({ phase_history: makePhaseHistory([1, 2, 3, 4, 5, 6]) }));
183
+
184
+ const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
185
+ await handleSprintAudit(['--dir', tmpDir]);
186
+ logSpy.mockRestore();
187
+
188
+ const reportPath = path.join(tmpDir, '.sprint-state', 'audit-report.json');
189
+ expect(fs.existsSync(reportPath)).toBe(true);
190
+ const report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
191
+ expect(report.verdict).toBe('PASS');
192
+ expect(report.sprint_id).toBe('sprint-test-audit');
193
+ });
194
+
195
+ it('handles corrupted sprint-state.json gracefully', async () => {
196
+ const stateDir = path.join(tmpDir, '.sprint-state');
197
+ fs.mkdirSync(stateDir, { recursive: true });
198
+ fs.writeFileSync(path.join(stateDir, 'sprint-state.json'), 'not-valid-json');
199
+
200
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
201
+ const code = await handleSprintAudit(['--dir', tmpDir]);
202
+ expect(code).toBe(1);
203
+ expect(errSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to parse'));
204
+ errSpy.mockRestore();
205
+ });
206
+ });
207
+ });
@@ -23,8 +23,18 @@
23
23
  'use strict';
24
24
 
25
25
  const path = require('path');
26
+ const fs = require('fs');
26
27
  const { SprintStateManager } = require('./sprint-state-manager');
27
28
 
29
+ /**
30
+ * Shared phase name lookup — single source of truth.
31
+ * Used by renderDashboard, sprint-audit, and Layer 1 gate check.
32
+ */
33
+ const PHASE_NAMES = {
34
+ 1: 'PREP', 2: 'DESIGN', 3: 'BUILD',
35
+ 4: 'VERIFY', 5: 'SHIP', 6: 'CLOSE',
36
+ };
37
+
28
38
  /**
29
39
  * Render ASCII dashboard from sprint state.
30
40
  * Reuses the formatSprintTable logic from sprint-status.
@@ -34,10 +44,7 @@ const { SprintStateManager } = require('./sprint-state-manager');
34
44
  function renderDashboard(state) {
35
45
  if (!state) return 'No sprint state found';
36
46
 
37
- const PHASE_NAMES = {
38
- '1': 'PREP', '2': 'DESIGN', '3': 'BUILD',
39
- '4': 'VERIFY', '5': 'SHIP', '6': 'CLOSE',
40
- };
47
+ const PHASE_NAMES_LOCAL = PHASE_NAMES;
41
48
 
42
49
  function statusIcon(status) {
43
50
  switch (status) {
@@ -75,7 +82,7 @@ function renderDashboard(state) {
75
82
  }
76
83
 
77
84
  const currentPhase = state.phase || 1;
78
- const currentPhaseName = PHASE_NAMES[String(currentPhase)] || `Phase ${currentPhase}`;
85
+ const currentPhaseName = PHASE_NAMES_LOCAL[String(currentPhase)] || `Phase ${currentPhase}`;
79
86
 
80
87
  // Progress bar
81
88
  const completedCount = Object.values(historyLookup).filter(h => h.status === 'completed').length;
@@ -99,7 +106,7 @@ function renderDashboard(state) {
99
106
  for (let i = 1; i <= 6; i++) {
100
107
  const entry = historyLookup[String(i)];
101
108
  const icon = statusIcon(entry?.status || (i < currentPhase ? 'completed' : i === currentPhase ? 'in_progress' : 'pending'));
102
- const name = (PHASE_NAMES[String(i)] || `Phase ${i}`).padEnd(10);
109
+ const name = (PHASE_NAMES_LOCAL[String(i)] || `Phase ${i}`).padEnd(10);
103
110
  const dur = formatDuration(entry?.duration_seconds).padEnd(8);
104
111
  lines.push(`| ${icon} Phase ${i}/6 ${name} ${dur} |`);
105
112
  }
@@ -190,10 +197,41 @@ async function handlePhaseTransition(args = []) {
190
197
  // Execute transition
191
198
  try {
192
199
  const manager = new SprintStateManager(projectDir);
200
+
201
+ // ── Layer 1: Pre-transition gate check ──
202
+ // Check BEFORE transitionPhase() to validate previous phase completion.
203
+ // Runs when status === 'in_progress' and phase >= 2.
204
+ // No side effects — only outputs WARNING, does not modify state.
205
+ if (status === 'in_progress' && phase >= 2) {
206
+ const preState = manager.read();
207
+ if (preState && Array.isArray(preState.phase_history)) {
208
+ const prevPhase = phase - 1;
209
+ const prevEntry = preState.phase_history.find(e => e.phase === prevPhase);
210
+ const prevName = PHASE_NAMES[prevPhase] || `Phase ${prevPhase}`;
211
+ // 'completed' and 'skipped' are both valid predecessor statuses
212
+ if (!prevEntry || (prevEntry.status !== 'completed' && prevEntry.status !== 'skipped')) {
213
+ console.warn(
214
+ `⚠️ [sprint-audit] Phase ${prevPhase} (${prevName}) not recorded as 'completed'`
215
+ );
216
+ console.warn(
217
+ ` Previous phase may have been skipped. Run: xp-gate phase-transition ${prevPhase} completed`
218
+ );
219
+ }
220
+ }
221
+ // If no sprint-state.json exists yet, skip check (no previous phase to validate)
222
+ }
223
+
193
224
  const newState = manager.transitionPhase(phase, status, { outputs });
194
225
 
195
226
  console.log(`✅ Phase ${phase} transitioned to '${status}'`);
196
227
 
228
+ // ── Layer 1.5: Auto-trigger Layer 2 reminder on Phase 6 completed ──
229
+ if (phase === 6 && status === 'completed') {
230
+ console.log('');
231
+ console.log('📋 [sprint-audit] Sprint complete — run final coverage audit:');
232
+ console.log(' npx xp-gate sprint-audit');
233
+ }
234
+
197
235
  // Auto-render dashboard if --render flag is set
198
236
  if (renderFlag) {
199
237
  console.log('');
@@ -208,4 +246,4 @@ async function handlePhaseTransition(args = []) {
208
246
  }
209
247
  }
210
248
 
211
- module.exports = { handlePhaseTransition, renderDashboard };
249
+ module.exports = { handlePhaseTransition, renderDashboard, PHASE_NAMES };
@@ -0,0 +1,235 @@
1
+ /**
2
+ * xp-gate sprint-audit — Layer 2: Final sprint completeness audit.
3
+ *
4
+ * Checks phase_history coverage, time records, artifact records,
5
+ * and state consistency at Phase 6 CLOSE.
6
+ *
7
+ * Usage:
8
+ * xp-gate sprint-audit [--dir <path>] [--json]
9
+ *
10
+ * Verdict rules:
11
+ * 6/6 completed + 0 errors → PASS (exit 0)
12
+ * >= 4/6 completed + 0 errors → PASS_WITH_WARNINGS (exit 0)
13
+ * < 4/6 completed or any errors → FAIL (exit 1)
14
+ * No sprint-state.json → SKIP (exit 0)
15
+ *
16
+ * @module sprint-audit
17
+ */
18
+
19
+ 'use strict';
20
+
21
+ const fs = require('fs');
22
+ const path = require('path');
23
+ const { PHASE_NAMES } = require('./phase-transition');
24
+
25
+ /**
26
+ * Run the sprint audit.
27
+ * @param {string[]} args - CLI arguments
28
+ * @returns {Promise<number>} Exit code
29
+ */
30
+ async function handleSprintAudit(args = []) {
31
+ if (args.includes('--help')) {
32
+ console.log('Usage: xp-gate sprint-audit [options]');
33
+ console.log('');
34
+ console.log('Options:');
35
+ console.log(' --dir <path> Project directory (default: cwd)');
36
+ console.log(' --json Output as JSON instead of human-readable');
37
+ console.log('');
38
+ console.log('Runs final sprint completeness audit at Phase 6 CLOSE.');
39
+ console.log('Checks phase coverage, time records, artifact records,');
40
+ console.log('and state consistency.');
41
+ return 0;
42
+ }
43
+
44
+ const dirIdx = args.indexOf('--dir');
45
+ const projectDir = dirIdx >= 0 ? args[dirIdx + 1] : process.cwd();
46
+ const jsonFlag = args.includes('--json');
47
+
48
+ const stateFile = path.join(projectDir, '.sprint-state', 'sprint-state.json');
49
+
50
+ // No sprint-state.json → SKIP
51
+ if (!fs.existsSync(stateFile)) {
52
+ if (jsonFlag) {
53
+ console.log(JSON.stringify({ verdict: 'SKIP', reason: 'No sprint-state.json found' }));
54
+ } else {
55
+ console.log('⏭️ No sprint-state.json found — skipping audit.');
56
+ }
57
+ return 0;
58
+ }
59
+
60
+ let state;
61
+ try {
62
+ state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
63
+ } catch (err) {
64
+ const result = {
65
+ verdict: 'FAIL',
66
+ errors: [`Failed to parse sprint-state.json: ${err.message}`],
67
+ };
68
+ if (jsonFlag) {
69
+ console.log(JSON.stringify(result, null, 2));
70
+ } else {
71
+ console.error(`❌ ${result.errors[0]}`);
72
+ }
73
+ return 1;
74
+ }
75
+
76
+ // ── Run checks ──
77
+ const warnings = [];
78
+ const errors = [];
79
+ const phaseHistory = Array.isArray(state.phase_history) ? state.phase_history : [];
80
+
81
+ // Build lookup
82
+ const historyLookup = {};
83
+ for (const entry of phaseHistory) {
84
+ historyLookup[entry.phase] = entry;
85
+ }
86
+
87
+ // Check 1: Phase coverage
88
+ const completedPhases = [];
89
+ const missingPhases = [];
90
+ const incompletePhases = [];
91
+
92
+ for (let i = 1; i <= 6; i++) {
93
+ const entry = historyLookup[i];
94
+ if (!entry) {
95
+ missingPhases.push(i);
96
+ } else if (entry.status === 'completed' || entry.status === 'skipped') {
97
+ completedPhases.push(i);
98
+ } else {
99
+ incompletePhases.push({ phase: i, name: PHASE_NAMES[i] || `Phase ${i}`, status: entry.status });
100
+ }
101
+ }
102
+
103
+ // Check 2: Time records for completed phases
104
+ for (const phaseNum of completedPhases) {
105
+ const entry = historyLookup[phaseNum];
106
+ if (entry.status === 'completed' && (entry.duration_seconds == null || entry.duration_seconds === 0)) {
107
+ warnings.push(`Phase ${phaseNum} (${PHASE_NAMES[phaseNum]}) completed but has no duration recorded`);
108
+ }
109
+ }
110
+
111
+ // Check 3: Artifact records (phase-{N}-summary.md)
112
+ const outputsDir = path.join(projectDir, '.sprint-state', 'phase-outputs');
113
+ for (const phaseNum of completedPhases) {
114
+ if (historyLookup[phaseNum].status === 'completed') {
115
+ const summaryFile = path.join(outputsDir, `phase-${phaseNum}-summary.md`);
116
+ if (!fs.existsSync(summaryFile)) {
117
+ warnings.push(`Phase ${phaseNum} summary missing: .sprint-state/phase-outputs/phase-${phaseNum}-summary.md`);
118
+ }
119
+ }
120
+ }
121
+
122
+ // Check 4: State consistency — phase field vs phase_history last entry
123
+ if (phaseHistory.length > 0) {
124
+ const lastEntry = phaseHistory[phaseHistory.length - 1];
125
+ if (state.phase !== undefined && state.phase !== lastEntry.phase) {
126
+ errors.push(
127
+ `State inconsistency: sprint-state.phase=${state.phase} but phase_history last entry is phase ${lastEntry.phase}`
128
+ );
129
+ }
130
+ }
131
+
132
+ // ── Determine verdict ──
133
+ const completedCount = completedPhases.length;
134
+ let verdict;
135
+ if (errors.length > 0 || completedCount < 4) {
136
+ verdict = 'FAIL';
137
+ } else if (completedCount >= 6 && errors.length === 0) {
138
+ verdict = 'PASS';
139
+ } else {
140
+ verdict = 'PASS_WITH_WARNINGS';
141
+ }
142
+
143
+ // ── Build result ──
144
+ const result = {
145
+ sprint_id: state.id || 'unknown',
146
+ branch: state.isolation?.branch || 'unknown',
147
+ coverage: {
148
+ completed: completedCount,
149
+ total: 6,
150
+ pct: Math.round((completedCount / 6) * 100),
151
+ },
152
+ missing_phases: missingPhases.map(p => ({ phase: p, name: PHASE_NAMES[p] || `Phase ${p}` })),
153
+ incomplete_phases: incompletePhases,
154
+ warnings,
155
+ errors,
156
+ verdict,
157
+ };
158
+
159
+ // ── Persist report (latest overwrites — expected behavior) ──
160
+ const reportPath = path.join(projectDir, '.sprint-state', 'audit-report.json');
161
+ try {
162
+ fs.writeFileSync(reportPath, JSON.stringify(result, null, 2), 'utf8');
163
+ } catch (err) {
164
+ // Report write failure is WARNING, not BLOCK
165
+ warnings.push(`Failed to write audit report: ${err.message}`);
166
+ }
167
+
168
+ // ── Output ──
169
+ if (jsonFlag) {
170
+ console.log(JSON.stringify(result, null, 2));
171
+ } else {
172
+ printHumanReadable(result, state);
173
+ }
174
+
175
+ // Exit code: FAIL → 1, others → 0
176
+ return verdict === 'FAIL' ? 1 : 0;
177
+ }
178
+
179
+ /**
180
+ * Print human-readable audit report.
181
+ * @param {object} result - Audit result object
182
+ * @param {object} state - Sprint state object
183
+ */
184
+ function printHumanReadable(result, state) {
185
+ const lines = [];
186
+ lines.push('');
187
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
188
+ lines.push(` Sprint Audit Report — ${result.sprint_id}`);
189
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
190
+ lines.push('');
191
+ lines.push(` Branch: ${result.branch}`);
192
+ lines.push(` Phase Coverage: ${result.coverage.completed}/${result.coverage.total} (${result.coverage.pct}%)`);
193
+ lines.push('');
194
+ lines.push(' Phase History:');
195
+
196
+ for (let i = 1; i <= 6; i++) {
197
+ const entry = result.incomplete_phases.find(p => p.phase === i);
198
+ const missing = result.missing_phases.find(p => p.phase === i);
199
+ const name = (PHASE_NAMES[i] || `Phase ${i}`).padEnd(10);
200
+
201
+ if (missing) {
202
+ lines.push(` ❌ Phase ${i}/6 ${name} — not recorded`);
203
+ } else if (entry) {
204
+ lines.push(` ⚠️ Phase ${i}/6 ${name} ${entry.status} ← not yet completed`);
205
+ } else {
206
+ lines.push(` ✅ Phase ${i}/6 ${name} completed`);
207
+ }
208
+ }
209
+
210
+ if (result.warnings.length > 0) {
211
+ lines.push('');
212
+ lines.push(' Warnings:');
213
+ for (const w of result.warnings) {
214
+ lines.push(` ⚠️ ${w}`);
215
+ }
216
+ }
217
+
218
+ if (result.errors.length > 0) {
219
+ lines.push('');
220
+ lines.push(' Errors:');
221
+ for (const e of result.errors) {
222
+ lines.push(` ❌ ${e}`);
223
+ }
224
+ }
225
+
226
+ lines.push('');
227
+ const issueCount = result.warnings.length + result.errors.length;
228
+ lines.push(` Overall: ${result.warnings.length} warning(s), ${result.errors.length} error(s)`);
229
+ lines.push(` Verdict: ${result.verdict}`);
230
+ lines.push('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
231
+
232
+ console.log(lines.join('\n'));
233
+ }
234
+
235
+ module.exports = { handleSprintAudit };
@@ -1,9 +1,9 @@
1
1
  # SRC/MOCK-POLICY KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Mock layering policy enforcement — Gate M3 of pre-push hook. Ensures integration tests use real implementations for internal dependencies, mock external dependencies, and annotate pending mocks with removal plans. Combines project scope scanning, mock decision engine, and per-file validation into a single pipeline.
@@ -1,9 +1,9 @@
1
1
  # SRC/MUTATION KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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": "@boyingliu01/xp-gate",
3
- "version": "0.14.28",
3
+ "version": "0.14.30",
4
4
  "description": "AI-driven development workflow: 6 quality gates + Delphi review + Sprint Flow",
5
5
  "bin": {
6
6
  "xp-gate": "./bin/xp-gate.js"
@@ -39,7 +39,7 @@
39
39
  "node": ">=18.0.0"
40
40
  },
41
41
  "scripts": {
42
- "version": "0.14.28",
42
+ "version": "0.14.30",
43
43
  "prepack": "node scripts/sync-package-content.js",
44
44
  "test": "echo \"Run tests\" && exit 0"
45
45
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xp-gate",
3
- "version": "0.14.28",
3
+ "version": "0.14.30",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Claude Code. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 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-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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.
@@ -103,6 +103,16 @@
103
103
 
104
104
  3. **等待用户确认 checkpoint**(如适用)
105
105
 
106
+ 4. **Phase 6 CLOSE 完成后运行 sprint-audit**(自动提醒,Layer 1.5 触发):
107
+ ```
108
+ npx xp-gate sprint-audit
109
+ ```
110
+ - 检查 phase 覆盖度、时间记录、输出物记录、状态一致性
111
+ - 输出人类可读报告或 JSON(`--json`)
112
+ - 报告写入 `.sprint-state/audit-report.json`(最新覆盖)
113
+ - Verdict: PASS / PASS_WITH_WARNINGS / FAIL / SKIP
114
+ - `phase-transition` CLI 在 Phase 6 completed 时自动输出提醒,无需额外指令
115
+
106
116
  ### Background Task Resume Protocol (MANDATORY — Issue #248)
107
117
 
108
118
  After dispatching background agents with `run_in_background=true`:
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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": "@boyingliu01/opencode-plugin",
3
- "version": "0.14.28",
3
+ "version": "0.14.30",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "description": "XP-Gate quality gates + AI workflow skills + Sprint Flow TUI sidebar for OpenCode",
@@ -1,9 +1,9 @@
1
1
  # SKILLS/DELPHI-REVIEW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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.
@@ -103,6 +103,16 @@
103
103
 
104
104
  3. **等待用户确认 checkpoint**(如适用)
105
105
 
106
+ 4. **Phase 6 CLOSE 完成后运行 sprint-audit**(自动提醒,Layer 1.5 触发):
107
+ ```
108
+ npx xp-gate sprint-audit
109
+ ```
110
+ - 检查 phase 覆盖度、时间记录、输出物记录、状态一致性
111
+ - 输出人类可读报告或 JSON(`--json`)
112
+ - 报告写入 `.sprint-state/audit-report.json`(最新覆盖)
113
+ - Verdict: PASS / PASS_WITH_WARNINGS / FAIL / SKIP
114
+ - `phase-transition` CLI 在 Phase 6 completed 时自动输出提醒,无需额外指令
115
+
106
116
  ### Background Task Resume Protocol (MANDATORY — Issue #248)
107
117
 
108
118
  After dispatching background agents with `run_in_background=true`:
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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.14.28",
3
+ "version": "0.14.30",
4
4
  "displayName": "XP-Gate",
5
5
  "description": "Extreme Programming quality gates + AI workflow skills for Qoder. Includes 10 quality gates (Gate 0-9), Sprint Flow (11 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-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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
  # PRINCIPLES CHECKER MODULE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Delphi Consensus Review — multi-round anonymous expert review (≥90% threshold, 3 experts from ≥2 providers, domestic models only). Supports design + code-walkthrough modes.
@@ -1,9 +1,9 @@
1
1
  # SKILLS/SPRINT-FLOW KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.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.
@@ -103,6 +103,16 @@
103
103
 
104
104
  3. **等待用户确认 checkpoint**(如适用)
105
105
 
106
+ 4. **Phase 6 CLOSE 完成后运行 sprint-audit**(自动提醒,Layer 1.5 触发):
107
+ ```
108
+ npx xp-gate sprint-audit
109
+ ```
110
+ - 检查 phase 覆盖度、时间记录、输出物记录、状态一致性
111
+ - 输出人类可读报告或 JSON(`--json`)
112
+ - 报告写入 `.sprint-state/audit-report.json`(最新覆盖)
113
+ - Verdict: PASS / PASS_WITH_WARNINGS / FAIL / SKIP
114
+ - `phase-transition` CLI 在 Phase 6 completed 时自动输出提醒,无需额外指令
115
+
106
116
  ### Background Task Resume Protocol (MANDATORY — Issue #248)
107
117
 
108
118
  After dispatching background agents with `run_in_background=true`:
@@ -1,9 +1,9 @@
1
1
  # SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
2
2
 
3
3
  **Generated:** 2026-07-22
4
- **Commit:** a39f4e7
4
+ **Commit:** a465b02
5
5
  **Branch:** main
6
- **Version:** 0.14.28.0
6
+ **Version:** 0.14.30.0
7
7
 
8
8
  ## OVERVIEW
9
9
  Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.