@planu/cli 4.10.9 → 4.10.11
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 +13 -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/validation-report-writer.js +6 -2
- package/dist/tools/create-spec.js +18 -1
- package/dist/tools/validate-lint.js +1 -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 +68 -21
- 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,16 @@
|
|
|
1
|
+
## [4.10.11] - 2026-07-08
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix(validate): honor executable spec compliance score
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
## [4.10.10] - 2026-07-08
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
- fix(create-spec): merge readiness-compliant spec generation
|
|
11
|
+
- fix(create-spec): generate readiness-compliant technical details
|
|
12
|
+
|
|
13
|
+
|
|
1
14
|
## [4.10.9] - 2026-07-07
|
|
2
15
|
|
|
3
16
|
### Bug Fixes
|
|
@@ -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
|
}
|
|
@@ -2,8 +2,9 @@ import { appendArtifact, readArtifact } from '../handoff-artifacts/io.js';
|
|
|
2
2
|
import { runSpecCompliance, } from './spec-compliance-runner.js';
|
|
3
3
|
export async function writeImplementationReviewReport(input) {
|
|
4
4
|
const specCompliance = await runSpecCompliance(input.spec, input.projectPath);
|
|
5
|
+
const reportScore = resolveExecutableValidationScore(input.score, specCompliance);
|
|
5
6
|
const gates = buildGates({
|
|
6
|
-
score:
|
|
7
|
+
score: reportScore,
|
|
7
8
|
lintPassed: input.lintPassed,
|
|
8
9
|
conventionRegression: input.conventionRegression ?? false,
|
|
9
10
|
specCompliance,
|
|
@@ -25,7 +26,7 @@ export async function writeImplementationReviewReport(input) {
|
|
|
25
26
|
reviewer,
|
|
26
27
|
specCompliance: reportCompliance,
|
|
27
28
|
minimalityReport: input.minimalityReport,
|
|
28
|
-
score:
|
|
29
|
+
score: reportScore ?? undefined,
|
|
29
30
|
completedAt: new Date().toISOString(),
|
|
30
31
|
};
|
|
31
32
|
try {
|
|
@@ -56,6 +57,9 @@ export async function writeImplementationReviewReport(input) {
|
|
|
56
57
|
};
|
|
57
58
|
}
|
|
58
59
|
}
|
|
60
|
+
function resolveExecutableValidationScore(score, specCompliance) {
|
|
61
|
+
return specCompliance.perScenario.length > 0 ? specCompliance.dimensionScore : score;
|
|
62
|
+
}
|
|
59
63
|
async function preserveUnobservedGates(input, nextGates) {
|
|
60
64
|
const currentGateNames = new Set(nextGates.map((gate) => gate.name));
|
|
61
65
|
const explicitlyObserved = new Set([
|
|
@@ -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
|
|
@@ -6,7 +6,7 @@ import { dirname, join } from 'node:path';
|
|
|
6
6
|
import { formatLintFailureDiagnostics, resolveProjectCommandPlan, runProjectCommandPlan, } from './validate-runtime.js';
|
|
7
7
|
/** Allowlist: alphanumerics, spaces, and safe shell chars. Rejects ; | $ ` & < > */
|
|
8
8
|
const SAFE_COMMAND_RE = /^[\w\s./:@~=-]+$/;
|
|
9
|
-
const DEFAULT_LINT_CHECK_TIMEOUT_MS =
|
|
9
|
+
const DEFAULT_LINT_CHECK_TIMEOUT_MS = 60_000;
|
|
10
10
|
const LINT_CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000;
|
|
11
11
|
function isSafeCommand(cmd) {
|
|
12
12
|
return SAFE_COMMAND_RE.test(cmd);
|
|
@@ -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
|
|
@@ -97,23 +99,24 @@ export async function handleValidate(args, server) {
|
|
|
97
99
|
conventionRegression: regressionDetected,
|
|
98
100
|
minimalityReport: minimalityReport ?? undefined,
|
|
99
101
|
});
|
|
102
|
+
const effectiveResult = buildEffectiveValidationResult(result, validationReport.specCompliance);
|
|
100
103
|
const qualityGateSummary = buildQualityGateSummary({
|
|
101
104
|
conventionViolations,
|
|
102
105
|
regressionDetected,
|
|
103
106
|
lintPassed: lintCheck.passed,
|
|
104
107
|
assurancePassed: assuranceGates.newCode.passed,
|
|
105
108
|
minimalityBlocked: minimalityReport?.blocked === true,
|
|
106
|
-
score:
|
|
109
|
+
score: effectiveResult.score,
|
|
107
110
|
});
|
|
108
111
|
const output = {
|
|
109
112
|
specId,
|
|
110
113
|
title: spec.title,
|
|
111
|
-
score:
|
|
112
|
-
fieldsImplemented:
|
|
113
|
-
fieldsTotal:
|
|
114
|
-
matches:
|
|
115
|
-
missing:
|
|
116
|
-
extra:
|
|
114
|
+
score: effectiveResult.score,
|
|
115
|
+
fieldsImplemented: effectiveResult.fieldsImplemented,
|
|
116
|
+
fieldsTotal: effectiveResult.fieldsTotal,
|
|
117
|
+
matches: effectiveResult.matches,
|
|
118
|
+
missing: effectiveResult.missing,
|
|
119
|
+
extra: effectiveResult.extra,
|
|
117
120
|
qualityIssues: result.qualityIssues.map((issue) => ({
|
|
118
121
|
file: issue.file,
|
|
119
122
|
line: issue.line,
|
|
@@ -200,7 +203,7 @@ export async function handleValidate(args, server) {
|
|
|
200
203
|
}
|
|
201
204
|
// Auto-suggest corrective actions based on validation results
|
|
202
205
|
// score is null when no criteria can be extracted — treat as 0 for comparisons.
|
|
203
|
-
const scoreValue =
|
|
206
|
+
const scoreValue = effectiveResult.score ?? 0;
|
|
204
207
|
const suggestions = [];
|
|
205
208
|
if (scoreValue < 50) {
|
|
206
209
|
suggestions.push('Critical: Many acceptance criteria not implemented. Review spec requirements.');
|
|
@@ -214,8 +217,8 @@ export async function handleValidate(args, server) {
|
|
|
214
217
|
if (!dod.isDone && spec.status === 'implementing') {
|
|
215
218
|
suggestions.push('Implementation incomplete. Review DoD checklist before marking as done.');
|
|
216
219
|
}
|
|
217
|
-
if (
|
|
218
|
-
suggestions.push(`${
|
|
220
|
+
if (effectiveResult.missing.length > 0) {
|
|
221
|
+
suggestions.push(`${effectiveResult.missing.length} criteria still missing. Run detect_drift to identify gaps.`);
|
|
219
222
|
}
|
|
220
223
|
if (graphCoverage.gaps.length > 0) {
|
|
221
224
|
suggestions.push(`${graphCoverage.gaps.length} graph-backed coverage gap(s) found without broad repository scan.`);
|
|
@@ -241,8 +244,8 @@ export async function handleValidate(args, server) {
|
|
|
241
244
|
specId,
|
|
242
245
|
input: { specType: spec.type, scope: spec.scope },
|
|
243
246
|
outcome: {
|
|
244
|
-
score:
|
|
245
|
-
missingCount:
|
|
247
|
+
score: effectiveResult.score,
|
|
248
|
+
missingCount: effectiveResult.missing.length,
|
|
246
249
|
qualityIssueCount: result.qualityIssues.length,
|
|
247
250
|
dorPassed: dor.isReady,
|
|
248
251
|
dodPassed: dod.isDone,
|
|
@@ -259,7 +262,7 @@ export async function handleValidate(args, server) {
|
|
|
259
262
|
type: 'validate',
|
|
260
263
|
specId,
|
|
261
264
|
input: { trigger: 'auto-quality-learn', rules: topIssueRules },
|
|
262
|
-
outcome: { score:
|
|
265
|
+
outcome: { score: effectiveResult.score, antipatterns: topIssueRules },
|
|
263
266
|
delta: { qualityGap: 100 - scoreValue },
|
|
264
267
|
timestamp: new Date().toISOString(),
|
|
265
268
|
}).catch((err) => {
|
|
@@ -275,10 +278,10 @@ export async function handleValidate(args, server) {
|
|
|
275
278
|
? '\n\n📁 HTML reports updated — commit to save: `git add planu/specs/ planu/conventions.json && git commit -m "chore(planu): update reports"`'
|
|
276
279
|
: '';
|
|
277
280
|
// SPEC-512: Compact output — only surface failures, cap to 200 tokens
|
|
278
|
-
const totalCriteria =
|
|
279
|
-
const passedCount =
|
|
281
|
+
const totalCriteria = effectiveResult.fieldsTotal;
|
|
282
|
+
const passedCount = effectiveResult.fieldsImplemented;
|
|
280
283
|
const failedCount = totalCriteria - passedCount;
|
|
281
|
-
const indeterminate =
|
|
284
|
+
const indeterminate = effectiveResult.indeterminate ?? [];
|
|
282
285
|
const effectiveScore = qualityGateSummary.effectiveScore ?? scoreValue;
|
|
283
286
|
const compactText = buildCompactValidateText({
|
|
284
287
|
score: effectiveScore,
|
|
@@ -286,20 +289,20 @@ export async function handleValidate(args, server) {
|
|
|
286
289
|
failedCount,
|
|
287
290
|
totalCriteria,
|
|
288
291
|
qualityFailures: qualityGateSummary.failures,
|
|
289
|
-
missing:
|
|
292
|
+
missing: effectiveResult.missing,
|
|
290
293
|
indeterminate,
|
|
291
294
|
commitReminder,
|
|
292
295
|
planuReminder,
|
|
293
296
|
});
|
|
294
297
|
const graphText = formatGraphCoverageText(graphCoverage);
|
|
295
298
|
// SPEC-512: Compact structuredContent — essential fields only at top level
|
|
296
|
-
const compactSummary = buildCompactSummary(specId,
|
|
299
|
+
const compactSummary = buildCompactSummary(specId, effectiveResult.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
|
|
297
300
|
const structuredBase = buildValidateStructuredContent({
|
|
298
301
|
specId,
|
|
299
302
|
title: spec.title,
|
|
300
|
-
score:
|
|
303
|
+
score: effectiveResult.score === null ? null : effectiveScore,
|
|
301
304
|
compactSummary,
|
|
302
|
-
result,
|
|
305
|
+
result: effectiveResult,
|
|
303
306
|
indeterminate,
|
|
304
307
|
dor,
|
|
305
308
|
dod,
|
|
@@ -382,7 +385,6 @@ function buildQualityGateSummary(input) {
|
|
|
382
385
|
const blockingConventionViolations = input.conventionViolations.filter((violation) => violation.severity === 'critical');
|
|
383
386
|
const failures = [
|
|
384
387
|
...(input.regressionDetected ? ['convention-regression'] : []),
|
|
385
|
-
...(blockingConventionViolations.length > 0 ? ['blocking-convention-violations'] : []),
|
|
386
388
|
...(!input.lintPassed ? ['lint'] : []),
|
|
387
389
|
...(!input.assurancePassed ? ['assurance'] : []),
|
|
388
390
|
...(input.minimalityBlocked ? ['minimality'] : []),
|
|
@@ -398,6 +400,40 @@ function buildQualityGateSummary(input) {
|
|
|
398
400
|
minimalityPassed: !input.minimalityBlocked,
|
|
399
401
|
};
|
|
400
402
|
}
|
|
403
|
+
function buildExecutableCoverage(specCompliance) {
|
|
404
|
+
if (specCompliance.scenarios.length === 0) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const matches = specCompliance.scenarios
|
|
408
|
+
.filter((scenario) => scenario.verdict === 'pass')
|
|
409
|
+
.map((scenario) => scenario.title);
|
|
410
|
+
const missing = specCompliance.scenarios
|
|
411
|
+
.filter((scenario) => scenario.verdict !== 'pass')
|
|
412
|
+
.map((scenario) => scenario.title);
|
|
413
|
+
return {
|
|
414
|
+
score: specCompliance.score,
|
|
415
|
+
fieldsImplemented: matches.length,
|
|
416
|
+
fieldsTotal: specCompliance.scenarios.length,
|
|
417
|
+
matches,
|
|
418
|
+
missing,
|
|
419
|
+
indeterminate: [],
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
function buildEffectiveValidationResult(result, specCompliance) {
|
|
423
|
+
const executableCoverage = buildExecutableCoverage(specCompliance);
|
|
424
|
+
if (executableCoverage === null) {
|
|
425
|
+
return result;
|
|
426
|
+
}
|
|
427
|
+
return {
|
|
428
|
+
...result,
|
|
429
|
+
score: executableCoverage.score,
|
|
430
|
+
fieldsImplemented: executableCoverage.fieldsImplemented,
|
|
431
|
+
fieldsTotal: executableCoverage.fieldsTotal,
|
|
432
|
+
matches: executableCoverage.matches,
|
|
433
|
+
missing: executableCoverage.missing,
|
|
434
|
+
indeterminate: executableCoverage.indeterminate,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
401
437
|
function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria) {
|
|
402
438
|
return {
|
|
403
439
|
specId,
|
|
@@ -487,6 +523,7 @@ function buildValidateStructuredContent(args) {
|
|
|
487
523
|
conventionCompliance: {
|
|
488
524
|
totalViolations: args.conventionCompliance.totalViolations,
|
|
489
525
|
regressionDetected: args.conventionCompliance.regressionDetected,
|
|
526
|
+
violations: args.conventionCompliance.violations,
|
|
490
527
|
violationsTop5: args.conventionCompliance.violations.slice(0, 5),
|
|
491
528
|
},
|
|
492
529
|
lintCheck: {
|
|
@@ -522,6 +559,16 @@ function buildValidateStructuredContent(args) {
|
|
|
522
559
|
});
|
|
523
560
|
const lintCheck = structured.lintCheck;
|
|
524
561
|
lintCheck.output = args.lintCheck.output ?? '';
|
|
562
|
+
applyValidateStructuredCompat({
|
|
563
|
+
structured,
|
|
564
|
+
fieldsImplemented: args.compactSummary.passed,
|
|
565
|
+
fieldsTotal: args.compactSummary.total,
|
|
566
|
+
matches: args.result.matches,
|
|
567
|
+
missing: args.result.missing,
|
|
568
|
+
extra: args.result.extra,
|
|
569
|
+
qualityIssues: args.qualityIssues,
|
|
570
|
+
conventionViolations: args.conventionCompliance.violations,
|
|
571
|
+
});
|
|
525
572
|
return structured;
|
|
526
573
|
}
|
|
527
574
|
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.11",
|
|
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.11",
|
|
38
|
+
"@planu/core-darwin-x64": "4.10.11",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.10.11",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.10.11",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.10.11",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.10.11",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.10.11",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.10.11"
|
|
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.11",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|