@planu/cli 3.9.12 → 3.9.14

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 (32) hide show
  1. package/dist/config/hook-templates/planu-spec-sanctity.sh +2 -3
  2. package/dist/engine/autopilot/bootstrap.js +0 -138
  3. package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.d.ts +2 -2
  4. package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.js +7 -28
  5. package/dist/engine/living-specs/index.d.ts +2 -2
  6. package/dist/engine/living-specs/index.js +10 -35
  7. package/dist/engine/progress-writer.d.ts +2 -2
  8. package/dist/engine/progress-writer.js +2 -2
  9. package/dist/engine/spec-format/lean-technical-generator.d.ts +1 -1
  10. package/dist/engine/spec-format/lean-technical-generator.js +2 -2
  11. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -1
  12. package/dist/engine/spec-format/technical-md-populator.js +2 -2
  13. package/dist/tools/sync-spec-state-handler.js +7 -0
  14. package/dist/tools/update-status/index.js +1 -1
  15. package/dist/types/impact-detection.d.ts +6 -0
  16. package/dist/types/index.d.ts +0 -1
  17. package/dist/types/index.js +0 -1
  18. package/package.json +7 -7
  19. package/dist/engine/implementation-brief/convention-extractor.d.ts +0 -5
  20. package/dist/engine/implementation-brief/convention-extractor.js +0 -75
  21. package/dist/engine/implementation-brief/extension-points.d.ts +0 -2
  22. package/dist/engine/implementation-brief/extension-points.js +0 -32
  23. package/dist/engine/implementation-brief/generator.d.ts +0 -3
  24. package/dist/engine/implementation-brief/generator.js +0 -139
  25. package/dist/engine/implementation-brief/helpers-scanner.d.ts +0 -3
  26. package/dist/engine/implementation-brief/helpers-scanner.js +0 -163
  27. package/dist/engine/implementation-brief/test-pattern-matcher.d.ts +0 -3
  28. package/dist/engine/implementation-brief/test-pattern-matcher.js +0 -90
  29. package/dist/engine/risk-analyzer/risk-generator.d.ts +0 -6
  30. package/dist/engine/risk-analyzer/risk-generator.js +0 -94
  31. package/dist/types/implementation-brief.d.ts +0 -41
  32. package/dist/types/implementation-brief.js +0 -3
@@ -21,7 +21,6 @@ set -euo pipefail
21
21
  ALLOWED_FILES=(
22
22
  "spec.md"
23
23
  "progress.json"
24
- "implementation-brief.md"
25
24
  )
26
25
 
27
26
  # ---------------------------------------------------------------------------
@@ -104,9 +103,9 @@ EOF
104
103
  Allowed files inside planu/specs/SPEC-*/ are:
105
104
  - spec.md
106
105
  - progress.json
107
- - implementation-brief.md
108
106
 
109
- Do NOT create technical.md, progress.md, PLAN.md, NOTES.md, ADRs, or other files here.
107
+ Do NOT create technical.md, progress.md, implementation-brief.md, prompt.md,
108
+ risk-register.md, PLAN.md, NOTES.md, ADRs, or other files here.
110
109
  Keep implementation notes in conversation context or in src/.
111
110
  EOF
112
111
  exit 2
@@ -24,140 +24,6 @@ function registerSessionContextFreshnessListener() {
24
24
  })();
25
25
  });
26
26
  }
