@planu/cli 4.10.9 → 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 CHANGED
@@ -1,3 +1,10 @@
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
+
1
8
  ## [4.10.9] - 2026-07-07
2
9
 
3
10
  ### 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 section only. */
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: ~15-20 lines. Only files section with create/modify/test and pending/done status.
3
- /** Generate lean ## Technical content: YAML-like metadata + files section only. */
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)\/[\w/-]+\.[a-z]+\b/g;
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]*?(?=^##\s|$)/m.exec(body);
22
- const createSection = /###\s+Create[\s\S]*?(?=###|^##\s|$)/m.exec(filesSection?.[0] ?? body);
23
- const modifySection = /###\s+Modify[\s\S]*?(?=###|^##\s|$)/m.exec(filesSection?.[0] ?? body);
24
- if (!createSection && !modifySection) {
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
  }
@@ -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
@@ -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.9",
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.9",
38
- "@planu/core-darwin-x64": "4.10.9",
39
- "@planu/core-linux-arm64-gnu": "4.10.9",
40
- "@planu/core-linux-arm64-musl": "4.10.9",
41
- "@planu/core-linux-x64-gnu": "4.10.9",
42
- "@planu/core-linux-x64-musl": "4.10.9",
43
- "@planu/core-win32-arm64-msvc": "4.10.9",
44
- "@planu/core-win32-x64-msvc": "4.10.9"
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.0",
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.24.0",
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.10.9",
4
+ "version": "4.10.10",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
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.9",
5
+ "version": "4.10.10",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": [
8
8
  "npx",