@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.
- package/CHANGELOG.md +12 -0
- package/dist/config/criteria-injection-rules.json +1 -1
- package/dist/config/dor-dod-items.json +3 -3
- package/dist/config/hook-templates/planu-spec-sanctity.sh +14 -5
- package/dist/config/server-instructions.js +1 -1
- package/dist/config/server-instructions.ts +1 -1
- package/dist/config/skill-templates/planu-new-spec.md +1 -1
- package/dist/config/skill-templates/planu-resume-work.md +1 -1
- package/dist/config/subagent-templates/planu-readiness-auditor.md +3 -3
- package/dist/config/subagent-templates/planu-spec-implementer.md +1 -1
- package/dist/config/workflow-conventions-catalog.json +3 -3
- package/dist/core/spec-api.js +1 -1
- package/dist/engine/agent-generator/builders.js +1 -1
- package/dist/engine/autopilot/bootstrap.js +0 -138
- package/dist/engine/autopilot/handlers-b2.d.ts +2 -2
- package/dist/engine/autopilot/handlers-b2.js +15 -19
- package/dist/engine/ci-generator/local-script.js +4 -4
- package/dist/engine/ci-generator/planu-steps.js +4 -5
- package/dist/engine/compliance/auto-remediator.js +0 -1
- package/dist/engine/drift/violation-resolver.js +0 -1
- package/dist/engine/health/auto-fixer.js +1 -33
- package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.d.ts +2 -2
- package/dist/engine/housekeeping/ephemeral-artifacts-cleaner.js +7 -28
- package/dist/engine/living-spec-analyzer.js +3 -34
- package/dist/engine/living-specs/index.d.ts +2 -2
- package/dist/engine/living-specs/index.js +10 -35
- package/dist/engine/planu-config-writer.js +1 -1
- package/dist/engine/progress-writer.d.ts +2 -2
- package/dist/engine/progress-writer.js +2 -2
- package/dist/engine/scan-project/index.js +2 -2
- package/dist/engine/skill-generator/skills-content.js +1 -1
- package/dist/engine/spec-format/lean-technical-generator.d.ts +1 -1
- package/dist/engine/spec-format/lean-technical-generator.js +2 -2
- package/dist/engine/spec-format/technical-md-populator.d.ts +1 -1
- package/dist/engine/spec-format/technical-md-populator.js +2 -2
- package/dist/engine/spec-migrator/drift-detector.js +1 -1
- package/dist/engine/spec-migrator/lean-migration.js +5 -4
- package/dist/engine/spec-migrator/planu-root-cleaner.js +1 -1
- package/dist/engine/spec-registry/packager.d.ts +1 -1
- package/dist/engine/spec-registry/packager.js +2 -2
- package/dist/engine/spec-registry/validator.js +1 -2
- package/dist/engine/spec-splitter.js +2 -2
- package/dist/engine/spec-summary-html/report-renderer.d.ts +3 -4
- package/dist/engine/spec-summary-html/report-renderer.js +6 -135
- package/dist/engine/spec-summary-html.js +1 -1
- package/dist/engine/universal-rules/rules/planu-english-specs.js +4 -6
- package/dist/server/routes/specs.js +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.js +1 -1
- package/dist/tools/create-spec/spec-builder.js +1 -1
- package/dist/tools/decompose-spec.js +1 -1
- package/dist/tools/git/pr-ops.js +2 -2
- package/dist/tools/git/sync-ops.js +1 -1
- package/dist/tools/reconcile-spec-living-handler.js +1 -1
- package/dist/tools/reconcile-spec.js +1 -1
- package/dist/tools/registry/install.js +27 -29
- package/dist/tools/reverse-engineer/handler.js +1 -1
- package/dist/tools/schemas/registry.js +1 -1
- package/dist/tools/spec-coverage.js +2 -2
- package/dist/tools/spec-templates.d.ts +1 -1
- package/dist/tools/spec-templates.js +17 -9
- package/dist/tools/tool-registry/group-infra.js +1 -1
- package/dist/tools/tool-registry/group-platform.js +1 -1
- package/dist/tools/update-status/index.js +1 -1
- package/dist/types/impact-detection.d.ts +6 -0
- package/dist/types/index.d.ts +0 -1
- package/dist/types/index.js +0 -1
- package/dist/types/spec-templates.d.ts +3 -5
- package/package.json +7 -7
- package/src/i18n/messages/en.json +3 -3
- package/src/i18n/messages/es.json +3 -3
- package/src/i18n/messages/pt.json +3 -3
- package/dist/engine/implementation-brief/convention-extractor.d.ts +0 -5
- package/dist/engine/implementation-brief/convention-extractor.js +0 -75
- package/dist/engine/implementation-brief/extension-points.d.ts +0 -2
- package/dist/engine/implementation-brief/extension-points.js +0 -32
- package/dist/engine/implementation-brief/generator.d.ts +0 -3
- package/dist/engine/implementation-brief/generator.js +0 -139
- package/dist/engine/implementation-brief/helpers-scanner.d.ts +0 -3
- package/dist/engine/implementation-brief/helpers-scanner.js +0 -163
- package/dist/engine/implementation-brief/test-pattern-matcher.d.ts +0 -3
- package/dist/engine/implementation-brief/test-pattern-matcher.js +0 -90
- package/dist/engine/risk-analyzer/risk-generator.d.ts +0 -6
- package/dist/engine/risk-analyzer/risk-generator.js +0 -94
- package/dist/types/implementation-brief.d.ts +0 -41
- package/dist/types/implementation-brief.js +0 -3
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
// engine/housekeeping/ephemeral-artifacts-cleaner.ts — SPEC-762
|
|
2
|
-
// Removes
|
|
3
|
-
// prompt.md)
|
|
4
|
-
|
|
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
|
|
32
|
-
*
|
|
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
|
|
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,
|
|
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
|
|
38
|
-
progressUpdated =
|
|
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
|
|
5
|
-
*
|
|
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,
|
|
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
|
-
|
|
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
|
|
84
|
-
*
|
|
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
|
|
109
|
-
const
|
|
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
|
|
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: '
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
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
|
|
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'
|
|
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,
|
|
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', '
|
|
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,
|
|
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
|
-
|
|
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.
|
|
293
|
-
progressPath:
|
|
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
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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, '&')
|
|
54
|
-
.replace(/</g, '<')
|
|
55
|
-
.replace(/>/g, '>')
|
|
56
|
-
.replace(/"/g, '"');
|
|
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">← 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
|
-
· 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
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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, '
|
|
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
|
|
9
|
+
All generated spec content is written in English, regardless of the user's conversation language:
|
|
10
10
|
|
|
11
11
|
- \`spec.md\`
|
|
12
|
-
- \`
|
|
13
|
-
-
|
|
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
|
}
|