@planu/cli 3.9.11 → 3.9.13

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 (85) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/config/criteria-injection-rules.json +1 -1
  3. package/dist/config/dor-dod-items.json +3 -3
  4. package/dist/config/hook-templates/planu-spec-sanctity.sh +14 -5
  5. package/dist/config/server-instructions.js +1 -1
  6. package/dist/config/server-instructions.ts +1 -1
  7. package/dist/config/skill-templates/planu-new-spec.md +1 -1
  8. package/dist/config/skill-templates/planu-resume-work.md +1 -1
  9. package/dist/config/subagent-templates/planu-readiness-auditor.md +3 -3
  10. package/dist/config/subagent-templates/planu-spec-implementer.md +1 -1
  11. package/dist/config/workflow-conventions-catalog.json +3 -3
  12. package/dist/core/spec-api.js +1 -1
  13. package/dist/engine/agent-generator/builders.js +1 -1
  14. package/dist/engine/autopilot/bootstrap.js +0 -138
  15. package/dist/engine/autopilot/handlers-b2.d.ts +2 -2
  16. package/dist/engine/autopilot/handlers-b2.js +15 -19
  17. package/dist/engine/ci-generator/local-script.js +4 -4
  18. package/dist/engine/ci-generator/planu-steps.js +4 -5
  19. package/dist/engine/compliance/auto-remediator.js +0 -1
  20. package/dist/engine/drift/violation-resolver.js +0 -1
  21. package/dist/engine/health/auto-fixer.js +1 -33
  22. package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.d.ts +2 -2
  23. package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.js +7 -28
  24. package/dist/engine/living-spec-analyzer.js +3 -34
  25. package/dist/engine/living-specs/index.d.ts +2 -2
  26. package/dist/engine/living-specs/index.js +10 -35
  27. package/dist/engine/planu-config-writer.js +1 -1
  28. package/dist/engine/progress-writer.d.ts +2 -2
  29. package/dist/engine/progress-writer.js +2 -2
  30. package/dist/engine/scan-project/index.js +2 -2
  31. package/dist/engine/skill-generator/skills-content.js +1 -1
  32. package/dist/engine/spec-format/lean-technical-generator.d.ts +1 -1
  33. package/dist/engine/spec-format/lean-technical-generator.js +2 -2
  34. package/dist/engine/spec-format/technical-md-populator.d.ts +1 -1
  35. package/dist/engine/spec-format/technical-md-populator.js +2 -2
  36. package/dist/engine/spec-migrator/drift-detector.js +1 -1
  37. package/dist/engine/spec-migrator/lean-migration.js +5 -4
  38. package/dist/engine/spec-migrator/planu-root-cleaner.js +1 -1
  39. package/dist/engine/spec-registry/packager.d.ts +1 -1
  40. package/dist/engine/spec-registry/packager.js +2 -2
  41. package/dist/engine/spec-registry/validator.js +1 -2
  42. package/dist/engine/spec-splitter.js +2 -2
  43. package/dist/engine/spec-summary-html/report-renderer.d.ts +3 -4
  44. package/dist/engine/spec-summary-html/report-renderer.js +6 -135
  45. package/dist/engine/spec-summary-html.js +1 -1
  46. package/dist/engine/universal-rules/rules/planu-english-specs.js +4 -6
  47. package/dist/server/routes/specs.js +1 -1
  48. package/dist/tools/create-spec/autopilot-analyzer.js +1 -1
  49. package/dist/tools/create-spec/spec-builder.js +1 -1
  50. package/dist/tools/decompose-spec.js +1 -1
  51. package/dist/tools/git/pr-ops.js +2 -2
  52. package/dist/tools/git/sync-ops.js +1 -1
  53. package/dist/tools/reconcile-spec-living-handler.js +1 -1
  54. package/dist/tools/reconcile-spec.js +1 -1
  55. package/dist/tools/registry/install.js +27 -29
  56. package/dist/tools/reverse-engineer/handler.js +1 -1
  57. package/dist/tools/schemas/registry.js +1 -1
  58. package/dist/tools/spec-coverage.js +2 -2
  59. package/dist/tools/spec-templates.d.ts +1 -1
  60. package/dist/tools/spec-templates.js +17 -9
  61. package/dist/tools/tool-registry/group-infra.js +1 -1
  62. package/dist/tools/tool-registry/group-platform.js +1 -1
  63. package/dist/tools/update-status/index.js +1 -1
  64. package/dist/types/impact-detection.d.ts +6 -0
  65. package/dist/types/index.d.ts +0 -1
  66. package/dist/types/index.js +0 -1
  67. package/dist/types/spec-templates.d.ts +3 -5
  68. package/package.json +7 -7
  69. package/src/i18n/messages/en.json +3 -3
  70. package/src/i18n/messages/es.json +3 -3
  71. package/src/i18n/messages/pt.json +3 -3
  72. package/dist/engine/implementation-brief/convention-extractor.d.ts +0 -5
  73. package/dist/engine/implementation-brief/convention-extractor.js +0 -75
  74. package/dist/engine/implementation-brief/extension-points.d.ts +0 -2
  75. package/dist/engine/implementation-brief/extension-points.js +0 -32
  76. package/dist/engine/implementation-brief/generator.d.ts +0 -3
  77. package/dist/engine/implementation-brief/generator.js +0 -139
  78. package/dist/engine/implementation-brief/helpers-scanner.d.ts +0 -3
  79. package/dist/engine/implementation-brief/helpers-scanner.js +0 -163
  80. package/dist/engine/implementation-brief/test-pattern-matcher.d.ts +0 -3
  81. package/dist/engine/implementation-brief/test-pattern-matcher.js +0 -90
  82. package/dist/engine/risk-analyzer/risk-generator.d.ts +0 -6
  83. package/dist/engine/risk-analyzer/risk-generator.js +0 -94
  84. package/dist/types/implementation-brief.d.ts +0 -41
  85. package/dist/types/implementation-brief.js +0 -3