27
- /**
28
- * SPEC-586: Fire-and-forget implementation-brief generation + impact detector
29
- * on spec:status:approved. Errors are silently logged and never block the parent tool.
30
- */
31
- function registerImplementationReadyListener() {
32
- onAutopilotEvent('spec:status:approved', (event) => {
33
- if (!event.specId) {
34
- return;
35
- }
36
- const { specId, projectPath, projectId } = event;
37
- void (async () => {
38
- try {
39
- const { glob } = await import('glob');
40
- const [specFiles, techFiles] = await Promise.all([
41
- glob(`planu/specs/${specId}-*/spec.md`, { cwd: projectPath, absolute: true }),
42
- glob(`planu/specs/${specId}-*/technical.md`, { cwd: projectPath, absolute: true }),
43
- ]);
44
- const specPath = specFiles[0];
45
- const technicalPath = techFiles[0];
46
- if (!specPath) {
47
- return;
48
- }
49
- const { readFile } = await import('node:fs/promises');
50
- const specBody = await readFile(specPath, 'utf-8').catch(() => '');
51
- const tagsMatch = /^tags:\s*\[([^\]]*)\]/m.exec(specBody);
52
- const targetMatch = /^target:\s*(.+)$/m.exec(specBody);
53
- const scopeMatch = /^scope:\s*(.+)$/m.exec(specBody);
54
- const tags = tagsMatch?.[1] !== undefined
55
- ? tagsMatch[1].split(',').map((t) => t.trim().replace(/^["']|["']$/g, ''))
56
- : [];
57
- const target = targetMatch?.[1]?.trim() ?? 'backend';
58
- const scope = scopeMatch?.[1]?.trim() ?? 'cross-module';
59
- void projectId;
60
- const { generateImplementationBrief } = await import('../implementation-brief/generator.js');
61
- generateImplementationBrief({
62
- specId,
63
- specPath,
64
- projectPath,
65
- description: specBody,
66
- tags,
67
- target,
68
- scope,
69
- }).catch((err) => {
70
- const msg = err instanceof Error ? err.message : String(err);
71
- console.error(`[autopilot] implementation-brief failed for ${specId}: ${msg}`);
72
- });
73
- if (technicalPath) {
74
- const { runImpactDetector } = await import('../impact-detector/index.js');
75
- runImpactDetector({
76
- specId,
77
- specPath,
78
- technicalPath,
79
- projectPath,
80
- }).catch((err) => {
81
- const msg = err instanceof Error ? err.message : String(err);
82
- console.error(`[autopilot] impact-detector failed for ${specId}: ${msg}`);
83
- });
84
- }
85
- }
86
- catch {
87
- /* best-effort — never surface to user */
88
- }
89
- })();
90
- });
91
- }
92
- /**
93
- * SPEC-627: Fire-and-forget risk-register generation on spec:status:approved.
94
- * Writes planu/specs/SPEC-XXX/risk-register.md from spec metadata.
95
- */
96
- function registerRiskAnalyzerListener() {
97
- onAutopilotEvent('spec:status:approved', (event) => {
98
- if (!event.specId) {
99
- return;
100
- }
101
- const { specId, projectPath } = event;
102
- void (async () => {
103
- try {
104
- const { glob } = await import('glob');
105
- const specFiles = await glob(`planu/specs/${specId}-*/spec.md`, { cwd: projectPath, absolute: true });
106
- const specPath = specFiles[0];
107
- if (!specPath) {
108
- return;
109
- }
110
- const { readFile, writeFile } = await import('node:fs/promises');
111
- const specBody = await readFile(specPath, 'utf-8');
112
- const typeMatch = /^type:\s*(.+)$/m.exec(specBody);
113
- const scopeMatch = /^scope:\s*(.+)$/m.exec(specBody);
114
- const difficultyMatch = /^difficulty:\s*(\d+)/m.exec(specBody);
115
- const riskMatch = /^risk:\s*(.+)$/m.exec(specBody);
116
- const tagsMatch = /^tags:\s*\[([^\]]*)\]/m.exec(specBody);
117
- const tags = tagsMatch?.[1] !== undefined
118
- ? tagsMatch[1].split(',').map((t) => t.trim().replace(/^["']|["']$/g, ''))
119
- : [];
120
- const { generateRiskRegister } = await import('../risk-analyzer/risk-generator.js');
121
- const rawDifficulty = parseInt(difficultyMatch?.[1] ?? '2', 10);
122
- const difficulty = (rawDifficulty >= 1 && rawDifficulty <= 5 ? rawDifficulty : 2);
123
- const rawRisk = riskMatch?.[1]?.trim() ?? 'low';
124
- const riskLevel = (['low', 'medium', 'high', 'critical'].includes(rawRisk) ? rawRisk : 'low');
125
- const ctx = {
126
- specId,
127
- title: specId,
128
- type: (typeMatch?.[1]?.trim() ?? 'feature'),
129
- scope: (scopeMatch?.[1]?.trim() ?? 'feature'),
130
- difficulty,
131
- risk: riskLevel,
132
- tags,
133
- language: 'TypeScript',
134
- framework: null,
135
- dependencyCount: 0,
136
- fileCount: 0,
137
- };
138
- const register = generateRiskRegister(ctx);
139
- const lines = [
140
- `# Risk Register — ${specId}`,
141
- `_Generated ${new Date().toISOString()}_`,
142
- '',
143
- `**Aggregate score:** ${register.aggregateRiskScore}/100`,
144
- '',
145
- '## Risks',
146
- ];
147
- for (const r of register.risks) {
148
- lines.push(`- **${r.category}** (${r.probability}% probability, ${r.impactDays}d impact): ${r.description}`);
149
- lines.push(` Mitigation: ${r.mitigationPlan}`);
150
- }
151
- const { dirname, join } = await import('node:path');
152
- const registerPath = join(dirname(specPath), 'risk-register.md');
153
- await writeFile(registerPath, lines.join('\n'), 'utf-8');
154
- }
155
- catch {
156
- /* best-effort — never surface to user */
157
- }
158
- })();
159
- });
160
- }
161
27
  /**
162
28
  * SPEC-600: Fire-and-forget cascade on approved→implementing.
163
29
  * Auto-recommends model + generates orchestration plan for complex specs.
@@ -260,10 +126,6 @@ export function bootstrapAutopilotHandlers() {
260
126
  }
261
127
  // SPEC-585: Register session-context freshness listener
262
128
  registerSessionContextFreshnessListener();
263
- // SPEC-586: Register implementation-brief + impact-detector on spec:status:approved
264
- registerImplementationReadyListener();
265
- // SPEC-627: Register risk-register generator on spec:status:approved
266
- registerRiskAnalyzerListener();
267
129
  // SPEC-600: Register cascade listener for implementing transition
268
130
  // SPEC-649: registerDoneCascadeListener removed — optimize_context no longer auto-runs on done
269
131
  registerImplementingCascadeListener();
@@ -1,7 +1,7 @@
1
1
  import type { EphemeralArtifactsResult, CleanEphemeralArtifactsInput } from '../../types/housekeeping.js';
2
2
  /**
3
- * Scan all planu/specs/SPEC-* directories. For each spec whose status is
4
- * `done` or `discarded`, delete (or report) the ephemeral artifact files.
3
+ * Scan all planu/specs/SPEC-* directories and delete (or report) obsolete
4
+ * sidecar artifact files.
5
5
  */
6
6
  export declare function cleanEphemeralArtifacts(input: CleanEphemeralArtifactsInput): Promise<EphemeralArtifactsResult>;
7
7
  //# sourceMappingURL=ephemeral-artifacts-cleaner.d.ts.map
@@ -1,25 +1,16 @@
1
1
  // engine/housekeeping/ephemeral-artifacts-cleaner.ts — SPEC-762
2
- // Removes ephemeral planning artifacts (risk-register.md, implementation-brief.md,
3
- // prompt.md) from spec directories where the spec is done or discarded.
4
- import { readdir, readFile, unlink } from 'node:fs/promises';
2
+ // Removes obsolete per-spec sidecar planning artifacts (risk-register.md,
3
+ // implementation-brief.md, prompt.md). Current Planu specs keep durable content
4
+ // in spec.md; these files are no longer generated or preserved.
5
+ import { access, readdir, unlink } from 'node:fs/promises';
5
6
  import { join } from 'node:path';
6
7
  // ---------------------------------------------------------------------------
7
8
  // Constants
8
9
  // ---------------------------------------------------------------------------
9
10
  const EPHEMERAL_FILENAMES = ['risk-register.md', 'implementation-brief.md', 'prompt.md'];
10
- const TERMINAL_STATUSES = new Set(['done', 'discarded']);
11
11
  // ---------------------------------------------------------------------------
12
12
  // Helpers
13
13
  // ---------------------------------------------------------------------------
14
- /** Extract the `status:` field from a spec.md YAML frontmatter block. */
15
- function extractStatus(raw) {
16
- const frontmatterMatch = /^---\r?\n([\s\S]*?)\r?\n---/m.exec(raw);
17
- if (!frontmatterMatch?.[1]) {
18
- return null;
19
- }
20
- const statusMatch = /^status:\s*(\S+)/m.exec(frontmatterMatch[1]);
21
- return statusMatch?.[1] ?? null;
22
- }
23
14
  /** Return true when the directory name looks like a Planu spec folder. */
24
15
  function isSpecDir(name) {
25
16
  return /^SPEC-\d+/.test(name);
@@ -28,8 +19,8 @@ function isSpecDir(name) {
28
19
  // Public API
29
20
  // ---------------------------------------------------------------------------
30
21
  /**
31
- * Scan all planu/specs/SPEC-* directories. For each spec whose status is
32
- * `done` or `discarded`, delete (or report) the ephemeral artifact files.
22
+ * Scan all planu/specs/SPEC-* directories and delete (or report) obsolete
23
+ * sidecar artifact files.
33
24
  */
34
25
  export async function cleanEphemeralArtifacts(input) {
35
26
  const specsRoot = join(input.projectPath, 'planu', 'specs');
@@ -50,22 +41,10 @@ export async function cleanEphemeralArtifacts(input) {
50
41
  return result; // planu/specs/ doesn't exist — nothing to clean
51
42
  }
52
43
  for (const specDir of specDirs) {
53
- const specMdPath = join(specDir, 'spec.md');
54
- let status;
55
- try {
56
- const raw = await readFile(specMdPath, 'utf-8');
57
- status = extractStatus(raw);
58
- }
59
- catch {
60
- continue; // no spec.md — skip this directory
61
- }
62
- if (status === null || !TERMINAL_STATUSES.has(status)) {
63
- continue; // spec is still active
64
- }
65
44
  for (const filename of EPHEMERAL_FILENAMES) {
66
45
  const filePath = join(specDir, filename);
67
46
  try {
68
- await readFile(filePath, 'utf-8'); // probe existence
47
+ await access(filePath); // probe existence
69
48
  }
70
49
  catch {
71
50
  continue; // file doesn't exist — skip
@@ -1,8 +1,8 @@
1
1
  import type { LivingSpecReconcileResult } from '../../types/index.js';
2
2
  /**
3
3
  * Auto-reconcile a spec against its actual implementation files.
4
- * Reads technical.md for file paths, spec.md for criteria,
5
- * scans files, matches criteria, updates progress.md.
4
+ * Reads spec.md for file paths and criteria, scans files, matches criteria,
5
+ * and writes the auto-reconcile summary into the inline ## Progress section.
6
6
  * Never throws -- returns partial result on error.
7
7
  */
8
8
  export declare function reconcileSpec(specId: string, projectPath: string): Promise<LivingSpecReconcileResult>;
@@ -1,9 +1,10 @@
1
1
  // engine/living-specs/index.ts -- Main auto-reconcile entry point (SPEC-330)
2
- import { readFile, writeFile, access } from 'node:fs/promises';
2
+ import { readFile, access } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { glob } from 'glob';
5
5
  import { scanFile, extractFilePaths } from './file-scanner.js';
6
6
  import { extractCriteriaLines, matchCriteria } from './criteria-matcher.js';
7
+ import { replaceSectionInSpec } from '../spec-format/replace-section.js';
7
8
  /**
8
9
  * Find the spec directory for a given specId within the project.
9
10
  * Looks for planu/specs/{specId}-SLUG pattern via glob.
@@ -32,24 +33,12 @@ async function readSafe(filePath) {
32
33
  function buildDriftFlags(fileResults) {
33
34
  return fileResults.filter((f) => !f.exists).map((f) => 'DRIFT: ' + f.path + ' missing');
34
35
  }
35
- /**
36
- * Append or update the Auto-Reconcile section in progress.md.
37
- * SPEC-461: Only updates if the file already exists (lean specs don't have progress.md).
38
- */
39
- async function updateProgressMd(progressPath, result) {
40
- // SPEC-461: Don't create progress.md for lean specs — only update if it already exists
41
- try {
42
- await access(progressPath);
43
- }
44
- catch {
45
- return; // File doesn't exist — lean format spec, skip silently
46
- }
47
- const existing = await readSafe(progressPath);
36
+ function buildAutoReconcileProgressBody(result) {
48
37
  const coveragePct = result.criteriaTotal > 0 ? Math.round((result.criteriaMet / result.criteriaTotal) * 100) : 0;
49
38
  const driftSection = result.driftFlags.length > 0
50
39
  ? '\n### Drift Flags\n\n' + result.driftFlags.map((f) => '- ' + f).join('\n') + '\n'
51
40
  : '';
52
- const section = '\n## Auto-Reconcile\n\n' +
41
+ return ('### Auto-Reconcile\n\n' +
53
42
  '> Last reconciled: ' +
54
43
  result.reconciledAt +
55
44
  '\n\n' +
@@ -67,21 +56,12 @@ async function updateProgressMd(progressPath, result) {
67
56
  String(coveragePct) +
68
57
  '%)' +
69
58
  driftSection +
70
- '\n';
71
- const AUTO_RECONCILE_MARKER = '## Auto-Reconcile';
72
- let newContent;
73
- if (existing.includes(AUTO_RECONCILE_MARKER)) {
74
- newContent = existing.replace(/\n## Auto-Reconcile[\s\S]*?(?=\n## |\n$|$)/, section);
75
- }
76
- else {
77
- newContent = existing + section;
78
- }
79
- await writeFile(progressPath, newContent, 'utf-8');
59
+ '\n');
80
60
  }
81
61
  /**
82
62
  * Auto-reconcile a spec against its actual implementation files.
83
- * Reads technical.md for file paths, spec.md for criteria,
84
- * scans files, matches criteria, updates progress.md.
63
+ * Reads spec.md for file paths and criteria, scans files, matches criteria,
64
+ * and writes the auto-reconcile summary into the inline ## Progress section.
85
65
  * Never throws -- returns partial result on error.
86
66
  */
87
67
  export async function reconcileSpec(specId, projectPath) {
@@ -103,14 +83,9 @@ export async function reconcileSpec(specId, projectPath) {
103
83
  empty.driftFlags.push('DRIFT: spec directory for ' + specId + ' not found');
104
84
  return empty;
105
85
  }
106
- const technicalPath = join(specDir, 'technical.md');
107
86
  const specMdPath = join(specDir, 'spec.md');
108
- const progressPath = join(specDir, 'progress.md');
109
- const [technicalContent, specMdContent] = await Promise.all([
110
- readSafe(technicalPath),
111
- readSafe(specMdPath),
112
- ]);
113
- const relativePaths = extractFilePaths(technicalContent);
87
+ const specMdContent = await readSafe(specMdPath);
88
+ const relativePaths = extractFilePaths(specMdContent);
114
89
  const absolutePaths = relativePaths.map((p) => join(projectPath, p));
115
90
  const fileResults = await Promise.all(absolutePaths.map((p) => scanFile(p)));
116
91
  const fileResultsWithRelative = fileResults.map((fr, i) => ({
@@ -134,7 +109,7 @@ export async function reconcileSpec(specId, projectPath) {
134
109
  fileResults: fileResultsWithRelative,
135
110
  criteriaResults,
136
111
  };
137
- await updateProgressMd(progressPath, result);
112
+ await replaceSectionInSpec(specMdPath, 'Progress', buildAutoReconcileProgressBody(result));
138
113
  return result;
139
114
  }
140
115
  catch (err) {
@@ -1,10 +1,10 @@
1
1
  import type { Spec } from '../types/index.js';
2
2
  /**
3
- * Generate initial progress.md content for a newly created spec.
3
+ * Generate initial ## Progress content for a newly created spec.
4
4
  */
5
5
  export declare function generateProgressContent(spec: Spec): string;
6
6
  /**
7
- * Generate an updated progress.md reflecting a status change.
7
+ * Generate updated ## Progress content reflecting a status change.
8
8
  */
9
9
  export declare function generateProgressUpdate(spec: Spec, previousStatus: string, existingContent: string): string;
10
10
  //# sourceMappingURL=progress-writer.d.ts.map
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generate initial progress.md content for a newly created spec.
2
+ * Generate initial ## Progress content for a newly created spec.
3
3
  */
4
4
  export function generateProgressContent(spec) {
5
5
  const sections = [
@@ -10,7 +10,7 @@ export function generateProgressContent(spec) {
10
10
  return sections.map((s) => `## ${s.title}\n\n${s.content}`).join('\n\n---\n\n');
11
11
  }
12
12
  /**
13
- * Generate an updated progress.md reflecting a status change.
13
+ * Generate updated ## Progress content reflecting a status change.
14
14
  */
15
15
  export function generateProgressUpdate(spec, previousStatus, existingContent) {
16
16
  const timestamp = new Date().toISOString();
@@ -1,5 +1,5 @@
1
1
  import type { LeanFileEntry, LeanTechnicalInput } from '../../types/index.js';
2
2
  export type { LeanFileEntry, LeanTechnicalInput };
3
- /** Generate lean technical.md content: YAML frontmatter + files section only. */
3
+ /** Generate lean ## Technical content: YAML-like metadata + files section only. */
4
4
  export declare function generateLeanTechnicalContent(input: LeanTechnicalInput): string;
5
5
  //# sourceMappingURL=lean-technical-generator.d.ts.map
@@ -1,6 +1,6 @@
1
- // engine/spec-format/lean-technical-generator.ts — Generates lean technical.md (SPEC-461)
1
+ // engine/spec-format/lean-technical-generator.ts — Generates lean ## Technical content (SPEC-461)
2
2
  // Output: ~15-20 lines. Only files section with create/modify/test and pending/done status.
3
- /** Generate lean technical.md content: YAML frontmatter + files section only. */
3
+ /** Generate lean ## Technical content: YAML-like metadata + files section only. */
4
4
  export function generateLeanTechnicalContent(input) {
5
5
  const { specId, filesToCreate = [], filesToModify = [], filesToTest = [] } = input;
6
6
  const lines = ['---', `spec: ${specId}`, '---', '', '## Files', ''];
@@ -1,6 +1,6 @@
1
1
  import type { LeanFileEntry } from '../../types/index.js';
2
2
  /**
3
- * Attempt to extract file paths from spec body for auto-populating technical.md.
3
+ * Attempt to extract file paths from spec body for auto-populating ## Technical.
4
4
  * Returns null when no structured file section is found (caller uses placeholder).
5
5
  */
6
6
  export declare function extractFilesFromSpecBody(specBody: string, projectPath: string): Promise<{
@@ -1,4 +1,4 @@
1
- // engine/spec-format/technical-md-populator.ts — SPEC-586: Parse spec body to populate technical.md
1
+ // engine/spec-format/technical-md-populator.ts — SPEC-586: Parse spec body to populate ## Technical
2
2
  import { stat } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  const FILE_PATH_RE = /\b(src|tests)\/[\w/-]+\.[a-z]+\b/g;
@@ -52,7 +52,7 @@ async function categorizeByExistence(paths, projectPath) {
52
52
  return { create, modify, test: [] };
53
53
  }
54
54
  /**
55
- * Attempt to extract file paths from spec body for auto-populating technical.md.
55
+ * Attempt to extract file paths from spec body for auto-populating ## Technical.
56
56
  * Returns null when no structured file section is found (caller uses placeholder).
57
57
  */
58
58
  export async function extractFilesFromSpecBody(specBody, projectPath) {
@@ -11,6 +11,7 @@ import { handleUpdateStatus } from './update-status/index.js';
11
11
  import { parseFrontmatter } from '../engine/frontmatter-parser.js';
12
12
  import { verifyTerminalFrontmatter } from '../engine/frontmatter-sha/index.js';
13
13
  import { appendTransitionEvent } from '../storage/transition-log.js';
14
+ import { cleanEphemeralArtifacts } from '../engine/housekeeping/index.js';
14
15
  import { readFile } from 'node:fs/promises';
15
16
  // ---------------------------------------------------------------------------
16
17
  // Sync helpers (exported for startup use)
@@ -155,6 +156,12 @@ export async function startupSync() {
155
156
  catch (err) {
156
157
  console.error(`[Planu] startupSync: failed to sync project ${project.path}:`, err);
157
158
  }
159
+ try {
160
+ await cleanEphemeralArtifacts({ projectPath: project.path, dryRun: false });
161
+ }
162
+ catch (err) {
163
+ console.error(`[Planu] startupSync: failed to clean spec artifacts ${project.path}:`, err);
164
+ }
158
165
  }));
159
166
  }
160
167
  // ---------------------------------------------------------------------------
@@ -788,7 +788,7 @@ export async function handleUpdateStatus(params, server) {
788
788
  }
789
789
  // SPEC-694: Auto-orchestration plan for cross-module/architectural specs on approved
790
790
  const orchestrationPlan = await resolveOrchestrationPlan(newStatus, spec.scope, specId, effectiveGatePath);
791
- // Sync spec.md frontmatter and progress.md
791
+ // Sync spec.md frontmatter and inline ## Progress section.
792
792
  // SPEC-698: capture warning so it surfaces in the tool response (was silent before)
793
793
  const syncResult = await syncSpecFiles(updatedSpec, originalStatus, newStatus, effectiveGatePath);
794
794
  const frontmatterSyncWarnings = syncResult.warning ? [syncResult.warning] : [];
@@ -13,6 +13,12 @@ export interface TestBreakHit {
13
13
  matchedPattern: 'tool-count' | 'snapshot' | 'license-plans-sync' | 'streams-anchor';
14
14
  linePreview: string;
15
15
  }
16
+ export type AnticipatedBreakPattern = 'tool-count' | 'snapshot' | 'license-plans-sync' | 'streams-anchor' | 'other';
17
+ export interface AnticipatedBreak {
18
+ testPath: string;
19
+ pattern: AnticipatedBreakPattern;
20
+ remediation: string;
21
+ }
16
22
  export interface ImpactDetectorInput {
17
23
  specId: string;
18
24
  specPath: string;
@@ -235,7 +235,6 @@ export * from './storage.js';
235
235
  export * from './observatory.js';
236
236
  export * from './orphan-spec-refs.js';
237
237
  export * from './session-safeguard.js';
238
- export * from './implementation-brief.js';
239
238
  export * from './impact-detection.js';
240
239
  export * from './criteria-injection.js';
241
240
  export * from './gemini.js';
@@ -232,7 +232,6 @@ export * from './storage.js';
232
232
  export * from './observatory.js';
233
233
  export * from './orphan-spec-refs.js';
234
234
  export * from './session-safeguard.js';
235
- export * from './implementation-brief.js';
236
235
  export * from './impact-detection.js';
237
236
  export * from './criteria-injection.js';
238
237
  export * from './gemini.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "3.9.12",
3
+ "version": "3.9.14",
4
4
  "description": "Planu — MCP Server for Spec Driven Development with native Rust acceleration for hot paths. Cross-platform (Linux/macOS/Windows, x64/arm64, glibc/musl).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -32,12 +32,12 @@
32
32
  "packageName": "@planu/core"
33
33
  },
34
34
  "optionalDependencies": {
35
- "@planu/core-darwin-arm64": "3.9.10",
36
- "@planu/core-darwin-x64": "3.9.10",
37
- "@planu/core-linux-arm64-gnu": "3.9.10",
38
- "@planu/core-linux-arm64-musl": "3.9.10",
39
- "@planu/core-linux-x64-gnu": "3.9.10",
40
- "@planu/core-linux-x64-musl": "3.9.10"
35
+ "@planu/core-darwin-arm64": "3.9.14",
36
+ "@planu/core-darwin-x64": "3.9.14",
37
+ "@planu/core-linux-arm64-gnu": "3.9.14",
38
+ "@planu/core-linux-arm64-musl": "3.9.14",
39
+ "@planu/core-linux-x64-gnu": "3.9.14",
40
+ "@planu/core-linux-x64-musl": "3.9.14"
41
41
  },
42
42
  "engines": {
43
43
  "node": ">=24.0.0"
@@ -1,5 +0,0 @@
1
- import type { ConventionExcerpt } from '../../types/index.js';
2
- export declare function extractConventions(projectPath: string, tags: string[], target: string, scope: string): Promise<{
3
- excerpts: ConventionExcerpt[];
4
- }>;
5
- //# sourceMappingURL=convention-extractor.d.ts.map
@@ -1,75 +0,0 @@
1
- // engine/implementation-brief/convention-extractor.ts — SPEC-586: Filter .claude/rules/ by tags
2
- import { readdir, readFile } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- const MAX_EXCERPT_CHARS = 400;
5
- const MAX_RULES = 3;
6
- const TAG_TO_RULE = {
7
- backend: ['typescript-eslint.md', 'architecture.md'],
8
- frontend: ['typescript-eslint.md'],
9
- testing: ['testing.md'],
10
- test: ['testing.md'],
11
- git: ['git-workflow.md'],
12
- autopilot: ['autopilot-first.md'],
13
- spec: ['sdd-methodology.md'],
14
- parallel: ['parallel-sessions.md'],
15
- };
16
- function rulesForTags(tags, target, scope) {
17
- const selected = new Set();
18
- const combined = [...tags, target, scope].map((t) => t.toLowerCase());
19
- for (const term of combined) {
20
- for (const [key, rules] of Object.entries(TAG_TO_RULE)) {
21
- if (term.includes(key)) {
22
- for (const r of rules) {
23
- selected.add(r);
24
- }
25
- }
26
- }
27
- }
28
- if (combined.some((t) => t === 'backend' || t === 'fullstack')) {
29
- selected.add('architecture.md');
30
- }
31
- return [...selected].slice(0, MAX_RULES);
32
- }
33
- async function readExcerpt(rulesDir, ruleFile) {
34
- try {
35
- const content = await readFile(join(rulesDir, ruleFile), 'utf-8');
36
- const lines = content.split('\n');
37
- let excerpt = '';
38
- for (const line of lines) {
39
- if (excerpt.length + line.length > MAX_EXCERPT_CHARS) {
40
- break;
41
- }
42
- excerpt += line + '\n';
43
- }
44
- return excerpt.trim();
45
- }
46
- catch {
47
- return '';
48
- }
49
- }
50
- async function listRuleFiles(rulesDir) {
51
- try {
52
- const entries = await readdir(rulesDir);
53
- return entries.filter((e) => e.endsWith('.md'));
54
- }
55
- catch {
56
- return [];
57
- }
58
- }
59
- export async function extractConventions(projectPath, tags, target, scope) {
60
- const rulesDir = join(projectPath, '.claude', 'rules');
61
- const available = await listRuleFiles(rulesDir);
62
- if (available.length === 0) {
63
- return { excerpts: [] };
64
- }
65
- const wanted = rulesForTags(tags, target, scope).filter((r) => available.includes(r));
66
- const excerpts = [];
67
- for (const ruleFile of wanted) {
68
- const excerpt = await readExcerpt(rulesDir, ruleFile);
69
- if (excerpt) {
70
- excerpts.push({ rule: ruleFile, excerpt });
71
- }
72
- }
73
- return { excerpts };
74
- }
75
- //# sourceMappingURL=convention-extractor.js.map
@@ -1,2 +0,0 @@
1
- export declare function enumerateExtensionPoints(projectPath: string): Promise<string[]>;
2
- //# sourceMappingURL=extension-points.d.ts.map
@@ -1,32 +0,0 @@
1
- // engine/implementation-brief/extension-points.ts — SPEC-586: Enumerate plugin registry paths
2
- import { stat } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- const REGISTRY_PATHS = [
5
- 'src/tools/create-spec/adapters',
6
- 'src/engine/detectors',
7
- 'src/tools/generate-tests/generators',
8
- 'src/config',
9
- 'src/engine/autopilot',
10
- 'src/engine/implementation-brief',
11
- 'src/engine/impact-detector',
12
- ];
13
- async function exists(p) {
14
- try {
15
- await stat(p);
16
- return true;
17
- }
18
- catch {
19
- return false;
20
- }
21
- }
22
- export async function enumerateExtensionPoints(projectPath) {
23
- const found = [];
24
- for (const rel of REGISTRY_PATHS) {
25
- const full = join(projectPath, rel);
26
- if (await exists(full)) {
27
- found.push(rel);
28
- }
29
- }
30
- return found;
31
- }
32
- //# sourceMappingURL=extension-points.js.map
@@ -1,3 +0,0 @@
1
- import type { ImplementationBriefInput } from '../../types/index.js';
2
- export declare function generateImplementationBrief(input: ImplementationBriefInput): Promise<void>;
3
- //# sourceMappingURL=generator.d.ts.map
@@ -1,139 +0,0 @@
1
- // engine/implementation-brief/generator.ts — SPEC-586: Orchestrate implementation brief generation
2
- import { writeFile, mkdir } from 'node:fs/promises';
3
- import { join, dirname } from 'node:path';
4
- import { scanReusableHelpers } from './helpers-scanner.js';
5
- import { findTestPatterns } from './test-pattern-matcher.js';
6
- import { extractConventions } from './convention-extractor.js';
7
- import { enumerateExtensionPoints } from './extension-points.js';
8
- import { predictTestBreaks } from '../impact-detector/test-break-predictor.js';
9
- const MAX_HELPERS = 10;
10
- function renderBrief(brief) {
11
- const lines = [];
12
- lines.push(`# Implementation Brief — ${brief.specId}`);
13
- lines.push(`_Generated ${brief.generatedAt}_`);
14
- lines.push('');
15
- lines.push('## Reusable helpers');
16
- if (brief.reusableHelpers.length === 0) {
17
- lines.push('_None found._');
18
- }
19
- else {
20
- for (const h of brief.reusableHelpers) {
21
- lines.push(`- **${h.symbol}** — \`${h.path}:${h.line}\` — ${h.purpose}`);
22
- }
23
- }
24
- lines.push('');
25
- lines.push('## Test patterns');
26
- if (brief.testPatterns.length === 0) {
27
- lines.push('_No similar tests found._');
28
- }
29
- else {
30
- for (const t of brief.testPatterns) {
31
- lines.push(`- \`${t.testPath}\` — ${t.whyRelevant}`);
32
- }
33
- }
34
- lines.push('');
35
- lines.push('## Applicable conventions');
36
- if (brief.conventions.excerpts.length === 0) {
37
- lines.push('_No matching rules found._');
38
- }
39
- else {
40
- for (const e of brief.conventions.excerpts) {
41
- lines.push(`### ${e.rule}`);
42
- lines.push('');
43
- lines.push(e.excerpt);
44
- lines.push('');
45
- }
46
- }
47
- lines.push('## Existing extension points');
48
- if (brief.extensionPoints.length === 0) {
49
- lines.push('_None detected._');
50
- }
51
- else {
52
- for (const ep of brief.extensionPoints) {
53
- lines.push(`- \`${ep}\``);
54
- }
55
- }
56
- lines.push('');
57
- lines.push('## Anticipated test breaks');
58
- if (brief.anticipatedBreaks.length === 0) {
59
- lines.push('_No anticipated breaks detected._');
60
- }
61
- else {
62
- for (const b of brief.anticipatedBreaks) {
63
- lines.push(`- \`${b.testPath}\` — pattern: **${b.pattern}** — ${b.remediation}`);
64
- }
65
- }
66
- lines.push('');
67
- return lines.join('\n');
68
- }
69
- function renderPrompt(specId, description, brief) {
70
- const lines = [];
71
- lines.push(`# Implementation prompt — ${specId}`);
72
- lines.push('');
73
- // What to build — first non-empty line of description (up to 120 chars)
74
- const goal = description
75
- .split('\n')
76
- .find((l) => l.trim().length > 0)
77
- ?.slice(0, 120) ?? specId;
78
- lines.push('## What to build');
79
- lines.push(goal.trim());
80
- lines.push('');
81
- /* v8 ignore start */
82
- // Files to CREATE — infer from helpers/extension points
83
- if (brief.extensionPoints.length > 0) {
84
- lines.push('## Files to CREATE (exact paths)');
85
- for (const ep of brief.extensionPoints.slice(0, 5)) {
86
- lines.push(`- ${ep}`);
87
- }
88
- lines.push('');
89
- }
90
- // Reusable helpers — key symbols to import (avoids discovery)
91
- if (brief.reusableHelpers.length > 0) {
92
- lines.push('## Reusable helpers (import, do not rewrite)');
93
- for (const h of brief.reusableHelpers.slice(0, 5)) {
94
- lines.push(`- \`${h.symbol}\` from \`${h.path}\` — ${h.purpose}`);
95
- }
96
- lines.push('');
97
- }
98
- // Anticipated test breaks
99
- if (brief.anticipatedBreaks.length > 0) {
100
- lines.push('## Tests that will break (exact fixes)');
101
- for (const b of brief.anticipatedBreaks) {
102
- lines.push(`- \`${b.testPath}\` — ${b.remediation}`);
103
- }
104
- lines.push('');
105
- }
106
- /* v8 ignore stop */
107
- lines.push('## Verify with');
108
- lines.push('```');
109
- lines.push('pnpm typecheck && pnpm lint && pnpm test');
110
- lines.push('```');
111
- return lines.join('\n');
112
- }
113
- export async function generateImplementationBrief(input) {
114
- const { specId, specPath, projectPath, description, tags, target, scope } = input;
115
- const [helpers, testPatterns, conventions, extensionPoints, anticipatedBreaks] = await Promise.all([
116
- scanReusableHelpers(projectPath, description, tags).catch(() => []),
117
- findTestPatterns(projectPath, description, tags, scope).catch(() => []),
118
- extractConventions(projectPath, tags, target, scope).catch(() => ({ excerpts: [] })),
119
- enumerateExtensionPoints(projectPath).catch(() => []),
120
- predictTestBreaks(projectPath).catch(() => []),
121
- ]);
122
- const brief = {
123
- specId,
124
- generatedAt: new Date().toISOString(),
125
- reusableHelpers: helpers.slice(0, MAX_HELPERS),
126
- testPatterns,
127
- conventions,
128
- extensionPoints,
129
- anticipatedBreaks,
130
- };
131
- const specDir = dirname(specPath);
132
- await mkdir(specDir, { recursive: true });
133
- // Write both implementation-brief.md and prompt.md (SPEC-629) in parallel
134
- await Promise.all([
135
- writeFile(join(specDir, 'implementation-brief.md'), renderBrief(brief), 'utf-8'),
136
- writeFile(join(specDir, 'prompt.md'), renderPrompt(specId, description, brief), 'utf-8'),
137
- ]);
138
- }
139
- //# sourceMappingURL=generator.js.map
@@ -1,3 +0,0 @@
1
- import type { ReusableHelper } from '../../types/index.js';
2
- export declare function scanReusableHelpers(projectPath: string, description: string, tags: string[]): Promise<ReusableHelper[]>;
3
- //# sourceMappingURL=helpers-scanner.d.ts.map
@@ -1,163 +0,0 @@
1
- // engine/implementation-brief/helpers-scanner.ts — SPEC-586: Semantic search for reusable helpers
2
- import { readdir, readFile, stat } from 'node:fs/promises';
3
- import { join, relative, extname } from 'node:path';
4
- const MAX_FILES = 300;
5
- const MAX_DEPTH = 4;
6
- const TOP_N = 10;
7
- const CODE_EXT = new Set(['.ts', '.js', '.py', '.go', '.rs', '.java', '.kt', '.rb', '.cs']);
8
- function extractKeywords(text) {
9
- return text
10
- .toLowerCase()
11
- .replace(/[^a-z0-9\s-]/g, ' ')
12
- .split(/\s+/)
13
- .filter((w) => w.length > 3)
14
- .filter((w) => !STOP_WORDS.has(w));
15
- }
16
- const STOP_WORDS = new Set([
17
- 'this',
18
- 'that',
19
- 'with',
20
- 'from',
21
- 'have',
22
- 'will',
23
- 'when',
24
- 'then',
25
- 'given',
26
- 'spec',
27
- 'planu',
28
- 'tool',
29
- 'file',
30
- 'path',
31
- 'type',
32
- 'into',
33
- 'also',
34
- 'each',
35
- 'only',
36
- 'over',
37
- 'such',
38
- 'more',
39
- 'some',
40
- 'after',
41
- 'must',
42
- ]);
43
- function scoreText(text, keywords) {
44
- const lower = text.toLowerCase();
45
- let score = 0;
46
- for (const kw of keywords) {
47
- if (lower.includes(kw)) {
48
- score += kw.length;
49
- }
50
- }
51
- return score;
52
- }
53
- async function scanDir(dir, projectPath, keywords, results, depth, budget) {
54
- if (depth > MAX_DEPTH || budget.remaining <= 0) {
55
- return;
56
- }
57
- let entries;
58
- try {
59
- entries = await readdir(dir);
60
- }
61
- catch {
62
- return;
63
- }
64
- for (const entry of entries) {
65
- if (budget.remaining <= 0) {
66
- break;
67
- }
68
- if (entry.startsWith('.') || entry === 'node_modules' || entry === 'dist') {
69
- continue;
70
- }
71
- const full = join(dir, entry);
72
- let s;
73
- try {
74
- s = await stat(full);
75
- }
76
- catch {
77
- continue;
78
- }
79
- if (s.isDirectory()) {
80
- await scanDir(full, projectPath, keywords, results, depth + 1, budget);
81
- }
82
- else if (CODE_EXT.has(extname(entry))) {
83
- const b = budget;
84
- b.remaining -= 1;
85
- const rel = relative(projectPath, full);
86
- const score = scoreText(rel, keywords);
87
- if (score > 0) {
88
- results.push({ path: rel, score });
89
- }
90
- }
91
- }
92
- }
93
- function parseFunctionSymbol(line) {
94
- const match = /export\s+(?:async\s+)?function\s+(\w+)|export\s+const\s+(\w+)\s*=|export\s+(?:type|interface|class)\s+(\w+)/.exec(line);
95
- if (!match) {
96
- return null;
97
- }
98
- return match[1] ?? match[2] ?? match[3] ?? null;
99
- }
100
- function extractPurpose(lines, exportLine) {
101
- for (let i = exportLine - 1; i >= Math.max(0, exportLine - 3); i--) {
102
- const line = lines[i]?.trim() ?? '';
103
- if (line.startsWith('//') || line.startsWith('*') || line.startsWith('/**')) {
104
- return line.replace(/^[/*\s]+/, '').slice(0, 80);
105
- }
106
- }
107
- return 'helper function';
108
- }
109
- async function extractHelpers(filePath, keywords, projectPath) {
110
- let content;
111
- try {
112
- content = await readFile(filePath, 'utf-8');
113
- }
114
- catch {
115
- return [];
116
- }
117
- const lines = content.split('\n');
118
- const helpers = [];
119
- for (let i = 0; i < lines.length; i++) {
120
- const line = lines[i] ?? '';
121
- if (!line.includes('export')) {
122
- continue;
123
- }
124
- const symbol = parseFunctionSymbol(line);
125
- if (!symbol) {
126
- continue;
127
- }
128
- const symbolScore = scoreText(symbol, keywords);
129
- if (symbolScore === 0) {
130
- continue;
131
- }
132
- helpers.push({
133
- symbol,
134
- path: relative(projectPath, filePath),
135
- line: i + 1,
136
- purpose: extractPurpose(lines, i),
137
- });
138
- }
139
- return helpers;
140
- }
141
- export async function scanReusableHelpers(projectPath, description, tags) {
142
- const srcDir = join(projectPath, 'src');
143
- const keywords = extractKeywords(`${description} ${tags.join(' ')}`);
144
- if (keywords.length === 0) {
145
- return [];
146
- }
147
- const candidates = [];
148
- const budget = { remaining: MAX_FILES };
149
- await scanDir(srcDir, projectPath, keywords, candidates, 0, budget);
150
- candidates.sort((a, b) => b.score - a.score);
151
- const top = candidates.slice(0, 20);
152
- const helpers = [];
153
- for (const c of top) {
154
- const full = join(projectPath, c.path);
155
- const extracted = await extractHelpers(full, keywords, projectPath);
156
- helpers.push(...extracted);
157
- if (helpers.length >= TOP_N) {
158
- break;
159
- }
160
- }
161
- return helpers.slice(0, TOP_N);
162
- }
163
- //# sourceMappingURL=helpers-scanner.js.map
@@ -1,3 +0,0 @@
1
- import type { TestPatternMatch } from '../../types/index.js';
2
- export declare function findTestPatterns(projectPath: string, description: string, tags: string[], scope: string): Promise<TestPatternMatch[]>;
3
- //# sourceMappingURL=test-pattern-matcher.d.ts.map
@@ -1,90 +0,0 @@
1
- // engine/implementation-brief/test-pattern-matcher.ts — SPEC-586: Find similar test patterns
2
- import { readdir, stat } from 'node:fs/promises';
3
- import { join, relative } from 'node:path';
4
- const MAX_TEST_FILES = 200;
5
- const TOP_N = 3;
6
- function extractKeywords(text) {
7
- return text
8
- .toLowerCase()
9
- .replace(/[^a-z0-9\s]/g, ' ')
10
- .split(/\s+/)
11
- .filter((w) => w.length > 3);
12
- }
13
- function scoreMatch(filePath, keywords) {
14
- const lower = filePath.toLowerCase();
15
- let score = 0;
16
- for (const kw of keywords) {
17
- if (lower.includes(kw)) {
18
- score += kw.length;
19
- }
20
- }
21
- return score;
22
- }
23
- async function collectTestFiles(dir, results, budget, depth) {
24
- if (budget.remaining <= 0 || depth > 5) {
25
- return;
26
- }
27
- let entries;
28
- try {
29
- entries = await readdir(dir);
30
- }
31
- catch {
32
- return;
33
- }
34
- for (const entry of entries) {
35
- if (budget.remaining <= 0) {
36
- break;
37
- }
38
- if (entry.startsWith('.') || entry === 'node_modules') {
39
- continue;
40
- }
41
- const full = join(dir, entry);
42
- let s;
43
- try {
44
- s = await stat(full);
45
- }
46
- catch {
47
- continue;
48
- }
49
- if (s.isDirectory()) {
50
- await collectTestFiles(full, results, budget, depth + 1);
51
- }
52
- else if (entry.endsWith('.test.ts') || entry.endsWith('.test.js')) {
53
- const b = budget;
54
- b.remaining -= 1;
55
- results.push(full);
56
- }
57
- }
58
- }
59
- function explainRelevance(filePath, keywords) {
60
- const matched = [];
61
- const lower = filePath.toLowerCase();
62
- for (const kw of keywords) {
63
- if (lower.includes(kw)) {
64
- matched.push(kw);
65
- }
66
- }
67
- return matched.length > 0
68
- ? `matches keywords: ${matched.slice(0, 3).join(', ')}`
69
- : 'structural similarity';
70
- }
71
- export async function findTestPatterns(projectPath, description, tags, scope) {
72
- const testsDir = join(projectPath, 'tests');
73
- const keywords = extractKeywords(`${description} ${tags.join(' ')} ${scope}`);
74
- if (keywords.length === 0) {
75
- return [];
76
- }
77
- const testFiles = [];
78
- const budget = { remaining: MAX_TEST_FILES };
79
- await collectTestFiles(testsDir, testFiles, budget, 0);
80
- const scored = testFiles
81
- .map((f) => ({ path: f, score: scoreMatch(relative(projectPath, f), keywords) }))
82
- .filter((x) => x.score > 0)
83
- .sort((a, b) => b.score - a.score)
84
- .slice(0, TOP_N);
85
- return scored.map((s) => ({
86
- testPath: relative(projectPath, s.path),
87
- whyRelevant: explainRelevance(relative(projectPath, s.path), keywords),
88
- }));
89
- }
90
- //# sourceMappingURL=test-pattern-matcher.js.map
@@ -1,6 +0,0 @@
1
- import type { RiskAnalysisContext, RiskRegister } from '../../types/index.js';
2
- /**
3
- * Generate an initial risk register from spec metadata.
4
- */
5
- export declare function generateRiskRegister(ctx: RiskAnalysisContext, totalBudget?: number): RiskRegister;
6
- //# sourceMappingURL=risk-generator.d.ts.map
@@ -1,94 +0,0 @@
1
- function buildRiskTemplates(ctx) {
2
- const templates = [];
3
- if (ctx.difficulty >= 4) {
4
- templates.push({
5
- description: 'High complexity may lead to estimation overrun',
6
- category: 'technical',
7
- probability: 40,
8
- impactDays: 3,
9
- mitigationPlan: 'Break down into smaller tasks and reassess estimates',
10
- });
11
- }
12
- if (ctx.scope === 'cross-module' || ctx.scope === 'architectural') {
13
- templates.push({
14
- description: 'Cross-module changes risk breaking existing integrations',
15
- category: 'integration',
16
- probability: 35,
17
- impactDays: 2,
18
- mitigationPlan: 'Add integration tests covering all affected modules before merging',
19
- });
20
- }
21
- if (ctx.dependencyCount > 3) {
22
- templates.push({
23
- description: 'Multiple dependencies increase coordination overhead',
24
- category: 'dependency',
25
- probability: 30,
26
- impactDays: 2,
27
- mitigationPlan: 'Establish dependency contracts early and monitor for version conflicts',
28
- });
29
- }
30
- if (ctx.risk === 'high' || ctx.risk === 'critical') {
31
- templates.push({
32
- description: 'Spec flagged as high-risk requires extra review',
33
- category: 'organizational',
34
- probability: 50,
35
- impactDays: 3,
36
- mitigationPlan: 'Schedule mandatory peer review and add approval gates before deployment',
37
- });
38
- }
39
- if (ctx.tags.includes('security')) {
40
- templates.push({
41
- description: 'Security-sensitive changes require audit',
42
- category: 'security',
43
- probability: 25,
44
- impactDays: 4,
45
- mitigationPlan: 'Engage security team for review and run SAST/DAST tools',
46
- });
47
- }
48
- if (ctx.fileCount > 10) {
49
- templates.push({
50
- description: 'Large number of files increases merge conflict risk',
51
- category: 'technical',
52
- probability: 30,
53
- impactDays: 1,
54
- mitigationPlan: 'Use feature flags and incremental PRs to reduce merge surface',
55
- });
56
- }
57
- return templates;
58
- }
59
- function buildRisks(ctx, templates) {
60
- return templates.map((t, index) => ({
61
- id: `R${index + 1}`,
62
- description: t.description,
63
- category: t.category,
64
- probability: t.probability,
65
- impactUsd: 0,
66
- impactDays: t.impactDays,
67
- owner: '',
68
- status: 'identified',
69
- mitigationPlan: t.mitigationPlan,
70
- specId: ctx.specId,
71
- }));
72
- }
73
- function computeAggregateScore(risks, totalBudgetDays) {
74
- const rawScore = risks.reduce((acc, r) => acc + (r.probability / 100) * r.impactDays, 0);
75
- const denominator = Math.max(totalBudgetDays, 1);
76
- const score = (rawScore / denominator) * 100;
77
- return Math.min(score, 100);
78
- }
79
- /**
80
- * Generate an initial risk register from spec metadata.
81
- */
82
- export function generateRiskRegister(ctx, totalBudget) {
83
- const budgetDays = totalBudget ?? 30;
84
- const templates = buildRiskTemplates(ctx);
85
- const risks = buildRisks(ctx, templates);
86
- const aggregateRiskScore = computeAggregateScore(risks, budgetDays);
87
- return {
88
- specId: ctx.specId,
89
- risks,
90
- aggregateRiskScore,
91
- generatedAt: new Date().toISOString(),
92
- };
93
- }
94
- //# sourceMappingURL=risk-generator.js.map
@@ -1,41 +0,0 @@
1
- export interface ReusableHelper {
2
- symbol: string;
3
- path: string;
4
- line: number;
5
- purpose: string;
6
- }
7
- export interface TestPatternMatch {
8
- testPath: string;
9
- whyRelevant: string;
10
- }
11
- export interface ConventionExcerpt {
12
- rule: string;
13
- excerpt: string;
14
- }
15
- export type AnticipatedBreakPattern = 'tool-count' | 'snapshot' | 'license-plans-sync' | 'streams-anchor' | 'other';
16
- export interface AnticipatedBreak {
17
- testPath: string;
18
- pattern: AnticipatedBreakPattern;
19
- remediation: string;
20
- }
21
- export interface ImplementationBrief {
22
- specId: string;
23
- generatedAt: string;
24
- reusableHelpers: ReusableHelper[];
25
- testPatterns: TestPatternMatch[];
26
- conventions: {
27
- excerpts: ConventionExcerpt[];
28
- };
29
- extensionPoints: string[];
30
- anticipatedBreaks: AnticipatedBreak[];
31
- }
32
- export interface ImplementationBriefInput {
33
- specId: string;
34
- specPath: string;
35
- projectPath: string;
36
- description: string;
37
- tags: string[];
38
- target: string;
39
- scope: string;
40
- }
41
- //# sourceMappingURL=implementation-brief.d.ts.map
@@ -1,3 +0,0 @@
1
- // types/implementation-brief.ts — SPEC-586: Implementation brief types
2
- export {};
3
- //# sourceMappingURL=implementation-brief.js.map