@boyingliu01/xp-gate 0.14.7 → 0.14.9
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 +31 -2
- package/lib/__tests__/next-sprint.test.js +231 -0
- package/lib/__tests__/update-hooks.test.js +12 -0
- package/lib/__tests__/upgrade-skill-sync.test.js +65 -0
- package/lib/check.js +115 -20
- package/lib/doctor.js +63 -0
- package/lib/gate-runner.js +209 -0
- package/lib/next-sprint.js +129 -0
- package/lib/shared-utils.js +1 -1
- package/lib/sprint-status.js +3 -1
- package/lib/update-hooks.js +1 -1
- package/lib/upgrade.js +8 -0
- package/mock-policy/AGENTS.md +4 -4
- package/mock-policy/__tests__/gate-m3.test.ts +384 -0
- package/mock-policy/__tests__/schema.test.ts +239 -0
- package/mutation/AGENTS.md +4 -4
- package/mutation/__tests__/init-baseline.test.ts +347 -0
- package/mutation/__tests__/mutmut-runner.test.ts +561 -0
- package/mutation/__tests__/runners-index.test.ts +134 -0
- package/mutation/__tests__/stryker-runner.test.ts +455 -0
- package/mutation/__tests__/update-baseline.test.ts +393 -0
- package/package.json +1 -1
- package/plugins/claude-code/.claude-plugin/plugin.json +1 -1
- package/plugins/claude-code/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/claude-code/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/claude-code/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/claude-code/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/opencode/package.json +1 -1
- package/plugins/opencode/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/opencode/skills/sprint-flow/SKILL.md +1 -1
- package/plugins/opencode/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/plugins/opencode/skills/test-specification-alignment/AGENTS.md +4 -4
- package/plugins/qoder/plugin.json +1 -1
- package/plugins/qoder/skills/delphi-review/AGENTS.md +4 -4
- package/plugins/qoder/skills/sprint-flow/AGENTS.md +4 -4
- package/plugins/qoder/skills/test-specification-alignment/AGENTS.md +4 -4
- package/principles/AGENTS.md +4 -4
- package/skills/delphi-review/AGENTS.md +4 -4
- package/skills/sprint-flow/AGENTS.md +4 -4
- package/skills/sprint-flow/SKILL.md +1 -1
- package/skills/sprint-flow/references/phase-2-design.md +34 -8
- package/skills/test-specification-alignment/AGENTS.md +4 -4
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @test update-baseline.ts - Mutation baseline update CLI
|
|
3
|
+
* @intent Verify parseArgs, updateBaseline, removeDeletedFiles, and readBaseline
|
|
4
|
+
* work correctly through the module's side effects
|
|
5
|
+
* @covers REQ-MUT-001 AC-004 (baseline update from mutation scores)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
9
|
+
|
|
10
|
+
vi.mock('fs/promises', () => ({
|
|
11
|
+
default: {
|
|
12
|
+
readFile: vi.fn(),
|
|
13
|
+
writeFile: vi.fn(),
|
|
14
|
+
access: vi.fn()
|
|
15
|
+
},
|
|
16
|
+
readFile: vi.fn(),
|
|
17
|
+
writeFile: vi.fn(),
|
|
18
|
+
access: vi.fn()
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
const mockFs = await import('fs/promises');
|
|
22
|
+
|
|
23
|
+
interface MockBaseline {
|
|
24
|
+
version: string;
|
|
25
|
+
generatedAt: string;
|
|
26
|
+
source: 'local' | 'ci';
|
|
27
|
+
scores: Record<string, { score: number; mutants: number; killed: number; survived: number }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function makeBaseline(overrides: Partial<MockBaseline> = {}): MockBaseline {
|
|
31
|
+
return {
|
|
32
|
+
version: '1.0.0',
|
|
33
|
+
generatedAt: '2024-01-01T00:00:00.000Z',
|
|
34
|
+
source: 'local',
|
|
35
|
+
scores: {},
|
|
36
|
+
...overrides
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('update-baseline.ts - Mutation Baseline Update CLI', () => {
|
|
41
|
+
let originalArgv: string[];
|
|
42
|
+
let originalExit: typeof process.exit;
|
|
43
|
+
let originalCwd: typeof process.cwd;
|
|
44
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
45
|
+
let consoleLogSpy: any;
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
47
|
+
let consoleErrorSpy: any;
|
|
48
|
+
let exitCode: number | undefined;
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
originalArgv = process.argv;
|
|
52
|
+
originalExit = process.exit;
|
|
53
|
+
originalCwd = process.cwd;
|
|
54
|
+
|
|
55
|
+
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
56
|
+
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
57
|
+
|
|
58
|
+
process.exit = ((code?: number) => {
|
|
59
|
+
exitCode = code;
|
|
60
|
+
return undefined as never;
|
|
61
|
+
}) as typeof process.exit;
|
|
62
|
+
|
|
63
|
+
process.cwd = () => '/fake/project';
|
|
64
|
+
|
|
65
|
+
vi.resetModules();
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
exitCode = undefined;
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
process.argv = originalArgv;
|
|
72
|
+
process.exit = originalExit;
|
|
73
|
+
process.cwd = originalCwd;
|
|
74
|
+
consoleLogSpy.mockRestore();
|
|
75
|
+
consoleErrorSpy.mockRestore();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
async function importModule(): Promise<void> {
|
|
79
|
+
await import('../update-baseline');
|
|
80
|
+
// Allow the main() promise chain to settle
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('parseArgs', () => {
|
|
85
|
+
it('should parse valid --scores argument with single file', async () => {
|
|
86
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:75.5'];
|
|
87
|
+
const baseline = makeBaseline();
|
|
88
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
89
|
+
JSON.stringify(baseline)
|
|
90
|
+
);
|
|
91
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
92
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
93
|
+
|
|
94
|
+
await importModule();
|
|
95
|
+
|
|
96
|
+
expect(exitCode).toBeUndefined();
|
|
97
|
+
expect(mockFs.default.writeFile).toHaveBeenCalled();
|
|
98
|
+
const written = JSON.parse(
|
|
99
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
100
|
+
);
|
|
101
|
+
expect(written.scores['src/foo.ts'].score).toBe(75.5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should parse multiple comma-separated scores', async () => {
|
|
105
|
+
process.argv = [
|
|
106
|
+
'node',
|
|
107
|
+
'update-baseline.js',
|
|
108
|
+
'--scores',
|
|
109
|
+
'src/a.ts:80.0,src/b.ts:65.3,src/c.ts:92.7'
|
|
110
|
+
];
|
|
111
|
+
const baseline = makeBaseline();
|
|
112
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
113
|
+
JSON.stringify(baseline)
|
|
114
|
+
);
|
|
115
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
116
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
117
|
+
|
|
118
|
+
await importModule();
|
|
119
|
+
|
|
120
|
+
const written = JSON.parse(
|
|
121
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
122
|
+
);
|
|
123
|
+
expect(Object.keys(written.scores)).toHaveLength(3);
|
|
124
|
+
expect(written.scores['src/a.ts'].score).toBe(80.0);
|
|
125
|
+
expect(written.scores['src/b.ts'].score).toBe(65.3);
|
|
126
|
+
expect(written.scores['src/c.ts'].score).toBe(92.7);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should throw and exit 1 when --scores is missing', async () => {
|
|
130
|
+
process.argv = ['node', 'update-baseline.js'];
|
|
131
|
+
|
|
132
|
+
await importModule();
|
|
133
|
+
|
|
134
|
+
expect(exitCode).toBe(1);
|
|
135
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
136
|
+
expect.stringContaining('Missing required argument: --scores')
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should throw and exit 1 when score format is invalid (missing colon)', async () => {
|
|
141
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts'];
|
|
142
|
+
|
|
143
|
+
await importModule();
|
|
144
|
+
|
|
145
|
+
expect(exitCode).toBe(1);
|
|
146
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
147
|
+
expect.stringContaining('Invalid score format')
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should throw and exit 1 when score value is NaN', async () => {
|
|
152
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:notanumber'];
|
|
153
|
+
|
|
154
|
+
await importModule();
|
|
155
|
+
|
|
156
|
+
expect(exitCode).toBe(1);
|
|
157
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
158
|
+
expect.stringContaining('Invalid score value')
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should throw and exit 1 when score is out of range (>100)', async () => {
|
|
163
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:150'];
|
|
164
|
+
|
|
165
|
+
await importModule();
|
|
166
|
+
|
|
167
|
+
expect(exitCode).toBe(1);
|
|
168
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
169
|
+
expect.stringContaining('Invalid score value')
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should throw and exit 1 when score is negative', async () => {
|
|
174
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:-5'];
|
|
175
|
+
|
|
176
|
+
await importModule();
|
|
177
|
+
|
|
178
|
+
expect(exitCode).toBe(1);
|
|
179
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
180
|
+
expect.stringContaining('Invalid score value')
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('updateBaseline (via module side effects)', () => {
|
|
186
|
+
it('should merge new scores into existing baseline entries', async () => {
|
|
187
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/existing.ts:88.5'];
|
|
188
|
+
const baseline = makeBaseline({
|
|
189
|
+
scores: {
|
|
190
|
+
'src/existing.ts': { score: 50.0, mutants: 10, killed: 5, survived: 5 }
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
194
|
+
JSON.stringify(baseline)
|
|
195
|
+
);
|
|
196
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
197
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
198
|
+
|
|
199
|
+
await importModule();
|
|
200
|
+
|
|
201
|
+
const written = JSON.parse(
|
|
202
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
203
|
+
);
|
|
204
|
+
// Score should be updated
|
|
205
|
+
expect(written.scores['src/existing.ts'].score).toBe(88.5);
|
|
206
|
+
// Existing metadata should be preserved
|
|
207
|
+
expect(written.scores['src/existing.ts'].mutants).toBe(10);
|
|
208
|
+
expect(written.scores['src/existing.ts'].killed).toBe(5);
|
|
209
|
+
expect(written.scores['src/existing.ts'].survived).toBe(5);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('should add new file entries with zeroed metadata', async () => {
|
|
213
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/new.ts:72.0'];
|
|
214
|
+
const baseline = makeBaseline({
|
|
215
|
+
scores: {
|
|
216
|
+
'src/old.ts': { score: 60.0, mutants: 8, killed: 4, survived: 4 }
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
220
|
+
JSON.stringify(baseline)
|
|
221
|
+
);
|
|
222
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
223
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
224
|
+
|
|
225
|
+
await importModule();
|
|
226
|
+
|
|
227
|
+
const written = JSON.parse(
|
|
228
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
229
|
+
);
|
|
230
|
+
expect(written.scores['src/new.ts']).toEqual({
|
|
231
|
+
score: 72.0,
|
|
232
|
+
mutants: 0,
|
|
233
|
+
killed: 0,
|
|
234
|
+
survived: 0
|
|
235
|
+
});
|
|
236
|
+
// Old entry should still be present
|
|
237
|
+
expect(written.scores['src/old.ts'].score).toBe(60.0);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should update generatedAt timestamp', async () => {
|
|
241
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:50.0'];
|
|
242
|
+
const baseline = makeBaseline({ generatedAt: '2020-01-01T00:00:00.000Z' });
|
|
243
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
244
|
+
JSON.stringify(baseline)
|
|
245
|
+
);
|
|
246
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
247
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
248
|
+
|
|
249
|
+
await importModule();
|
|
250
|
+
|
|
251
|
+
const written = JSON.parse(
|
|
252
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
253
|
+
);
|
|
254
|
+
expect(written.generatedAt).not.toBe('2020-01-01T00:00:00.000Z');
|
|
255
|
+
// Should be a valid ISO date string
|
|
256
|
+
expect(new Date(written.generatedAt).toISOString()).toBe(written.generatedAt);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('should round score to 1 decimal place', async () => {
|
|
260
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:66.66666'];
|
|
261
|
+
const baseline = makeBaseline();
|
|
262
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
263
|
+
JSON.stringify(baseline)
|
|
264
|
+
);
|
|
265
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
266
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
267
|
+
|
|
268
|
+
await importModule();
|
|
269
|
+
|
|
270
|
+
const written = JSON.parse(
|
|
271
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
272
|
+
);
|
|
273
|
+
expect(written.scores['src/foo.ts'].score).toBe(66.7);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe('removeDeletedFiles (via module side effects)', () => {
|
|
278
|
+
it('should remove files that no longer exist on disk', async () => {
|
|
279
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/keep.ts:80.0'];
|
|
280
|
+
const baseline = makeBaseline({
|
|
281
|
+
scores: {
|
|
282
|
+
'src/keep.ts': { score: 70.0, mutants: 10, killed: 7, survived: 3 },
|
|
283
|
+
'src/deleted.ts': { score: 50.0, mutants: 5, killed: 2, survived: 3 }
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
287
|
+
JSON.stringify(baseline)
|
|
288
|
+
);
|
|
289
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockImplementation(
|
|
290
|
+
(filePath: string) => {
|
|
291
|
+
if (filePath.includes('deleted.ts')) {
|
|
292
|
+
return Promise.reject(new Error('ENOENT'));
|
|
293
|
+
}
|
|
294
|
+
return Promise.resolve(undefined);
|
|
295
|
+
}
|
|
296
|
+
);
|
|
297
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
298
|
+
|
|
299
|
+
await importModule();
|
|
300
|
+
|
|
301
|
+
const written = JSON.parse(
|
|
302
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
303
|
+
);
|
|
304
|
+
expect(written.scores['src/keep.ts']).toBeDefined();
|
|
305
|
+
expect(written.scores['src/deleted.ts']).toBeUndefined();
|
|
306
|
+
expect(consoleLogSpy).toHaveBeenCalledWith(
|
|
307
|
+
expect.stringContaining('Removing deleted file from baseline: src/deleted.ts')
|
|
308
|
+
);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should keep all files when they all exist on disk', async () => {
|
|
312
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/a.ts:90.0'];
|
|
313
|
+
const baseline = makeBaseline({
|
|
314
|
+
scores: {
|
|
315
|
+
'src/a.ts': { score: 80.0, mutants: 10, killed: 8, survived: 2 },
|
|
316
|
+
'src/b.ts': { score: 70.0, mutants: 8, killed: 5, survived: 3 }
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
320
|
+
JSON.stringify(baseline)
|
|
321
|
+
);
|
|
322
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
323
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
324
|
+
|
|
325
|
+
await importModule();
|
|
326
|
+
|
|
327
|
+
const written = JSON.parse(
|
|
328
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mock.calls[0][1] as string
|
|
329
|
+
);
|
|
330
|
+
expect(Object.keys(written.scores)).toHaveLength(2);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('readBaseline (via module side effects)', () => {
|
|
335
|
+
it('should fail with error when baseline file does not exist', async () => {
|
|
336
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:50.0'];
|
|
337
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockRejectedValue(
|
|
338
|
+
new Error('ENOENT: no such file')
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
await importModule();
|
|
342
|
+
|
|
343
|
+
expect(exitCode).toBe(1);
|
|
344
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
345
|
+
expect.stringContaining('Failed to read baseline file')
|
|
346
|
+
);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should fail with error when baseline file contains invalid JSON', async () => {
|
|
350
|
+
process.argv = ['node', 'update-baseline.js', '--scores', 'src/foo.ts:50.0'];
|
|
351
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
352
|
+
'not-valid-json{{{'
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
await importModule();
|
|
356
|
+
|
|
357
|
+
expect(exitCode).toBe(1);
|
|
358
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
359
|
+
expect.stringContaining('Failed to read baseline file')
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
describe('end-to-end output', () => {
|
|
365
|
+
it('should log per-file scores and total file count', async () => {
|
|
366
|
+
process.argv = [
|
|
367
|
+
'node',
|
|
368
|
+
'update-baseline.js',
|
|
369
|
+
'--scores',
|
|
370
|
+
'src/a.ts:80.0,src/b.ts:60.0'
|
|
371
|
+
];
|
|
372
|
+
const baseline = makeBaseline();
|
|
373
|
+
(mockFs.default.readFile as ReturnType<typeof vi.fn>).mockResolvedValue(
|
|
374
|
+
JSON.stringify(baseline)
|
|
375
|
+
);
|
|
376
|
+
(mockFs.default.access as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
377
|
+
(mockFs.default.writeFile as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
|
378
|
+
|
|
379
|
+
await importModule();
|
|
380
|
+
|
|
381
|
+
const allLogs = consoleLogSpy.mock.calls.map((c: unknown[]) => c[0]);
|
|
382
|
+
expect(allLogs).toEqual(
|
|
383
|
+
expect.arrayContaining([
|
|
384
|
+
'Reading existing mutation baseline...',
|
|
385
|
+
'Updating 2 file score(s)...',
|
|
386
|
+
'Checking for deleted files...',
|
|
387
|
+
'Total files: 2'
|
|
388
|
+
])
|
|
389
|
+
);
|
|
390
|
+
expect(allLogs).toEqual(expect.arrayContaining([' src/a.ts: 80%', ' src/b.ts: 60%']));
|
|
391
|
+
});
|
|
392
|
+
});
|
|
393
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xp-gate",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
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
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.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
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.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.
|
|
@@ -167,7 +167,7 @@ PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE
|
|
|
167
167
|
| Phase | Name | Key Actions | Output |
|
|
168
168
|
|-------|------|-------------|--------|
|
|
169
169
|
| 1/6 | PREP | Detect protected branch → Create git worktree → AUTO-ESTIMATE sizing → Classify (lightweight/standard/complex) | Worktree path + impact assessment |
|
|
170
|
-
| 2/6 | DESIGN | brainstorming → autoplan → delphi-review (HARD-GATE ≥90% consensus) → to-issues → specification.yaml | specification.yaml + slices-manifest.json |
|
|
170
|
+
| 2/6 | DESIGN | CONTEXT.md 预检 (#322) → brainstorming (如需要) → autoplan → delphi-review (HARD-GATE ≥90% consensus) → to-issues → specification.yaml | specification.yaml + slices-manifest.json |
|
|
171
171
|
| 3/6 | BUILD | GITHOOKS-GATE → DELPHI-GATE → ralph-loop (default) or parallel → TDD → freeze → blind review → verification | MVP code |
|
|
172
172
|
| 4/6 | VERIFY | delphi-review --mode code-walkthrough → test-specification-alignment → browse QA → learn + retro | Review report + feedback-log.md |
|
|
173
173
|
| 5/6 | SHIP | VERSION-GATE → finishing-a-development-branch → ship (create PR) → land-and-deploy → merge to main + CI + canary | PR URL + deploy status + merge confirmation |
|
|
@@ -11,6 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
## Part A: THINK(需求探索与设计)
|
|
13
13
|
|
|
14
|
+
### Step 0: CONTEXT.md 预检(v0.14.9+ — Issue #322)
|
|
15
|
+
|
|
16
|
+
在进入 brainstorming 前,检查 `CONTEXT.md` 是否已存在:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
IF CONTEXT.md 存在于项目根目录:
|
|
20
|
+
→ 读取已有设计上下文
|
|
21
|
+
→ SKIP brainstorming(设计已存在,避免重复探索)
|
|
22
|
+
→ 直接进入 Part B PLAN(使用已有 CONTEXT.md 和设计文档作为输入)
|
|
23
|
+
→ 输出: "[sprint-flow] CONTEXT.md 已存在,跳过需求探索,直接进入共识评审"
|
|
24
|
+
ELSE:
|
|
25
|
+
→ 继续执行 Step 1(结构化需求探索)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**CONTEXT.md 预检路由表**:
|
|
29
|
+
|
|
30
|
+
| CONTEXT.md 状态 | brainstorming | 输入到 Part B | 说明 |
|
|
31
|
+
|----------------|---------------|---------------|------|
|
|
32
|
+
| 不存在 | ✅ 执行 | brainstorming 输出 + autoplan 输出 | 标准路径 |
|
|
33
|
+
| 存在 | ❌ SKIP | 已有 CONTEXT.md + 设计文档 | 快速路径 (Issue #322) |
|
|
34
|
+
|
|
14
35
|
### 调用 Skills
|
|
15
36
|
|
|
16
37
|
- `brainstorming` (superpowers) — **HARD-GATE**: 设计未批准 → 不可进入实现
|
|
@@ -102,14 +123,18 @@ skill(name="brainstorming", user_message="[需求描述]")
|
|
|
102
123
|
skill(name="design-shotgun", user_message="[Pain Document 内容 + 需求描述]")
|
|
103
124
|
```
|
|
104
125
|
|
|
105
|
-
#### Step 0.5: DESIGN 路由分叉(v0.14.0+ — Issue #306)
|
|
126
|
+
#### Step 0.5: DESIGN 路由分叉(v0.14.0+ — Issue #306, #322)
|
|
106
127
|
|
|
107
|
-
根据 Phase 1/6 PREP (AUTO-ESTIMATE) 的 `change_type`
|
|
128
|
+
根据 Phase 1/6 PREP (AUTO-ESTIMATE) 的 `change_type` 以及 Part A 的 CONTEXT.md 预检结果,决定走哪条路径:
|
|
108
129
|
|
|
109
130
|
```
|
|
110
131
|
读取 .sprint-state/sprint-state.json → auto_estimate.change_type
|
|
132
|
+
读取 CONTEXT.md 存在状态
|
|
111
133
|
|
|
112
|
-
IF
|
|
134
|
+
IF CONTEXT.md 存在(Part A Step 0 预检结果):
|
|
135
|
+
→ 快速路径: SKIP brainstorming, 使用已有 CONTEXT.md + 设计文档
|
|
136
|
+
→ 直接进入 autoplan (标准) 或 delphi-review (增量)
|
|
137
|
+
ELSE IF change_type == "修改已存在代码":
|
|
113
138
|
→ 增量优化路径: SKIP autoplan
|
|
114
139
|
→ 直接进入 Step 2b: delphi-review (lightweight: 2 专家, 1 轮)
|
|
115
140
|
ELSE (change_type == "新增功能" 或 未定义):
|
|
@@ -118,11 +143,12 @@ ELSE (change_type == "新增功能" 或 未定义):
|
|
|
118
143
|
|
|
119
144
|
**路由决策表**:
|
|
120
145
|
|
|
121
|
-
| change_type |
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
146
|
+
| CONTEXT.md | change_type | brainstorming | autoplan | delphi-review |
|
|
147
|
+
|-----------|------------|---------------|----------|---------------|
|
|
148
|
+
| 存在 | 任意 | ❌ SKIP | ✅ 执行 (标准) | 标准 (3 专家) |
|
|
149
|
+
| 不存在 | `修改已存在代码` | ✅ 执行 | ❌ SKIP | lightweight (2 专家, 1 轮) |
|
|
150
|
+
| 不存在 | `新增功能` | ✅ 执行 | ✅ 执行 | 标准 (3 专家) |
|
|
151
|
+
| 不存在 | `undefined` / 缺失 | ✅ 执行 | ✅ 执行 | 标准 (3 专家) |
|
|
126
152
|
|
|
127
153
|
**HARD-GATE 不变**: 两条路径最终都必须产出 APPROVED 的 delphi-reviewed.json。
|
|
128
154
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.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
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.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
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.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.
|
|
@@ -167,7 +167,7 @@ PREP → DESIGN → BUILD → VERIFY → SHIP → CLOSE
|
|
|
167
167
|
| Phase | Name | Key Actions | Output |
|
|
168
168
|
|-------|------|-------------|--------|
|
|
169
169
|
| 1/6 | PREP | Detect protected branch → Create git worktree → AUTO-ESTIMATE sizing → Classify (lightweight/standard/complex) | Worktree path + impact assessment |
|
|
170
|
-
| 2/6 | DESIGN | brainstorming → autoplan → delphi-review (HARD-GATE ≥90% consensus) → to-issues → specification.yaml | specification.yaml + slices-manifest.json |
|
|
170
|
+
| 2/6 | DESIGN | CONTEXT.md 预检 (#322) → brainstorming (如需要) → autoplan → delphi-review (HARD-GATE ≥90% consensus) → to-issues → specification.yaml | specification.yaml + slices-manifest.json |
|
|
171
171
|
| 3/6 | BUILD | GITHOOKS-GATE → DELPHI-GATE → ralph-loop (default) or parallel → TDD → freeze → blind review → verification | MVP code |
|
|
172
172
|
| 4/6 | VERIFY | delphi-review --mode code-walkthrough → test-specification-alignment → browse QA → learn + retro | Review report + feedback-log.md |
|
|
173
173
|
| 5/6 | SHIP | VERSION-GATE → finishing-a-development-branch → ship (create PR) → land-and-deploy → merge to main + CI + canary | PR URL + deploy status + merge confirmation |
|
|
@@ -11,6 +11,27 @@
|
|
|
11
11
|
|
|
12
12
|
## Part A: THINK(需求探索与设计)
|
|
13
13
|
|
|
14
|
+
### Step 0: CONTEXT.md 预检(v0.14.9+ — Issue #322)
|
|
15
|
+
|
|
16
|
+
在进入 brainstorming 前,检查 `CONTEXT.md` 是否已存在:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
IF CONTEXT.md 存在于项目根目录:
|
|
20
|
+
→ 读取已有设计上下文
|
|
21
|
+
→ SKIP brainstorming(设计已存在,避免重复探索)
|
|
22
|
+
→ 直接进入 Part B PLAN(使用已有 CONTEXT.md 和设计文档作为输入)
|
|
23
|
+
→ 输出: "[sprint-flow] CONTEXT.md 已存在,跳过需求探索,直接进入共识评审"
|
|
24
|
+
ELSE:
|
|
25
|
+
→ 继续执行 Step 1(结构化需求探索)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
**CONTEXT.md 预检路由表**:
|
|
29
|
+
|
|
30
|
+
| CONTEXT.md 状态 | brainstorming | 输入到 Part B | 说明 |
|
|
31
|
+
|----------------|---------------|---------------|------|
|
|
32
|
+
| 不存在 | ✅ 执行 | brainstorming 输出 + autoplan 输出 | 标准路径 |
|
|
33
|
+
| 存在 | ❌ SKIP | 已有 CONTEXT.md + 设计文档 | 快速路径 (Issue #322) |
|
|
34
|
+
|
|
14
35
|
### 调用 Skills
|
|
15
36
|
|
|
16
37
|
- `brainstorming` (superpowers) — **HARD-GATE**: 设计未批准 → 不可进入实现
|
|
@@ -102,14 +123,18 @@ skill(name="brainstorming", user_message="[需求描述]")
|
|
|
102
123
|
skill(name="design-shotgun", user_message="[Pain Document 内容 + 需求描述]")
|
|
103
124
|
```
|
|
104
125
|
|
|
105
|
-
#### Step 0.5: DESIGN 路由分叉(v0.14.0+ — Issue #306)
|
|
126
|
+
#### Step 0.5: DESIGN 路由分叉(v0.14.0+ — Issue #306, #322)
|
|
106
127
|
|
|
107
|
-
根据 Phase 1/6 PREP (AUTO-ESTIMATE) 的 `change_type`
|
|
128
|
+
根据 Phase 1/6 PREP (AUTO-ESTIMATE) 的 `change_type` 以及 Part A 的 CONTEXT.md 预检结果,决定走哪条路径:
|
|
108
129
|
|
|
109
130
|
```
|
|
110
131
|
读取 .sprint-state/sprint-state.json → auto_estimate.change_type
|
|
132
|
+
读取 CONTEXT.md 存在状态
|
|
111
133
|
|
|
112
|
-
IF
|
|
134
|
+
IF CONTEXT.md 存在(Part A Step 0 预检结果):
|
|
135
|
+
→ 快速路径: SKIP brainstorming, 使用已有 CONTEXT.md + 设计文档
|
|
136
|
+
→ 直接进入 autoplan (标准) 或 delphi-review (增量)
|
|
137
|
+
ELSE IF change_type == "修改已存在代码":
|
|
113
138
|
→ 增量优化路径: SKIP autoplan
|
|
114
139
|
→ 直接进入 Step 2b: delphi-review (lightweight: 2 专家, 1 轮)
|
|
115
140
|
ELSE (change_type == "新增功能" 或 未定义):
|
|
@@ -118,11 +143,12 @@ ELSE (change_type == "新增功能" 或 未定义):
|
|
|
118
143
|
|
|
119
144
|
**路由决策表**:
|
|
120
145
|
|
|
121
|
-
| change_type |
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
|
125
|
-
|
|
|
146
|
+
| CONTEXT.md | change_type | brainstorming | autoplan | delphi-review |
|
|
147
|
+
|-----------|------------|---------------|----------|---------------|
|
|
148
|
+
| 存在 | 任意 | ❌ SKIP | ✅ 执行 (标准) | 标准 (3 专家) |
|
|
149
|
+
| 不存在 | `修改已存在代码` | ✅ 执行 | ❌ SKIP | lightweight (2 专家, 1 轮) |
|
|
150
|
+
| 不存在 | `新增功能` | ✅ 执行 | ✅ 执行 | 标准 (3 专家) |
|
|
151
|
+
| 不存在 | `undefined` / 缺失 | ✅ 执行 | ✅ 执行 | 标准 (3 专家) |
|
|
126
152
|
|
|
127
153
|
**HARD-GATE 不变**: 两条路径最终都必须产出 APPROVED 的 delphi-reviewed.json。
|
|
128
154
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# SKILLS/TEST-SPECIFICATION-ALIGNMENT KNOWLEDGE BASE
|
|
2
2
|
|
|
3
|
-
**Generated:** 2026-07-
|
|
4
|
-
**Commit:**
|
|
5
|
-
**Branch:**
|
|
6
|
-
**Version:** 0.14.
|
|
3
|
+
**Generated:** 2026-07-13
|
|
4
|
+
**Commit:** cb954fe
|
|
5
|
+
**Branch:** sprint/2026-07-13-01
|
|
6
|
+
**Version:** 0.14.9.0
|
|
7
7
|
|
|
8
8
|
## OVERVIEW
|
|
9
9
|
Test-Specification Alignment Engine — two-stage validation ensuring tests accurately reflect requirements and design specs.
|