peaks-cli 1.0.16 → 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.
Files changed (37) hide show
  1. package/bin/peaks.js +0 -0
  2. package/dist/src/cli/commands/request-commands.js +109 -3
  3. package/dist/src/cli/commands/scan-commands.d.ts +3 -0
  4. package/dist/src/cli/commands/scan-commands.js +194 -0
  5. package/dist/src/cli/commands/workspace-commands.d.ts +3 -0
  6. package/dist/src/cli/commands/workspace-commands.js +32 -0
  7. package/dist/src/cli/program.js +4 -0
  8. package/dist/src/services/artifacts/artifact-lint-service.d.ts +23 -0
  9. package/dist/src/services/artifacts/artifact-lint-service.js +80 -0
  10. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +28 -0
  11. package/dist/src/services/artifacts/artifact-prerequisites.js +77 -0
  12. package/dist/src/services/artifacts/repair-cycle-service.d.ts +23 -0
  13. package/dist/src/services/artifacts/repair-cycle-service.js +52 -0
  14. package/dist/src/services/artifacts/request-artifact-service.d.ts +14 -0
  15. package/dist/src/services/artifacts/request-artifact-service.js +73 -21
  16. package/dist/src/services/scan/acceptance-coverage-service.d.ts +42 -0
  17. package/dist/src/services/scan/acceptance-coverage-service.js +135 -0
  18. package/dist/src/services/scan/archetype-service.d.ts +5 -0
  19. package/dist/src/services/scan/archetype-service.js +253 -0
  20. package/dist/src/services/scan/diff-scope-service.d.ts +40 -0
  21. package/dist/src/services/scan/diff-scope-service.js +198 -0
  22. package/dist/src/services/scan/existing-system-service.d.ts +7 -0
  23. package/dist/src/services/scan/existing-system-service.js +300 -0
  24. package/dist/src/services/scan/scan-types.d.ts +59 -0
  25. package/dist/src/services/scan/scan-types.js +1 -0
  26. package/dist/src/services/scan/type-sanity-service.d.ts +23 -0
  27. package/dist/src/services/scan/type-sanity-service.js +108 -0
  28. package/dist/src/services/workspace/workspace-service.d.ts +16 -0
  29. package/dist/src/services/workspace/workspace-service.js +66 -0
  30. package/dist/src/shared/version.d.ts +1 -1
  31. package/dist/src/shared/version.js +1 -1
  32. package/package.json +1 -1
  33. package/skills/peaks-qa/SKILL.md +42 -0
  34. package/skills/peaks-rd/SKILL.md +65 -2
  35. package/skills/peaks-solo/SKILL.md +389 -263
  36. package/skills/peaks-solo/references/existing-system-extraction.md +78 -0
  37. package/skills/peaks-solo/results.tsv +0 -1
@@ -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: feature | bug | refactor | clarification
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: feature | bug | refactor | clarification
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: feature | bug | refactor | clarification
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: feature | bug | refactor | clarification
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 extractStateAndCreated(markdown) {
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
- return createdAt === undefined ? { state } : { state, createdAt };
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 } = extractStateAndCreated(body);
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 { updated, previousState } = updateStatusBlock(existing.content, options.newState, timestamp, options.reason);
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
+ }
@@ -0,0 +1,5 @@
1
+ import type { ArchetypeReport } from './scan-types.js';
2
+ export type ArchetypeScanOptions = {
3
+ projectRoot: string;
4
+ };
5
+ export declare function scanArchetype(options: ArchetypeScanOptions): Promise<ArchetypeReport>;