@planu/cli 4.10.8 → 4.10.10
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 +18 -0
- package/dist/engine/spec-format/lean-technical-generator.d.ts +1 -1
- package/dist/engine/spec-format/lean-technical-generator.js +14 -3
- package/dist/engine/spec-format/technical-md-populator.js +17 -7
- package/dist/engine/spec-format/unified-spec-builder.js +10 -1
- package/dist/engine/validator.js +6 -4
- package/dist/tools/create-spec.js +18 -1
- package/dist/tools/validate-structured-compat.d.ts +19 -0
- package/dist/tools/validate-structured-compat.js +14 -0
- package/dist/tools/validate.js +13 -0
- package/dist/types/spec-format.d.ts +1 -0
- package/package.json +11 -11
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [4.10.10] - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(create-spec): merge readiness-compliant spec generation
|
|
5
|
+
- fix(create-spec): generate readiness-compliant technical details
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## [4.10.9] - 2026-07-07
|
|
9
|
+
|
|
10
|
+
### Bug Fixes
|
|
11
|
+
- fix: promote SPEC-1115 validation scorer evidence
|
|
12
|
+
- fix: merge SPEC-1115 validation scorer evidence
|
|
13
|
+
- fix(SPEC-1115): require complete traceability evidence
|
|
14
|
+
|
|
15
|
+
### Chores
|
|
16
|
+
- chore: refresh native dependency lock metadata
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
## [4.10.8] - 2026-07-07
|
|
2
20
|
|
|
3
21
|
### Refactoring
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LeanFileEntry, LeanTechnicalInput } from '../../types/index.js';
|
|
2
2
|
export type { LeanFileEntry, LeanTechnicalInput };
|
|
3
|
-
/** Generate lean ## Technical content: YAML-like metadata + files
|
|
3
|
+
/** Generate lean ## Technical content: YAML-like metadata + files and verification notes. */
|
|
4
4
|
export declare function generateLeanTechnicalContent(input: LeanTechnicalInput): string;
|
|
5
5
|
//# sourceMappingURL=lean-technical-generator.d.ts.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
// engine/spec-format/lean-technical-generator.ts — Generates lean ## Technical content (SPEC-461)
|
|
2
|
-
// Output:
|
|
3
|
-
/** Generate lean ## Technical content: YAML-like metadata + files
|
|
2
|
+
// Output: lean files section with enough detail for readiness to evaluate concrete scope.
|
|
3
|
+
/** Generate lean ## Technical content: YAML-like metadata + files and verification notes. */
|
|
4
4
|
export function generateLeanTechnicalContent(input) {
|
|
5
|
-
const { specId, filesToCreate = [], filesToModify = [], filesToTest = [] } = input;
|
|
5
|
+
const { specId, filesToCreate = [], filesToModify = [], filesToTest = [], includeDecisionRequiredPlaceholder = false, } = input;
|
|
6
6
|
const lines = ['---', `spec: ${specId}`, '---'];
|
|
7
7
|
if (filesToCreate.length === 0 && filesToModify.length === 0 && filesToTest.length === 0) {
|
|
8
|
+
if (includeDecisionRequiredPlaceholder) {
|
|
9
|
+
lines.push('', '## Files', '', '- Decision required: identify expected source and test files before implementation.', '', '## Implementation Notes', '', '- File inference did not find grounded `src/` or `tests/` paths.', '- Do not fabricate authoritative paths; update this section during review with concrete files.');
|
|
10
|
+
}
|
|
8
11
|
return lines.join('\n');
|
|
9
12
|
}
|
|
10
13
|
lines.push('', '## Files', '');
|
|
@@ -29,6 +32,14 @@ export function generateLeanTechnicalContent(input) {
|
|
|
29
32
|
}
|
|
30
33
|
lines.push('');
|
|
31
34
|
}
|
|
35
|
+
lines.push('## Implementation Notes', '');
|
|
36
|
+
lines.push('- Keep implementation scoped to the files listed above unless review updates this spec.');
|
|
37
|
+
if (filesToTest.length > 0) {
|
|
38
|
+
lines.push(`- Verification starts with: \`pnpm vitest run ${filesToTest.map((file) => file.path).join(' ')}\`.`);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
lines.push('- Decision required: add at least one concrete automated test path before approval.');
|
|
42
|
+
}
|
|
32
43
|
return lines.join('\n');
|
|
33
44
|
}
|
|
34
45
|
//# sourceMappingURL=lean-technical-generator.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
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
|
-
const FILE_PATH_RE = /\b(src|tests)\/[
|
|
4
|
+
const FILE_PATH_RE = /\b(?:src|tests)\/[a-zA-Z0-9/_.-]+\.[a-z]+\b/g;
|
|
5
5
|
function extractPathsFromSection(sectionText) {
|
|
6
6
|
const paths = [];
|
|
7
7
|
const listRe = /^-\s+`?(src\/[\w./-]+|tests\/[\w./-]+)`?/gm;
|
|
@@ -18,10 +18,12 @@ function extractPathsFromSection(sectionText) {
|
|
|
18
18
|
return paths;
|
|
19
19
|
}
|
|
20
20
|
function parseSectionedFileLists(body) {
|
|
21
|
-
const filesSection = /##\s+Files\s+likely\s+to\s+change[\s\S]*?(
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
|
|
21
|
+
const filesSection = /##\s+Files\s+likely\s+to\s+change[\s\S]*?(?=\n##\s|$)/.exec(body);
|
|
22
|
+
const scopedBody = filesSection?.[0] ?? body;
|
|
23
|
+
const createSection = /###\s+Create[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
|
|
24
|
+
const modifySection = /###\s+Modify[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
|
|
25
|
+
const testSection = /###\s+Test[\s\S]*?(?=\n###|\n##\s|$)/.exec(scopedBody);
|
|
26
|
+
if (!createSection && !modifySection && !testSection) {
|
|
25
27
|
return null;
|
|
26
28
|
}
|
|
27
29
|
return {
|
|
@@ -33,13 +35,21 @@ function parseSectionedFileLists(body) {
|
|
|
33
35
|
path: p,
|
|
34
36
|
status: 'pending',
|
|
35
37
|
})),
|
|
36
|
-
test: []
|
|
38
|
+
test: extractPathsFromSection(testSection?.[0] ?? '').map((p) => ({
|
|
39
|
+
path: p,
|
|
40
|
+
status: 'pending',
|
|
41
|
+
})),
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
async function categorizeByExistence(paths, projectPath) {
|
|
40
45
|
const create = [];
|
|
41
46
|
const modify = [];
|
|
47
|
+
const test = [];
|
|
42
48
|
for (const p of paths) {
|
|
49
|
+
if (p.startsWith('tests/')) {
|
|
50
|
+
test.push({ path: p, status: 'pending' });
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
43
53
|
const full = join(projectPath, p);
|
|
44
54
|
try {
|
|
45
55
|
await stat(full);
|
|
@@ -49,7 +59,7 @@ async function categorizeByExistence(paths, projectPath) {
|
|
|
49
59
|
create.push({ path: p, status: 'pending' });
|
|
50
60
|
}
|
|
51
61
|
}
|
|
52
|
-
return { create, modify, test
|
|
62
|
+
return { create, modify, test };
|
|
53
63
|
}
|
|
54
64
|
/**
|
|
55
65
|
* Attempt to extract file paths from spec body for auto-populating ## Technical.
|
|
@@ -27,6 +27,7 @@ export function buildUnifiedSpecContent(leanSpecBody, leanTechnicalBody) {
|
|
|
27
27
|
// Split the generated technical body into its constituent sections so we can
|
|
28
28
|
// inject only the ones the user did not provide.
|
|
29
29
|
const generatedFiles = extractSection(technicalBodyRaw, 'Files');
|
|
30
|
+
const generatedImplementationNotes = extractSection(technicalBodyRaw, 'Implementation Notes');
|
|
30
31
|
// Generated technical body always starts with `## Files`. If the user already
|
|
31
32
|
// authored both, return the spec body untouched.
|
|
32
33
|
if (userHasTechnical && userHasFiles) {
|
|
@@ -37,13 +38,21 @@ export function buildUnifiedSpecContent(leanSpecBody, leanTechnicalBody) {
|
|
|
37
38
|
sectionsToAppend.push('## Technical', '');
|
|
38
39
|
}
|
|
39
40
|
if (!userHasFiles && generatedFiles !== null) {
|
|
40
|
-
sectionsToAppend.push(generatedFiles);
|
|
41
|
+
sectionsToAppend.push(demoteTopLevelHeadings(generatedFiles));
|
|
42
|
+
}
|
|
43
|
+
if (!userHasTechnical && generatedImplementationNotes !== null) {
|
|
44
|
+
sectionsToAppend.push('', demoteTopLevelHeadings(generatedImplementationNotes));
|
|
41
45
|
}
|
|
42
46
|
if (sectionsToAppend.length === 0) {
|
|
43
47
|
return leanSpecBody.endsWith('\n') ? leanSpecBody : `${leanSpecBody}\n`;
|
|
44
48
|
}
|
|
45
49
|
return `${leanSpecBody.trimEnd()}\n\n${sectionsToAppend.join('\n').trimEnd()}\n`;
|
|
46
50
|
}
|
|
51
|
+
function demoteTopLevelHeadings(section) {
|
|
52
|
+
return section.replace(/^(#{2,6})(\s+\S.*)$/gm, (_match, hashes, rest) => {
|
|
53
|
+
return `${hashes}#${rest}`;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
47
56
|
function stripFencedCodeBlocks(body) {
|
|
48
57
|
return body.replace(/```[\s\S]*?```/g, '').replace(/~~~[\s\S]*?~~~/g, '');
|
|
49
58
|
}
|
package/dist/engine/validator.js
CHANGED
|
@@ -22,14 +22,16 @@ function normalizeCriterionForEvidence(value) {
|
|
|
22
22
|
.toLowerCase();
|
|
23
23
|
}
|
|
24
24
|
function rowHasValidationEvidence(row) {
|
|
25
|
-
const
|
|
25
|
+
const hasImplementationEvidence = [
|
|
26
|
+
row.scenario?.trim(),
|
|
26
27
|
row.testEvidence?.length,
|
|
27
28
|
row.contractEvidence?.length,
|
|
28
29
|
row.manualEvidence?.trim(),
|
|
29
|
-
row.validationEvidence?.trim(),
|
|
30
|
-
row.reviewerEvidence?.trim(),
|
|
31
30
|
].some((value) => Boolean(value));
|
|
32
|
-
return row.changedFiles.length > 0 &&
|
|
31
|
+
return (row.changedFiles.length > 0 &&
|
|
32
|
+
hasImplementationEvidence &&
|
|
33
|
+
Boolean(row.validationEvidence?.trim()) &&
|
|
34
|
+
Boolean(row.reviewerEvidence?.trim()));
|
|
33
35
|
}
|
|
34
36
|
async function readTraceabilityEvidence(spec, projectPath) {
|
|
35
37
|
try {
|
|
@@ -113,13 +113,29 @@ function handleClarification(_server, description, knowledge, autopilot, params)
|
|
|
113
113
|
function hasFileEntries(files) {
|
|
114
114
|
return files.create.length + files.modify.length + files.test.length > 0;
|
|
115
115
|
}
|
|
116
|
+
function mergeTechnicalFiles(primary, fallback) {
|
|
117
|
+
return {
|
|
118
|
+
create: mergeFileEntries(primary.create, fallback.create),
|
|
119
|
+
modify: mergeFileEntries(primary.modify, fallback.modify),
|
|
120
|
+
test: mergeFileEntries(primary.test, fallback.test),
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
function mergeFileEntries(primary, fallback) {
|
|
124
|
+
const byPath = new Map();
|
|
125
|
+
for (const file of [...primary, ...fallback]) {
|
|
126
|
+
if (!byPath.has(file.path)) {
|
|
127
|
+
byPath.set(file.path, file);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return [...byPath.values()];
|
|
131
|
+
}
|
|
116
132
|
async function resolveTechnicalFiles(input) {
|
|
117
133
|
const generatedSource = [input.generatedTechnicalSection, input.generatedSpecBody]
|
|
118
134
|
.filter((part) => part.trim().length > 0)
|
|
119
135
|
.join('\n\n');
|
|
120
136
|
const extracted = await extractFilesFromSpecBody(generatedSource, input.projectPath);
|
|
121
137
|
if (extracted !== null && hasFileEntries(extracted)) {
|
|
122
|
-
return extracted;
|
|
138
|
+
return mergeTechnicalFiles(extracted, input.autopilot.suggestedFiles);
|
|
123
139
|
}
|
|
124
140
|
if (input.fallbackReason !== undefined && hasFileEntries(input.autopilot.suggestedFiles)) {
|
|
125
141
|
return input.autopilot.suggestedFiles;
|
|
@@ -687,6 +703,7 @@ export async function handleCreateSpec(inputParams, server) {
|
|
|
687
703
|
filesToCreate: groundedTechnical.files.create,
|
|
688
704
|
filesToModify: groundedTechnical.files.modify,
|
|
689
705
|
filesToTest: groundedTechnical.files.test,
|
|
706
|
+
includeDecisionRequiredPlaceholder: spec.scope !== 'trivial',
|
|
690
707
|
});
|
|
691
708
|
// SPEC-709: write unified spec.md from origin — no separate technical.md.
|
|
692
709
|
// The legacy two-file output is preserved by appending the technical body
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
interface ValidateStructuredCompatInput {
|
|
2
|
+
structured: Record<string, unknown>;
|
|
3
|
+
fieldsImplemented: number;
|
|
4
|
+
fieldsTotal: number;
|
|
5
|
+
matches: string[];
|
|
6
|
+
missing: string[];
|
|
7
|
+
extra: string[];
|
|
8
|
+
qualityIssues: unknown[];
|
|
9
|
+
conventionViolations: {
|
|
10
|
+
category: string;
|
|
11
|
+
file: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
message: string;
|
|
14
|
+
severity: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
export declare function applyValidateStructuredCompat(input: ValidateStructuredCompatInput): void;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=validate-structured-compat.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function applyValidateStructuredCompat(input) {
|
|
2
|
+
const structured = input.structured;
|
|
3
|
+
structured.fieldsImplemented = input.fieldsImplemented;
|
|
4
|
+
structured.fieldsTotal = input.fieldsTotal;
|
|
5
|
+
structured.matches = input.matches;
|
|
6
|
+
structured.missing = input.missing;
|
|
7
|
+
structured.extra = input.extra;
|
|
8
|
+
structured.qualityIssues = input.qualityIssues;
|
|
9
|
+
const conventionCompliance = structured.conventionCompliance;
|
|
10
|
+
if (conventionCompliance !== undefined) {
|
|
11
|
+
conventionCompliance.violations = input.conventionViolations;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=validate-structured-compat.js.map
|
package/dist/tools/validate.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable max-lines -- validate coordinates multiple legacy gate payloads in one tool. */
|
|
1
2
|
// tools/validate.ts — Compare spec vs implementation (SPEC-595: elicitation on failures)
|
|
2
3
|
// Uses the validator engine to check coverage, missing items, quality issues.
|
|
3
4
|
import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
|
|
@@ -19,6 +20,7 @@ import { buildMinimalityReport } from './validate-minimality.js';
|
|
|
19
20
|
import { buildValidateFailureFallback, validateStrictLayoutOrError } from './validate-helpers.js';
|
|
20
21
|
import { runLintCheck } from './validate-lint.js';
|
|
21
22
|
import { checkPlanuUncommittedChanges, runAssuranceGates } from './validate-assurance.js';
|
|
23
|
+
import { applyValidateStructuredCompat } from './validate-structured-compat.js';
|
|
22
24
|
// Re-export for external use (SPEC-018)
|
|
23
25
|
export { validateContractCompliance };
|
|
24
26
|
// SPEC-595: server? param enables elicitation on failures
|
|
@@ -487,6 +489,7 @@ function buildValidateStructuredContent(args) {
|
|
|
487
489
|
conventionCompliance: {
|
|
488
490
|
totalViolations: args.conventionCompliance.totalViolations,
|
|
489
491
|
regressionDetected: args.conventionCompliance.regressionDetected,
|
|
492
|
+
violations: args.conventionCompliance.violations,
|
|
490
493
|
violationsTop5: args.conventionCompliance.violations.slice(0, 5),
|
|
491
494
|
},
|
|
492
495
|
lintCheck: {
|
|
@@ -522,6 +525,16 @@ function buildValidateStructuredContent(args) {
|
|
|
522
525
|
});
|
|
523
526
|
const lintCheck = structured.lintCheck;
|
|
524
527
|
lintCheck.output = args.lintCheck.output ?? '';
|
|
528
|
+
applyValidateStructuredCompat({
|
|
529
|
+
structured,
|
|
530
|
+
fieldsImplemented: args.compactSummary.passed,
|
|
531
|
+
fieldsTotal: args.compactSummary.total,
|
|
532
|
+
matches: args.result.matches,
|
|
533
|
+
missing: args.result.missing,
|
|
534
|
+
extra: args.result.extra,
|
|
535
|
+
qualityIssues: args.qualityIssues,
|
|
536
|
+
conventionViolations: args.conventionCompliance.violations,
|
|
537
|
+
});
|
|
525
538
|
return structured;
|
|
526
539
|
}
|
|
527
540
|
function buildCompactValidateText(args) {
|
|
@@ -132,6 +132,7 @@ export interface LeanTechnicalInput {
|
|
|
132
132
|
filesToCreate?: LeanFileEntry[];
|
|
133
133
|
filesToModify?: LeanFileEntry[];
|
|
134
134
|
filesToTest?: LeanFileEntry[];
|
|
135
|
+
includeDecisionRequiredPlaceholder?: boolean;
|
|
135
136
|
}
|
|
136
137
|
export interface ImplementationContractInput {
|
|
137
138
|
description: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.10",
|
|
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",
|
|
@@ -34,14 +34,14 @@
|
|
|
34
34
|
"packageName": "@planu/core"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@planu/core-darwin-arm64": "4.10.
|
|
38
|
-
"@planu/core-darwin-x64": "4.10.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.10.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.10.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.10.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.10.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.10.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.10.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.10.10",
|
|
38
|
+
"@planu/core-darwin-x64": "4.10.10",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.10.10",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.10.10",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.10.10",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.10.10",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.10.10",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.10.10"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -144,7 +144,7 @@
|
|
|
144
144
|
"@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
|
|
145
145
|
"@stryker-mutator/core": "^9.6.1",
|
|
146
146
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
147
|
-
"@supabase/supabase-js": "^2.110.
|
|
147
|
+
"@supabase/supabase-js": "^2.110.1",
|
|
148
148
|
"@types/node": "^26.1.0",
|
|
149
149
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
150
150
|
"@vitest/coverage-v8": "^4.1.10",
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
"happy-dom": "^20.10.6",
|
|
157
157
|
"husky": "^9.1.7",
|
|
158
158
|
"javascript-obfuscator": "^5.4.3",
|
|
159
|
-
"knip": "^6.
|
|
159
|
+
"knip": "^6.25.0",
|
|
160
160
|
"lint-staged": "^17.0.8",
|
|
161
161
|
"madge": "^8.0.0",
|
|
162
162
|
"prettier": "^3.9.4",
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.10.
|
|
5
|
+
"version": "4.10.10",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|