peaks-cli 1.0.17 → 1.0.18
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/peaks.js +0 -0
- package/dist/src/cli/commands/request-commands.js +109 -3
- package/dist/src/cli/commands/scan-commands.d.ts +3 -0
- package/dist/src/cli/commands/scan-commands.js +194 -0
- package/dist/src/cli/commands/workspace-commands.d.ts +3 -0
- package/dist/src/cli/commands/workspace-commands.js +32 -0
- package/dist/src/cli/program.js +4 -0
- package/dist/src/services/artifacts/artifact-lint-service.d.ts +23 -0
- package/dist/src/services/artifacts/artifact-lint-service.js +80 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +28 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +77 -0
- package/dist/src/services/artifacts/repair-cycle-service.d.ts +23 -0
- package/dist/src/services/artifacts/repair-cycle-service.js +52 -0
- package/dist/src/services/artifacts/request-artifact-service.d.ts +14 -0
- package/dist/src/services/artifacts/request-artifact-service.js +73 -21
- package/dist/src/services/scan/acceptance-coverage-service.d.ts +42 -0
- package/dist/src/services/scan/acceptance-coverage-service.js +135 -0
- package/dist/src/services/scan/archetype-service.d.ts +5 -0
- package/dist/src/services/scan/archetype-service.js +253 -0
- package/dist/src/services/scan/diff-scope-service.d.ts +40 -0
- package/dist/src/services/scan/diff-scope-service.js +198 -0
- package/dist/src/services/scan/existing-system-service.d.ts +7 -0
- package/dist/src/services/scan/existing-system-service.js +300 -0
- package/dist/src/services/scan/scan-types.d.ts +59 -0
- package/dist/src/services/scan/scan-types.js +1 -0
- package/dist/src/services/scan/type-sanity-service.d.ts +23 -0
- package/dist/src/services/scan/type-sanity-service.js +108 -0
- package/dist/src/services/workspace/workspace-service.d.ts +16 -0
- package/dist/src/services/workspace/workspace-service.js +66 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-qa/SKILL.md +42 -0
- package/skills/peaks-rd/SKILL.md +65 -2
- package/skills/peaks-solo/SKILL.md +275 -57
- package/skills/peaks-solo/references/existing-system-extraction.md +78 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type RepairCycleEntry = {
|
|
2
|
+
cycle: number;
|
|
3
|
+
timestamp: string;
|
|
4
|
+
reason: string;
|
|
5
|
+
};
|
|
6
|
+
export type RepairCycleReport = {
|
|
7
|
+
requestId: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
path: string;
|
|
10
|
+
cycleCount: number;
|
|
11
|
+
maxCycles: number;
|
|
12
|
+
remaining: number;
|
|
13
|
+
atCap: boolean;
|
|
14
|
+
blocked: boolean;
|
|
15
|
+
entries: RepairCycleEntry[];
|
|
16
|
+
};
|
|
17
|
+
export type RepairCycleStatusOptions = {
|
|
18
|
+
projectRoot: string;
|
|
19
|
+
requestId: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
maxCycles?: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function getRepairCycleStatus(options: RepairCycleStatusOptions): Promise<RepairCycleReport | null>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { showRequestArtifact } from './request-artifact-service.js';
|
|
2
|
+
const DEFAULT_MAX_CYCLES = 3;
|
|
3
|
+
// Matches transition notes Solo writes during repair routing.
|
|
4
|
+
// Format example:
|
|
5
|
+
// - transition note (2026-05-25T08:00:00.000Z): QA return-to-rd cycle 1: failing acceptance items A, B
|
|
6
|
+
// - transition note (2026-05-25T09:00:00.000Z): QA cycle 2: regression in module X
|
|
7
|
+
const REPAIR_NOTE_PATTERN = /-\s*transition note\s*\(([^)]+)\)\s*:\s*(?:QA(?:\s+return-to-rd)?\s+cycle\s+(\d+))\s*:?\s*(.*?)$/i;
|
|
8
|
+
export async function getRepairCycleStatus(options) {
|
|
9
|
+
const showOptions = {
|
|
10
|
+
projectRoot: options.projectRoot,
|
|
11
|
+
role: 'rd',
|
|
12
|
+
requestId: options.requestId
|
|
13
|
+
};
|
|
14
|
+
if (options.sessionId !== undefined) {
|
|
15
|
+
showOptions.sessionId = options.sessionId;
|
|
16
|
+
}
|
|
17
|
+
const artifact = await showRequestArtifact(showOptions);
|
|
18
|
+
if (artifact === null) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const maxCycles = options.maxCycles ?? DEFAULT_MAX_CYCLES;
|
|
22
|
+
const lines = artifact.content.split(/\r?\n/);
|
|
23
|
+
const entries = [];
|
|
24
|
+
for (const rawLine of lines) {
|
|
25
|
+
const match = REPAIR_NOTE_PATTERN.exec(rawLine);
|
|
26
|
+
if (match === null)
|
|
27
|
+
continue;
|
|
28
|
+
const [, timestamp, cycleStr, reason] = match;
|
|
29
|
+
if (timestamp === undefined || cycleStr === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const cycle = Number(cycleStr);
|
|
32
|
+
if (!Number.isFinite(cycle) || cycle < 1)
|
|
33
|
+
continue;
|
|
34
|
+
entries.push({ cycle, timestamp, reason: (reason ?? '').trim() });
|
|
35
|
+
}
|
|
36
|
+
// Distinct cycle numbers — repair loop may write the same cycle note multiple times.
|
|
37
|
+
const distinct = new Set(entries.map((entry) => entry.cycle));
|
|
38
|
+
const cycleCount = distinct.size;
|
|
39
|
+
const remaining = Math.max(0, maxCycles - cycleCount);
|
|
40
|
+
const atCap = cycleCount >= maxCycles;
|
|
41
|
+
return {
|
|
42
|
+
requestId: options.requestId,
|
|
43
|
+
sessionId: artifact.sessionId,
|
|
44
|
+
path: artifact.path,
|
|
45
|
+
cycleCount,
|
|
46
|
+
maxCycles,
|
|
47
|
+
remaining,
|
|
48
|
+
atCap,
|
|
49
|
+
blocked: atCap,
|
|
50
|
+
entries
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES, type PrerequisiteCheckResult, type RequestType } from './artifact-prerequisites.js';
|
|
2
|
+
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType, type RequestType };
|
|
1
3
|
export type RequestArtifactRole = 'prd' | 'ui' | 'rd' | 'qa' | 'sc';
|
|
2
4
|
export type CreateRequestArtifactOptions = {
|
|
3
5
|
role: RequestArtifactRole;
|
|
@@ -5,6 +7,7 @@ export type CreateRequestArtifactOptions = {
|
|
|
5
7
|
projectRoot: string;
|
|
6
8
|
sessionId?: string;
|
|
7
9
|
apply?: boolean;
|
|
10
|
+
requestType?: RequestType;
|
|
8
11
|
clock?: () => string;
|
|
9
12
|
};
|
|
10
13
|
export type CreateRequestArtifactResult = {
|
|
@@ -22,6 +25,7 @@ export type RequestArtifactSummary = {
|
|
|
22
25
|
requestId: string;
|
|
23
26
|
path: string;
|
|
24
27
|
state: string;
|
|
28
|
+
requestType: RequestType;
|
|
25
29
|
createdAt?: string;
|
|
26
30
|
};
|
|
27
31
|
export type ListRequestArtifactsOptions = {
|
|
@@ -49,10 +53,20 @@ export type TransitionRequestArtifactOptions = {
|
|
|
49
53
|
newState: RequestArtifactState;
|
|
50
54
|
sessionId?: string;
|
|
51
55
|
reason?: string;
|
|
56
|
+
allowIncomplete?: boolean;
|
|
52
57
|
clock?: () => string;
|
|
53
58
|
};
|
|
54
59
|
export type TransitionRequestArtifactResult = RequestArtifactSummary & {
|
|
55
60
|
previousState: string;
|
|
56
61
|
content: string;
|
|
62
|
+
bypassedPrerequisites?: PrerequisiteCheckResult;
|
|
57
63
|
};
|
|
64
|
+
export declare class PrerequisitesNotSatisfiedError extends Error {
|
|
65
|
+
readonly code = "PREREQUISITES_MISSING";
|
|
66
|
+
readonly role: RequestArtifactRole;
|
|
67
|
+
readonly newState: RequestArtifactState;
|
|
68
|
+
readonly sessionId: string;
|
|
69
|
+
readonly missing: PrerequisiteCheckResult['missing'];
|
|
70
|
+
constructor(role: RequestArtifactRole, newState: RequestArtifactState, sessionId: string, missing: PrerequisiteCheckResult['missing']);
|
|
71
|
+
}
|
|
58
72
|
export declare function transitionRequestArtifact(options: TransitionRequestArtifactOptions): Promise<TransitionRequestArtifactResult | null>;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { isDirectory, listDirectories, pathExists } from '../../shared/fs.js';
|
|
4
|
+
import { checkPrerequisites, DEFAULT_REQUEST_TYPE, isRequestType, VALID_REQUEST_TYPES } from './artifact-prerequisites.js';
|
|
5
|
+
export { VALID_REQUEST_TYPES, DEFAULT_REQUEST_TYPE, isRequestType };
|
|
4
6
|
const REQUEST_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
5
7
|
const VALID_ROLES = new Set(['prd', 'ui', 'rd', 'qa', 'sc']);
|
|
6
8
|
function defaultClock() {
|
|
@@ -12,11 +14,11 @@ function dateSlugFromIso(iso) {
|
|
|
12
14
|
function defaultSessionId(iso) {
|
|
13
15
|
return `${dateSlugFromIso(iso)}-session`;
|
|
14
16
|
}
|
|
15
|
-
function renderPrdTemplate(requestId, sessionId, timestamp) {
|
|
17
|
+
function renderPrdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
16
18
|
return `# PRD Request ${requestId}
|
|
17
19
|
|
|
18
20
|
- session: ${sessionId}
|
|
19
|
-
- type:
|
|
21
|
+
- type: ${requestType}
|
|
20
22
|
- source: <ticket, message URL, or "verbal" with a short sanitized quote>
|
|
21
23
|
- raw input (sanitized): <one-paragraph restatement of what the user actually asked for>
|
|
22
24
|
|
|
@@ -59,11 +61,12 @@ function renderPrdTemplate(requestId, sessionId, timestamp) {
|
|
|
59
61
|
- state: draft
|
|
60
62
|
`;
|
|
61
63
|
}
|
|
62
|
-
function renderUiTemplate(requestId, sessionId, timestamp) {
|
|
64
|
+
function renderUiTemplate(requestId, sessionId, timestamp, requestType) {
|
|
63
65
|
return `# UI Request ${requestId}
|
|
64
66
|
|
|
65
67
|
- session: ${sessionId}
|
|
66
68
|
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
69
|
+
- type: ${requestType}
|
|
67
70
|
- scope: full new surface | iteration on existing surface | regression fix | visual refresh
|
|
68
71
|
- design direction: editorial | bento | Swiss | luxury | retro-futurist | glass | product-system | other-explicit-name
|
|
69
72
|
|
|
@@ -108,13 +111,13 @@ function renderUiTemplate(requestId, sessionId, timestamp) {
|
|
|
108
111
|
- state: draft
|
|
109
112
|
`;
|
|
110
113
|
}
|
|
111
|
-
function renderRdTemplate(requestId, sessionId, timestamp) {
|
|
114
|
+
function renderRdTemplate(requestId, sessionId, timestamp, requestType) {
|
|
112
115
|
return `# RD Request ${requestId}
|
|
113
116
|
|
|
114
117
|
- session: ${sessionId}
|
|
115
118
|
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
116
119
|
- linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
117
|
-
- type:
|
|
120
|
+
- type: ${requestType}
|
|
118
121
|
|
|
119
122
|
## Red-line scope
|
|
120
123
|
|
|
@@ -165,14 +168,14 @@ function renderRdTemplate(requestId, sessionId, timestamp) {
|
|
|
165
168
|
- state: draft
|
|
166
169
|
`;
|
|
167
170
|
}
|
|
168
|
-
function renderQaTemplate(requestId, sessionId, timestamp) {
|
|
171
|
+
function renderQaTemplate(requestId, sessionId, timestamp, requestType) {
|
|
169
172
|
return `# QA Request ${requestId}
|
|
170
173
|
|
|
171
174
|
- session: ${sessionId}
|
|
172
175
|
- linked-prd: .peaks/${sessionId}/prd/requests/${requestId}.md
|
|
173
176
|
- linked-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
|
|
174
177
|
- linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
175
|
-
- type:
|
|
178
|
+
- type: ${requestType}
|
|
176
179
|
|
|
177
180
|
## Red-line boundary check
|
|
178
181
|
|
|
@@ -220,7 +223,7 @@ function renderQaTemplate(requestId, sessionId, timestamp) {
|
|
|
220
223
|
- state: draft
|
|
221
224
|
`;
|
|
222
225
|
}
|
|
223
|
-
function renderScTemplate(requestId, sessionId, timestamp) {
|
|
226
|
+
function renderScTemplate(requestId, sessionId, timestamp, requestType) {
|
|
224
227
|
return `# SC Request ${requestId}
|
|
225
228
|
|
|
226
229
|
- session: ${sessionId}
|
|
@@ -228,7 +231,7 @@ function renderScTemplate(requestId, sessionId, timestamp) {
|
|
|
228
231
|
- linked-rd: .peaks/${sessionId}/rd/requests/${requestId}.md
|
|
229
232
|
- linked-qa: .peaks/${sessionId}/qa/requests/${requestId}.md
|
|
230
233
|
- linked-ui: .peaks/${sessionId}/ui/requests/${requestId}.md (when UI involved)
|
|
231
|
-
- type:
|
|
234
|
+
- type: ${requestType}
|
|
232
235
|
|
|
233
236
|
## Change impact
|
|
234
237
|
|
|
@@ -272,18 +275,18 @@ function renderScTemplate(requestId, sessionId, timestamp) {
|
|
|
272
275
|
- state: draft
|
|
273
276
|
`;
|
|
274
277
|
}
|
|
275
|
-
function renderTemplate(role, requestId, sessionId, timestamp) {
|
|
278
|
+
function renderTemplate(role, requestId, sessionId, timestamp, requestType) {
|
|
276
279
|
switch (role) {
|
|
277
280
|
case 'prd':
|
|
278
|
-
return renderPrdTemplate(requestId, sessionId, timestamp);
|
|
281
|
+
return renderPrdTemplate(requestId, sessionId, timestamp, requestType);
|
|
279
282
|
case 'ui':
|
|
280
|
-
return renderUiTemplate(requestId, sessionId, timestamp);
|
|
283
|
+
return renderUiTemplate(requestId, sessionId, timestamp, requestType);
|
|
281
284
|
case 'rd':
|
|
282
|
-
return renderRdTemplate(requestId, sessionId, timestamp);
|
|
285
|
+
return renderRdTemplate(requestId, sessionId, timestamp, requestType);
|
|
283
286
|
case 'qa':
|
|
284
|
-
return renderQaTemplate(requestId, sessionId, timestamp);
|
|
287
|
+
return renderQaTemplate(requestId, sessionId, timestamp, requestType);
|
|
285
288
|
case 'sc':
|
|
286
|
-
return renderScTemplate(requestId, sessionId, timestamp);
|
|
289
|
+
return renderScTemplate(requestId, sessionId, timestamp, requestType);
|
|
287
290
|
}
|
|
288
291
|
}
|
|
289
292
|
export async function createRequestArtifact(options) {
|
|
@@ -293,11 +296,12 @@ export async function createRequestArtifact(options) {
|
|
|
293
296
|
if (!REQUEST_ID_PATTERN.test(options.requestId)) {
|
|
294
297
|
throw new Error(`Invalid request id: ${options.requestId} (expected letters, digits, dots, underscores, or dashes)`);
|
|
295
298
|
}
|
|
299
|
+
const requestType = options.requestType ?? DEFAULT_REQUEST_TYPE;
|
|
296
300
|
const clock = options.clock ?? defaultClock;
|
|
297
301
|
const timestamp = clock();
|
|
298
302
|
const sessionId = options.sessionId ?? defaultSessionId(timestamp);
|
|
299
303
|
const path = join(options.projectRoot, '.peaks', sessionId, options.role, 'requests', `${options.requestId}.md`);
|
|
300
|
-
const content = renderTemplate(options.role, options.requestId, sessionId, timestamp);
|
|
304
|
+
const content = renderTemplate(options.role, options.requestId, sessionId, timestamp, requestType);
|
|
301
305
|
if (options.apply !== true) {
|
|
302
306
|
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: false };
|
|
303
307
|
}
|
|
@@ -308,9 +312,10 @@ export async function createRequestArtifact(options) {
|
|
|
308
312
|
await writeFile(path, content, 'utf8');
|
|
309
313
|
return { role: options.role, requestId: options.requestId, sessionId, path, content, applied: true };
|
|
310
314
|
}
|
|
311
|
-
function
|
|
315
|
+
function extractMetadata(markdown) {
|
|
312
316
|
let state = 'unknown';
|
|
313
317
|
let createdAt;
|
|
318
|
+
let requestType = DEFAULT_REQUEST_TYPE;
|
|
314
319
|
for (const rawLine of markdown.split(/\r?\n/)) {
|
|
315
320
|
const line = rawLine.trim();
|
|
316
321
|
const stateMatch = /^-\s*state:\s*(.+?)\s*$/.exec(line);
|
|
@@ -321,16 +326,28 @@ function extractStateAndCreated(markdown) {
|
|
|
321
326
|
const createdMatch = /^-\s*created:\s*(.+?)\s*$/.exec(line);
|
|
322
327
|
if (createdMatch !== null && createdMatch[1] !== undefined) {
|
|
323
328
|
createdAt = createdMatch[1];
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
const typeMatch = /^-\s*type:\s*(.+?)\s*$/.exec(line);
|
|
332
|
+
if (typeMatch !== null && typeMatch[1] !== undefined) {
|
|
333
|
+
const candidate = typeMatch[1];
|
|
334
|
+
if (isRequestType(candidate)) {
|
|
335
|
+
requestType = candidate;
|
|
336
|
+
}
|
|
337
|
+
// Placeholder values (e.g. "feature | bug | refactor | clarification") fall back to default.
|
|
324
338
|
}
|
|
325
339
|
}
|
|
326
|
-
|
|
340
|
+
const base = { state, requestType };
|
|
341
|
+
if (createdAt !== undefined)
|
|
342
|
+
base.createdAt = createdAt;
|
|
343
|
+
return base;
|
|
327
344
|
}
|
|
328
345
|
async function readSummary(projectRoot, sessionId, role, fileName) {
|
|
329
346
|
const path = join(projectRoot, '.peaks', sessionId, role, 'requests', fileName);
|
|
330
347
|
const body = await readFile(path, 'utf8');
|
|
331
|
-
const { state, createdAt } =
|
|
348
|
+
const { state, createdAt, requestType } = extractMetadata(body);
|
|
332
349
|
const requestId = fileName.replace(/\.md$/, '');
|
|
333
|
-
const summary = { role, sessionId, requestId, path, state };
|
|
350
|
+
const summary = { role, sessionId, requestId, path, state, requestType };
|
|
334
351
|
if (createdAt !== undefined) {
|
|
335
352
|
summary.createdAt = createdAt;
|
|
336
353
|
}
|
|
@@ -407,6 +424,21 @@ const ALLOWED_STATES_PER_ROLE = {
|
|
|
407
424
|
export function allowedStatesForRole(role) {
|
|
408
425
|
return ALLOWED_STATES_PER_ROLE[role];
|
|
409
426
|
}
|
|
427
|
+
export class PrerequisitesNotSatisfiedError extends Error {
|
|
428
|
+
code = 'PREREQUISITES_MISSING';
|
|
429
|
+
role;
|
|
430
|
+
newState;
|
|
431
|
+
sessionId;
|
|
432
|
+
missing;
|
|
433
|
+
constructor(role, newState, sessionId, missing) {
|
|
434
|
+
super(`Cannot transition ${role} to ${newState}: ${missing.length} required artifact${missing.length === 1 ? '' : 's'} missing under .peaks/${sessionId}/`);
|
|
435
|
+
this.name = 'PrerequisitesNotSatisfiedError';
|
|
436
|
+
this.role = role;
|
|
437
|
+
this.newState = newState;
|
|
438
|
+
this.sessionId = sessionId;
|
|
439
|
+
this.missing = missing;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
410
442
|
function updateStatusBlock(markdown, newState, timestamp, reason) {
|
|
411
443
|
const lines = markdown.split(/\r?\n/);
|
|
412
444
|
let previousState = 'unknown';
|
|
@@ -467,9 +499,25 @@ export async function transitionRequestArtifact(options) {
|
|
|
467
499
|
if (existing === null) {
|
|
468
500
|
return null;
|
|
469
501
|
}
|
|
502
|
+
const prerequisiteResult = await checkPrerequisites({
|
|
503
|
+
projectRoot: options.projectRoot,
|
|
504
|
+
sessionId: existing.sessionId,
|
|
505
|
+
role: options.role,
|
|
506
|
+
newState: options.newState,
|
|
507
|
+
requestId: options.requestId,
|
|
508
|
+
requestType: existing.requestType
|
|
509
|
+
});
|
|
510
|
+
if (!prerequisiteResult.ok && options.allowIncomplete !== true) {
|
|
511
|
+
throw new PrerequisitesNotSatisfiedError(options.role, options.newState, existing.sessionId, prerequisiteResult.missing);
|
|
512
|
+
}
|
|
470
513
|
const clock = options.clock ?? defaultClock;
|
|
471
514
|
const timestamp = clock();
|
|
472
|
-
const
|
|
515
|
+
const bypassNote = !prerequisiteResult.ok && options.allowIncomplete === true
|
|
516
|
+
? `bypassed prerequisites (${prerequisiteResult.missing.map((entry) => entry.path).join(', ')})`
|
|
517
|
+
: undefined;
|
|
518
|
+
const combinedReason = [options.reason, bypassNote].filter((part) => part !== undefined && part.length > 0).join(' | ');
|
|
519
|
+
const reasonForNote = combinedReason.length > 0 ? combinedReason : undefined;
|
|
520
|
+
const { updated, previousState } = updateStatusBlock(existing.content, options.newState, timestamp, reasonForNote);
|
|
473
521
|
await writeFile(existing.path, updated, 'utf8');
|
|
474
522
|
const result = {
|
|
475
523
|
role: options.role,
|
|
@@ -477,11 +525,15 @@ export async function transitionRequestArtifact(options) {
|
|
|
477
525
|
requestId: options.requestId,
|
|
478
526
|
path: existing.path,
|
|
479
527
|
state: options.newState,
|
|
528
|
+
requestType: existing.requestType,
|
|
480
529
|
previousState,
|
|
481
530
|
content: updated
|
|
482
531
|
};
|
|
483
532
|
if (existing.createdAt !== undefined) {
|
|
484
533
|
result.createdAt = existing.createdAt;
|
|
485
534
|
}
|
|
535
|
+
if (!prerequisiteResult.ok && options.allowIncomplete === true) {
|
|
536
|
+
result.bypassedPrerequisites = prerequisiteResult;
|
|
537
|
+
}
|
|
486
538
|
return result;
|
|
487
539
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type AcceptanceItem = {
|
|
2
|
+
id: string;
|
|
3
|
+
text: string;
|
|
4
|
+
line: number;
|
|
5
|
+
};
|
|
6
|
+
export type TestCase = {
|
|
7
|
+
title: string;
|
|
8
|
+
acceptanceIds: string[];
|
|
9
|
+
line: number;
|
|
10
|
+
};
|
|
11
|
+
export type CoverageEntry = {
|
|
12
|
+
acceptanceId: string;
|
|
13
|
+
acceptanceText: string;
|
|
14
|
+
testCases: string[];
|
|
15
|
+
};
|
|
16
|
+
export type AcceptanceCoverageReport = {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
prdPath: string;
|
|
19
|
+
testCasesPath: string;
|
|
20
|
+
acceptanceItems: AcceptanceItem[];
|
|
21
|
+
testCases: TestCase[];
|
|
22
|
+
coverage: CoverageEntry[];
|
|
23
|
+
uncovered: AcceptanceItem[];
|
|
24
|
+
unlinkedTestCases: TestCase[];
|
|
25
|
+
invalidReferences: Array<{
|
|
26
|
+
testCaseTitle: string;
|
|
27
|
+
reference: string;
|
|
28
|
+
}>;
|
|
29
|
+
};
|
|
30
|
+
export type AcceptanceCoverageOptions = {
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
requestId: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
};
|
|
35
|
+
export type AcceptanceCoverageError = {
|
|
36
|
+
kind: 'prd-not-found';
|
|
37
|
+
} | {
|
|
38
|
+
kind: 'test-cases-not-found';
|
|
39
|
+
expectedPath: string;
|
|
40
|
+
};
|
|
41
|
+
export declare function getAcceptanceCoverage(options: AcceptanceCoverageOptions): Promise<AcceptanceCoverageReport | AcceptanceCoverageError>;
|
|
42
|
+
export declare function isAcceptanceCoverageError(value: AcceptanceCoverageReport | AcceptanceCoverageError): value is AcceptanceCoverageError;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { pathExists, readText } from '../../shared/fs.js';
|
|
3
|
+
import { showRequestArtifact } from '../artifacts/request-artifact-service.js';
|
|
4
|
+
const ACCEPTANCE_SECTION_PATTERN = /^##\s+(?:Acceptance criteria|验收标准|Acceptance Criteria)\s*$/m;
|
|
5
|
+
function extractAcceptanceItems(prdBody) {
|
|
6
|
+
const lines = prdBody.split(/\r?\n/);
|
|
7
|
+
const startMatch = ACCEPTANCE_SECTION_PATTERN.exec(prdBody);
|
|
8
|
+
if (startMatch === null) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
// Find the line where the header starts.
|
|
12
|
+
let headerLine = -1;
|
|
13
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
14
|
+
if (ACCEPTANCE_SECTION_PATTERN.test((lines[i] ?? '') + '\n')) {
|
|
15
|
+
headerLine = i;
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
if (headerLine === -1) {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
const items = [];
|
|
23
|
+
let counter = 0;
|
|
24
|
+
for (let i = headerLine + 1; i < lines.length; i += 1) {
|
|
25
|
+
const raw = lines[i] ?? '';
|
|
26
|
+
if (/^##\s/.test(raw))
|
|
27
|
+
break; // next section
|
|
28
|
+
const bulletMatch = /^\s*-\s+(.+?)\s*$/.exec(raw);
|
|
29
|
+
if (bulletMatch === null)
|
|
30
|
+
continue;
|
|
31
|
+
const text = bulletMatch[1] ?? '';
|
|
32
|
+
if (text.length === 0)
|
|
33
|
+
continue;
|
|
34
|
+
// Skip placeholder-only bullets ("...", "<...>", etc.)
|
|
35
|
+
if (/^\.{2,}$/.test(text) || /^<[^>]+>$/.test(text))
|
|
36
|
+
continue;
|
|
37
|
+
counter += 1;
|
|
38
|
+
items.push({ id: `A${counter}`, text, line: i + 1 });
|
|
39
|
+
}
|
|
40
|
+
return items;
|
|
41
|
+
}
|
|
42
|
+
const TEST_CASE_HEADER_PATTERN = /^##\s+Test Case:\s*(.+?)\s*$/;
|
|
43
|
+
const ACCEPTANCE_FIELD_PATTERN = /^\s*-\s+\*\*Acceptance:\*\*\s+(.+?)\s*$/i;
|
|
44
|
+
function extractTestCases(qaBody) {
|
|
45
|
+
const lines = qaBody.split(/\r?\n/);
|
|
46
|
+
const cases = [];
|
|
47
|
+
let current = null;
|
|
48
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
49
|
+
const raw = lines[i] ?? '';
|
|
50
|
+
const headerMatch = TEST_CASE_HEADER_PATTERN.exec(raw);
|
|
51
|
+
if (headerMatch !== null) {
|
|
52
|
+
if (current !== null) {
|
|
53
|
+
cases.push({ title: current.title, acceptanceIds: current.ids, line: current.line });
|
|
54
|
+
}
|
|
55
|
+
current = { title: (headerMatch[1] ?? '').trim(), line: i + 1, ids: [] };
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (current === null)
|
|
59
|
+
continue;
|
|
60
|
+
const acceptanceMatch = ACCEPTANCE_FIELD_PATTERN.exec(raw);
|
|
61
|
+
if (acceptanceMatch !== null) {
|
|
62
|
+
const refs = (acceptanceMatch[1] ?? '').split(/[,\s]+/).map((part) => part.trim()).filter((part) => part.length > 0);
|
|
63
|
+
current.ids.push(...refs);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (current !== null) {
|
|
67
|
+
cases.push({ title: current.title, acceptanceIds: current.ids, line: current.line });
|
|
68
|
+
}
|
|
69
|
+
return cases;
|
|
70
|
+
}
|
|
71
|
+
function buildCoverage(items, cases) {
|
|
72
|
+
const itemMap = new Map();
|
|
73
|
+
for (const item of items)
|
|
74
|
+
itemMap.set(item.id, item);
|
|
75
|
+
const coverageMap = new Map();
|
|
76
|
+
for (const item of items)
|
|
77
|
+
coverageMap.set(item.id, []);
|
|
78
|
+
const invalidReferences = [];
|
|
79
|
+
for (const testCase of cases) {
|
|
80
|
+
for (const ref of testCase.acceptanceIds) {
|
|
81
|
+
const normalized = ref.toUpperCase();
|
|
82
|
+
if (!itemMap.has(normalized)) {
|
|
83
|
+
invalidReferences.push({ testCaseTitle: testCase.title, reference: ref });
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
coverageMap.get(normalized)?.push(testCase.title);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const coverage = items.map((item) => ({
|
|
90
|
+
acceptanceId: item.id,
|
|
91
|
+
acceptanceText: item.text,
|
|
92
|
+
testCases: coverageMap.get(item.id) ?? []
|
|
93
|
+
}));
|
|
94
|
+
const uncovered = items.filter((item) => (coverageMap.get(item.id) ?? []).length === 0);
|
|
95
|
+
return { coverage, uncovered, invalidReferences };
|
|
96
|
+
}
|
|
97
|
+
export async function getAcceptanceCoverage(options) {
|
|
98
|
+
const showOptions = {
|
|
99
|
+
projectRoot: options.projectRoot,
|
|
100
|
+
role: 'prd',
|
|
101
|
+
requestId: options.requestId
|
|
102
|
+
};
|
|
103
|
+
if (options.sessionId !== undefined) {
|
|
104
|
+
showOptions.sessionId = options.sessionId;
|
|
105
|
+
}
|
|
106
|
+
const prdArtifact = await showRequestArtifact(showOptions);
|
|
107
|
+
if (prdArtifact === null) {
|
|
108
|
+
return { kind: 'prd-not-found' };
|
|
109
|
+
}
|
|
110
|
+
const sessionId = prdArtifact.sessionId;
|
|
111
|
+
const testCasesPath = join(options.projectRoot, '.peaks', sessionId, 'qa', 'test-cases', `${options.requestId}.md`);
|
|
112
|
+
if (!(await pathExists(testCasesPath))) {
|
|
113
|
+
return { kind: 'test-cases-not-found', expectedPath: testCasesPath };
|
|
114
|
+
}
|
|
115
|
+
const qaBody = await readText(testCasesPath);
|
|
116
|
+
const acceptanceItems = extractAcceptanceItems(prdArtifact.content);
|
|
117
|
+
const testCases = extractTestCases(qaBody);
|
|
118
|
+
const { coverage, uncovered, invalidReferences } = buildCoverage(acceptanceItems, testCases);
|
|
119
|
+
const unlinkedTestCases = testCases.filter((testCase) => testCase.acceptanceIds.length === 0);
|
|
120
|
+
const ok = uncovered.length === 0 && invalidReferences.length === 0 && acceptanceItems.length > 0;
|
|
121
|
+
return {
|
|
122
|
+
ok,
|
|
123
|
+
prdPath: prdArtifact.path,
|
|
124
|
+
testCasesPath,
|
|
125
|
+
acceptanceItems,
|
|
126
|
+
testCases,
|
|
127
|
+
coverage,
|
|
128
|
+
uncovered,
|
|
129
|
+
unlinkedTestCases,
|
|
130
|
+
invalidReferences
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function isAcceptanceCoverageError(value) {
|
|
134
|
+
return value.kind !== undefined;
|
|
135
|
+
}
|