@@ -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,7 +1,7 @@
1
1
  // engine/living-spec-analyzer.ts — Heuristic analysis of spec criteria vs codebase
2
2
  // Compares acceptance criteria against actual code to produce a living spec report.
3
3
  // No LLM calls — purely file-existence and pattern-matching heuristics.
4
- import { readFile, writeFile, stat } from 'node:fs/promises';
4
+ import { readFile, stat } from 'node:fs/promises';
5
5
  import { replaceSectionInSpec } from './spec-format/replace-section.js';
6
6
  import { join } from 'node:path';
7
7
  import { glob } from 'glob';
@@ -34,8 +34,8 @@ export async function analyzeLivingSpec(spec, projectPath) {
34
34
  progressUpdated = await updateProgressInSpec(spec.specPath, assessments, completionPercent);
35
35
  }
36
36
  else if (spec.progressPath) {
37
- // Legacy fallback: spec.md path unknown but progress.md path set
38
- progressUpdated = await updateProgressFile(spec.progressPath, assessments, completionPercent);
37
+ // Legacy progress.md paths are read-only compatibility metadata in unified spec mode.
38
+ progressUpdated = false;
39
39
  }
40
40
  return {
41
41
  specId: spec.id,
@@ -362,37 +362,6 @@ function extractSectionBodyLocal(content, sectionName) {
362
362
  const end = next ? next.index : normalized.length;
363
363
  return normalized.slice(afterHeading, end).trim();
364
364
  }
365
- /**
366
- * Update the spec's progress.md with living spec analysis results.
367
- * Legacy fallback: only called when specPath is unknown but progressPath exists.
368
- */
369
- async function updateProgressFile(progressPath, assessments, completionPercent) {
370
- try {
371
- let content;
372
- try {
373
- content = await readFile(progressPath, 'utf-8');
374
- }
375
- catch {
376
- content = '';
377
- }
378
- const timestamp = new Date().toISOString();
379
- const sectionHeader = '### Living Spec Analysis';
380
- const newSection = buildProgressSection(assessments, completionPercent, timestamp);
381
- if (content.includes(sectionHeader)) {
382
- // Replace existing section (up to next ## or end of file)
383
- content = content.replace(/### Living Spec Analysis[\s\S]*?(?=\n### |\n## |\n---\n|$)/, newSection);
384
- }
385
- else {
386
- // Append new section
387
- content = content.trimEnd() + '\n\n---\n\n' + newSection + '\n';
388
- }
389
- await writeFile(progressPath, content, 'utf-8');
390
- return true;
391
- }
392
- catch {
393
- return false;
394
- }
395
- }
396
365
  /**
397
366
  * Build the markdown section for living spec progress.
398
367
  */
@@ -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) {
@@ -57,7 +57,7 @@ export function getSpecFileNames(naming) {
57
57
  if (naming === 'legacy') {
58
58
  return { spec: 'HU.md', technical: 'FICHA-TECNICA.md', progress: 'PROGRESS.md' };
59
59
  }
60
- return { spec: 'spec.md', technical: 'technical.md', progress: 'progress.md' };
60
+ return { spec: 'spec.md', technical: 'spec.md', progress: 'spec.md' };
61
61
  }
62
62
  /**
63
63
  * Check if a specLocation uses legacy naming.
@@ -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();
@@ -101,7 +101,7 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
101
101
  });
102
102
  const specDir = join(input.path, SPEC_LOCATION, `${specId}-${slug}`);
103
103
  spec.specPath = join(specDir, 'spec.md');
104
- spec.technicalPath = join(specDir, 'technical.md');
104
+ spec.technicalPath = spec.specPath;
105
105
  const body = `# ${spec.title}\n\nAuto-generated by scan_project.\n`;
106
106
  await writeSpecFiles(specDir, spec, body);
107
107
  await createSpec(input.projectId, spec);
@@ -127,7 +127,7 @@ async function generateSpecs(input, modules, moduleResults, crossAnalysis, healt
127
127
  });
128
128
  const overviewDir = join(input.path, SPEC_LOCATION, `${overviewSpecId}-${overviewSlug}`);
129
129
  overviewSpec.specPath = join(overviewDir, 'spec.md');
130
- overviewSpec.technicalPath = join(overviewDir, 'technical.md');
130
+ overviewSpec.technicalPath = overviewSpec.specPath;
131
131
  const overviewBody = overviewContent
132
132
  ? `# Project Overview — Automated Scan\n\n${overviewContent}\n`
133
133
  : `# Project Overview — Automated Scan\n\nAuto-generated by scan_project.\n`;
@@ -43,7 +43,7 @@ export function generateDomainSkill(config, name, description, knowledge, specCo
43
43
  '1. **Understand** — Read spec criteria, review existing code, identify affected modules',
44
44
  '2. **Implement** — Write code following architecture patterns and project conventions',
45
45
  '3. **Validate** — Run tests, typecheck, lint; verify all spec criteria pass',
46
- '4. **Track** — Update PROGRESS.md or spec status with session results',
46
+ '4. **Track** — Update spec status or the inline ## Progress section with session results',
47
47
  '5. **Commit** — Commit with conventional commit message (feat/fix/refactor)',
48
48
  '',
49
49
  '## Guidelines',
@@ -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,7 +11,7 @@ const CANONICAL_ROOT_FILES = new Set([
11
11
  'roadmap.html',
12
12
  ]);
13
13
  const CANONICAL_ROOT_DIRS = new Set(['specs']);
14
- const CANONICAL_SPEC_FILES = new Set(['spec.md', 'technical.md']);
14
+ const CANONICAL_SPEC_FILES = new Set(['spec.md']);
15
15
  async function scanPlanuRoot(projectPath) {
16
16
  const rootFiles = [];
17
17
  const planuDir = join(projectPath, 'planu');
@@ -7,8 +7,10 @@ import { safeUnlink } from './git-aware-fs.js';
7
7
  import { parseFrontmatterLocal } from './frontmatter-parser.js';
8
8
  import { generateLeanSpecContent } from '../spec-format/lean-spec-generator.js';
9
9
  import { generateLeanTechnicalContent } from '../spec-format/lean-technical-generator.js';
10
+ import { buildUnifiedSpecContent } from '../spec-format/unified-spec-builder.js';
10
11
  /** Files to delete from old-format specs. */
11
12
  const OBSOLETE_FILES = [
13
+ 'technical.md',
12
14
  'progress.md',
13
15
  'executive-report.html',
14
16
  'technical-report.html',
@@ -380,7 +382,7 @@ export async function migrateSpecToLean(specDir, projectPath) {
380
382
  const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
381
383
  // Inject real criteria (replace the default one)
382
384
  const leanSpecWithCriteria = replaceCriteria(leanSpecLines, criteria);
383
- // Generate lean technical.md (estimation only in spec.md — SPEC-462)
385
+ // Generate the legacy technical body, then fold it into unified spec.md.
384
386
  const leanTech = generateLeanTechnicalContent({
385
387
  specId: id,
386
388
  filesToCreate: files.create.map((p) => ({ path: p, status: 'pending' })),
@@ -394,9 +396,8 @@ export async function migrateSpecToLean(specDir, projectPath) {
394
396
  catch (backupErr) {
395
397
  return `backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`;
396
398
  }
397
- // Write lean files
398
- await atomicWriteFile(specPath, leanSpecWithCriteria);
399
- await atomicWriteFile(techPath, leanTech);
399
+ // Write one unified spec.md.
400
+ await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecWithCriteria, leanTech));
400
401
  // Delete obsolete files
401
402
  for (const file of OBSOLETE_FILES) {
402
403
  const filePath = join(specDir, file);
@@ -14,7 +14,7 @@ const CANONICAL_ROOT_FILES = new Set([
14
14
  /** Directories allowed in planu/ root. */
15
15
  const CANONICAL_ROOT_DIRS = new Set(['specs']);
16
16
  /** Files allowed inside each planu/specs/SPEC-XXX/ directory. */
17
- const CANONICAL_SPEC_FILES = new Set(['spec.md', 'technical.md']);
17
+ const CANONICAL_SPEC_FILES = new Set(['spec.md']);
18
18
  /** Remove a file from git tracking (best-effort, silent if not a git repo or file not tracked). */
19
19
  function gitRmCached(absolutePath, cwd) {
20
20
  try {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Pack a spec directory into a gzipped archive buffer.
3
- * Only includes recognized spec files (spec.md, technical.md, progress.md, planu-registry.json).
3
+ * Only includes recognized spec files (spec.md, planu-registry.json).
4
4
  * @param specDir - Absolute path to the spec directory.
5
5
  * @returns Gzipped buffer containing the archived spec files.
6
6
  */
@@ -10,10 +10,10 @@ const gunzipAsync = promisify(gunzip);
10
10
  /** Maximum package size in bytes (500 KB). */
11
11
  const MAX_PACKAGE_SIZE = 500 * 1024;
12
12
  /** Files that may be included in a spec package. */
13
- const ALLOWED_FILES = ['spec.md', 'technical.md', 'progress.md', 'planu-registry.json'];
13
+ const ALLOWED_FILES = ['spec.md', 'planu-registry.json'];
14
14
  /**
15
15
  * Pack a spec directory into a gzipped archive buffer.
16
- * Only includes recognized spec files (spec.md, technical.md, progress.md, planu-registry.json).
16
+ * Only includes recognized spec files (spec.md, planu-registry.json).
17
17
  * @param specDir - Absolute path to the spec directory.
18
18
  * @returns Gzipped buffer containing the archived spec files.
19
19
  */
@@ -7,8 +7,7 @@ const MAX_TOTAL_SIZE = 500 * 1024;
7
7
  /** Semver regex (simplified: major.minor.patch with optional pre-release). */
8
8
  const SEMVER_RE = /^\d+\.\d+\.\d+(-[\w.]+)?$/;
9
9
  /** Required files in a publishable spec directory. */
10
- // SPEC-461: Lean format — only 2 required files (no progress.md)
11
- const REQUIRED_FILES = ['spec.md', 'technical.md'];
10
+ const REQUIRED_FILES = ['spec.md'];
12
11
  /** Required fields in the planu-registry.json manifest. */
13
12
  const REQUIRED_MANIFEST_FIELDS = [
14
13
  'name',
@@ -289,8 +289,8 @@ export function buildChildSpecs(original, proposals, nextIdFn) {
289
289
  createdAt: now,
290
290
  updatedAt: now,
291
291
  specPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
292
- technicalPath: original.technicalPath.replace(/\/[^/]+\/technical\.md$/, `/${childId}-${childSlug}/technical.md`),
293
- progressPath: original.progressPath?.replace(/\/[^/]+\/progress\.md$/, `/${childId}-${childSlug}/progress.md`),
292
+ technicalPath: original.specPath.replace(/\/[^/]+\/spec\.md$/, `/${childId}-${childSlug}/spec.md`),
293
+ progressPath: undefined,
294
294
  estimation,
295
295
  actuals: null,
296
296
  target: original.target,
@@ -1,9 +1,8 @@
1
1
  import type { Spec } from '../../types/index.js';
2
2
  /**
3
- * Regenerate executive-report.html and technical-report.html for each spec.
4
- * Reads actual spec.md and technical.md content to produce rich HTML reports.
5
- * Also generates structural reports as fallback data source.
6
- * Best-effort per spec — one failure doesn't block others.
3
+ * Per-spec HTML reports are deprecated. The unified docs site renders spec.md
4
+ * on demand, so this function intentionally does not write files inside
5
+ * planu/specs/SPEC-* directories.
7
6
  */
8
7
  export declare function regeneratePerSpecReports(specs: Spec[]): Promise<void>;
9
8
  //# sourceMappingURL=report-renderer.d.ts.map
@@ -1,139 +1,10 @@
1
- import { writeFile, readFile } from 'node:fs/promises';
2
- import { join, dirname } from 'node:path';
3
- import { generatePerSpecExecutiveReport, generatePerSpecTechnicalReport, } from '../doc-generator/per-spec-report/index.js';
4
- import { buildSpecMetricsSection, buildEffortDistributionSvg, buildSpecDependenciesSection, buildSpecTagsSection, } from '../doc-generator/per-spec-report/spec-section-builders.js';
5
- import { extractPerSpecData } from '../doc-generator/per-spec-report/spec-data-extractor.js';
6
- import { markdownToHtml } from '../markdown-renderer.js';
7
- const STATUS_COLORS = {
8
- draft: { bg: '#e5e7eb', fg: '#374151' },
9
- review: { bg: '#dbeafe', fg: '#1d4ed8' },
10
- approved: { bg: '#d1fae5', fg: '#065f46' },
11
- implementing: { bg: '#fef3c7', fg: '#92400e' },
12
- done: { bg: '#d1fae5', fg: '#065f46' },
13
- discarded: { bg: '#f3f4f6', fg: '#9ca3af' },
14
- };
15
- const REPORT_CSS = `
16
- body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; max-width: 900px; margin: 0 auto; padding: 24px; color: #1f2937; background: #fff; line-height: 1.6; font-size: 14px; }
17
- .header { border-bottom: 3px solid #4F46E5; padding-bottom: 16px; margin-bottom: 24px; }
18
- .header h1 { font-size: 1.1em; color: #6b7280; font-weight: 400; margin: 0 0 4px; }
19
- .header h2 { font-size: 1.6em; color: #111827; margin: 0; }
20
- .header .meta { margin-top: 8px; font-size: 0.85em; color: #6b7280; }
21
- .badge { display: inline-block; padding: 2px 10px; border-radius: 12px; font-size: 0.78em; font-weight: 600; }
22
- .nav { margin-bottom: 24px; }
23
- .nav a { color: #4F46E5; text-decoration: none; font-size: 0.85em; }
24
- .nav a:hover { text-decoration: underline; }
25
- .section { margin-bottom: 28px; }
26
- .section h2 { font-size: 1.15em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin-bottom: 14px; }
27
- .metric-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; margin-bottom: 16px; }
28
- .metric-card { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 14px; text-align: center; }
29
- .metric-value { font-size: 1.8em; font-weight: 700; color: #4F46E5; line-height: 1; }
30
- .metric-label { font-size: 0.78em; color: #6b7280; margin-top: 4px; }
31
- .tags-section .tag { display: inline-block; padding: 2px 8px; border-radius: 4px; background: #ede9fe; color: #5b21b6; font-size: 0.8em; margin: 2px 4px; }
32
- .md-content h1 { font-size: 1.4em; color: #111827; border-bottom: 2px solid #e5e7eb; padding-bottom: 8px; margin: 24px 0 12px; }
33
- .md-content h2 { font-size: 1.2em; color: #4F46E5; border-left: 4px solid #4F46E5; padding-left: 10px; margin: 20px 0 10px; }
34
- .md-content h3 { font-size: 1.05em; color: #374151; margin: 16px 0 8px; }
35
- .md-content p { margin: 8px 0; }
36
- .md-content ul, .md-content ol { padding-left: 24px; margin: 8px 0; }
37
- .md-content li { margin: 4px 0; }
38
- .md-content blockquote { border-left: 4px solid #d1d5db; padding: 8px 16px; margin: 12px 0; color: #6b7280; background: #f9fafb; border-radius: 0 6px 6px 0; }
39
- .md-content code { background: #f3f4f6; padding: 1px 5px; border-radius: 3px; font-size: 0.9em; }
40
- .md-content pre { background: #1f2937; color: #e5e7eb; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
41
- .md-content pre code { background: none; color: inherit; padding: 0; }
42
- .md-table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 0.85em; }
43
- .md-table th { text-align: left; padding: 8px 10px; background: #f9fafb; border-bottom: 2px solid #e5e7eb; font-weight: 600; color: #374151; }
44
- .md-table td { padding: 8px 10px; border-bottom: 1px solid #f3f4f6; }
45
- .md-table tr:hover { background: #f9fafb; }
46
- .md-content hr { border: none; border-top: 1px solid #e5e7eb; margin: 20px 0; }
47
- .md-content input[type=checkbox] { margin-right: 6px; }
48
- .footer { margin-top: 32px; padding-top: 16px; border-top: 1px solid #e5e7eb; font-size: 0.78em; color: #9ca3af; text-align: center; }
49
- .footer a { color: #4F46E5; text-decoration: none; }
50
- @media print { body { padding: 0; } .nav { display: none; } }`;
51
- function escapeHtml(text) {
52
- return text
53
- .replace(/&/g, '&amp;')
54
- .replace(/</g, '&lt;')
55
- .replace(/>/g, '&gt;')
56
- .replace(/"/g, '&quot;');
57
- }
58
- function buildMetricsDashboard(spec) {
59
- const data = extractPerSpecData(spec, [], []);
60
- return [
61
- buildSpecMetricsSection(data),
62
- buildEffortDistributionSvg(data.devHours, data.reviewHours),
63
- buildSpecDependenciesSection(data),
64
- buildSpecTagsSection(data),
65
- ]
66
- .filter(Boolean)
67
- .join('\n');
68
- }
69
- function buildReportHtml(spec, title, mdContent) {
70
- const colors = STATUS_COLORS[spec.status] ?? { bg: '#e5e7eb', fg: '#374151' };
71
- const date = new Date().toLocaleDateString('en-US', {
72
- year: 'numeric',
73
- month: 'long',
74
- day: 'numeric',
75
- });
76
- const bodyHtml = markdownToHtml(mdContent);
77
- const dashboard = buildMetricsDashboard(spec);
78
- return `<!DOCTYPE html>
79
- <html lang="en">
80
- <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0">
81
- <title>${escapeHtml(title)}</title>
82
- <style>${REPORT_CSS}</style></head>
83
- <body>
84
- <div class="nav"><a href="../../index.html">&larr; Back to Dashboard</a></div>
85
- <div class="header">
86
- <h1>Planu — Spec Report</h1>
87
- <h2>${escapeHtml(spec.id)}: ${escapeHtml(spec.title)}</h2>
88
- <div class="meta">
89
- <span class="badge" style="background:${colors.bg};color:${colors.fg}">${escapeHtml(spec.status)}</span>
90
- &middot; Generated ${escapeHtml(date)}
91
- </div>
92
- </div>
93
- ${dashboard}
94
- <div class="md-content">${bodyHtml}</div>
95
- <div class="footer">Generated by <strong>Planu</strong> — Spec Driven Development</div>
96
- </body></html>`;
97
- }
98
- async function readMdSafe(path) {
99
- try {
100
- return await readFile(path, 'utf-8');
101
- }
102
- catch {
103
- return '';
104
- }
105
- }
106
1
  /**
107
- * Regenerate executive-report.html and technical-report.html for each spec.
108
- * Reads actual spec.md and technical.md content to produce rich HTML reports.
109
- * Also generates structural reports as fallback data source.
110
- * Best-effort per spec — one failure doesn't block others.
2
+ * Per-spec HTML reports are deprecated. The unified docs site renders spec.md
3
+ * on demand, so this function intentionally does not write files inside
4
+ * planu/specs/SPEC-* directories.
111
5
  */
112
- export async function regeneratePerSpecReports(specs) {
113
- await Promise.all(specs.map(async (spec) => {
114
- if (!spec.specPath) {
115
- return;
116
- }
117
- const specDir = dirname(spec.specPath);
118
- try {
119
- const [specMd, techMd] = await Promise.all([
120
- readMdSafe(spec.specPath),
121
- readMdSafe(spec.technicalPath),
122
- ]);
123
- const execHtml = specMd
124
- ? buildReportHtml(spec, `Executive Report — ${spec.id}: ${spec.title}`, specMd)
125
- : generatePerSpecExecutiveReport(spec, [], []).content;
126
- const techHtml = techMd
127
- ? buildReportHtml(spec, `Technical Report — ${spec.id}: ${spec.title}`, techMd)
128
- : generatePerSpecTechnicalReport(spec, [], [], []).content;
129
- await Promise.all([
130
- writeFile(join(specDir, 'executive-report.html'), execHtml, 'utf-8'),
131
- writeFile(join(specDir, 'technical-report.html'), techHtml, 'utf-8'),
132
- ]);
133
- }
134
- catch {
135
- /* best-effort per spec */
136
- }
137
- }));
6
+ export function regeneratePerSpecReports(specs) {
7
+ void specs;
8
+ return Promise.resolve();
138
9
  }
139
10
  //# sourceMappingURL=report-renderer.js.map
@@ -43,7 +43,7 @@ async function scanFilesystemSpecs(projectPath) {
43
43
  createdAt: str(m.created ?? m.createdAt),
44
44
  updatedAt: str(m.updated ?? m.updatedAt),
45
45
  specPath: join(specsDir, folder, 'spec.md'),
46
- technicalPath: join(specsDir, folder, 'technical.md'),
46
+ technicalPath: join(specsDir, folder, 'spec.md'),
47
47
  estimation: {
48
48
  devHours: num(m.devHours, 8),
49
49
  reviewHours: num(m.reviewHours, 2),
@@ -6,14 +6,11 @@ Auto-generated by \`init_project\`. Do not edit manually.
6
6
 
7
7
  ## Rule
8
8
 
9
- All generated spec artifacts are written in English, regardless of the user's conversation language:
9
+ All generated spec content is written in English, regardless of the user's conversation language:
10
10
 
11
11
  - \`spec.md\`
12
- - \`technical.md\`
13
- - architecture notes inside \`planu/specs/**\`
14
- - acceptance criteria
15
- - implementation notes
16
- - validation and reconciliation notes
12
+ - inline \`## Technical\`, \`## Files\`, and \`## Progress\` sections
13
+ - acceptance criteria, implementation notes, validation notes, and reconciliation notes
17
14
 
18
15
  User-facing chat may use the user's preferred language. The spec contract itself stays in English so every agent, tool, and CI gate can parse it consistently.
19
16
 
@@ -21,6 +18,7 @@ User-facing chat may use the user's preferred language. The spec contract itself
21
18
 
22
19
  - Do not create mixed-language acceptance criteria.
23
20
  - Do not translate BDD keywords.
21
+ - Do not create standalone \`technical.md\`, \`progress.md\`, \`HU.md\`, \`FICHA-TECNICA.md\`, or \`PROGRESS.md\`.
24
22
  - Do not approve a spec that contains unresolved placeholders such as \`to be determined\`, \`TBD\`, \`TODO\`, or equivalent filler.
25
23
  `;
26
24
  }