@planu/cli 4.4.0 → 4.4.1
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 +10 -0
- package/dist/engine/evidence-gates/artifact-reader.js +10 -5
- package/dist/engine/spec-migrator/planu-canonical-policy.d.ts +1 -0
- package/dist/engine/spec-migrator/planu-canonical-policy.js +7 -0
- package/dist/engine/spec-migrator/strict-planu-cleanup.d.ts +2 -2
- package/dist/engine/spec-migrator/strict-planu-cleanup.js +35 -20
- package/dist/tools/validate.js +5 -3
- package/dist/types/spec-format.d.ts +5 -0
- package/package.json +11 -11
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -50,13 +50,18 @@ const ContractValidationSchema = z.object({
|
|
|
50
50
|
summary: z.string().optional(),
|
|
51
51
|
});
|
|
52
52
|
function resolveSpecPath(spec, projectPath) {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
const specPath = spec.specPath;
|
|
54
|
+
if (!specPath?.trim()) {
|
|
55
|
+
return null;
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
if (isAbsolute(specPath) || !projectPath) {
|
|
58
|
+
return specPath;
|
|
59
|
+
}
|
|
60
|
+
return join(projectPath, specPath);
|
|
57
61
|
}
|
|
58
62
|
function specEvidencePath(spec, filename, projectPath) {
|
|
59
|
-
|
|
63
|
+
const specPath = resolveSpecPath(spec, projectPath);
|
|
64
|
+
return specPath ? join(dirname(specPath), 'evidence', filename) : null;
|
|
60
65
|
}
|
|
61
66
|
function handoffEvidencePath(projectId, specId, filename) {
|
|
62
67
|
return join(projectDataDir(projectId), 'handoffs', specId, filename);
|
|
@@ -81,7 +86,7 @@ async function readUnknown(paths) {
|
|
|
81
86
|
}
|
|
82
87
|
async function readOptional(args) {
|
|
83
88
|
try {
|
|
84
|
-
const found = await readUnknown(args.paths);
|
|
89
|
+
const found = await readUnknown(args.paths.filter((path) => Boolean(path)));
|
|
85
90
|
if (!found) {
|
|
86
91
|
return undefined;
|
|
87
92
|
}
|
|
@@ -3,6 +3,7 @@ export declare const PLANU_CANONICAL_POLICY: PlanuCanonicalPathPolicy;
|
|
|
3
3
|
export declare function isCanonicalPlanuRootFile(name: string): boolean;
|
|
4
4
|
export declare function isCanonicalPlanuRootDir(name: string): boolean;
|
|
5
5
|
export declare function isCanonicalSpecFile(name: string): boolean;
|
|
6
|
+
export declare function isCanonicalSpecDir(name: string): boolean;
|
|
6
7
|
export declare function mustMergeBeforeDeleteSpecFile(name: string): boolean;
|
|
7
8
|
export declare function isCanonicalReleaseFile(relativeToPlanu: string): boolean;
|
|
8
9
|
export declare function canonicalContractText(): string;
|
|
@@ -4,6 +4,7 @@ export const PLANU_CANONICAL_POLICY = {
|
|
|
4
4
|
canonicalRootFiles: ['conventions.json', 'context.md', 'session-context.md', 'session.json'],
|
|
5
5
|
canonicalRootDirs: ['releases', 'specs'],
|
|
6
6
|
canonicalSpecFiles: ['spec.md'],
|
|
7
|
+
canonicalSpecDirs: ['evidence'],
|
|
7
8
|
forbiddenHostAssetRootDirs: ['agents', 'skills', 'rules', 'hooks'],
|
|
8
9
|
generatedRuntimePatterns: [
|
|
9
10
|
'planu/index.html',
|
|
@@ -38,6 +39,7 @@ export const PLANU_CANONICAL_POLICY = {
|
|
|
38
39
|
const ROOT_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootFiles);
|
|
39
40
|
const ROOT_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalRootDirs);
|
|
40
41
|
const SPEC_FILE_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecFiles);
|
|
42
|
+
const SPEC_DIR_SET = new Set(PLANU_CANONICAL_POLICY.canonicalSpecDirs);
|
|
41
43
|
const LEGACY_MERGE_SET = new Set(PLANU_CANONICAL_POLICY.legacyMergeBeforeDeleteFiles);
|
|
42
44
|
export function isCanonicalPlanuRootFile(name) {
|
|
43
45
|
return ROOT_FILE_SET.has(name);
|
|
@@ -48,6 +50,9 @@ export function isCanonicalPlanuRootDir(name) {
|
|
|
48
50
|
export function isCanonicalSpecFile(name) {
|
|
49
51
|
return SPEC_FILE_SET.has(name);
|
|
50
52
|
}
|
|
53
|
+
export function isCanonicalSpecDir(name) {
|
|
54
|
+
return SPEC_DIR_SET.has(name);
|
|
55
|
+
}
|
|
51
56
|
export function mustMergeBeforeDeleteSpecFile(name) {
|
|
52
57
|
return LEGACY_MERGE_SET.has(name);
|
|
53
58
|
}
|
|
@@ -66,6 +71,8 @@ export function canonicalContractText() {
|
|
|
66
71
|
' specs/',
|
|
67
72
|
' SPEC-XXX-slug/',
|
|
68
73
|
' spec.md',
|
|
74
|
+
' evidence/',
|
|
75
|
+
' *.json',
|
|
69
76
|
'',
|
|
70
77
|
'Host adapters are written outside planu/:',
|
|
71
78
|
' Claude Code: .claude/agents, .claude/skills, .claude/rules',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { StrictPlanuCleanupResult, StrictPlanuValidationResult } from '../../types/index.js';
|
|
1
|
+
import type { StrictPlanuCleanupResult, StrictPlanuValidationOptions, StrictPlanuValidationResult } from '../../types/index.js';
|
|
2
2
|
import { PLANU_CANONICAL_POLICY } from './planu-canonical-policy.js';
|
|
3
3
|
export declare function runStrictPlanuCleanup(projectPath: string): Promise<StrictPlanuCleanupResult>;
|
|
4
|
-
export declare function validateStrictPlanuLayout(projectPath: string): Promise<StrictPlanuValidationResult>;
|
|
4
|
+
export declare function validateStrictPlanuLayout(projectPath: string, options?: StrictPlanuValidationOptions): Promise<StrictPlanuValidationResult>;
|
|
5
5
|
export { PLANU_CANONICAL_POLICY };
|
|
6
6
|
//# sourceMappingURL=strict-planu-cleanup.d.ts.map
|
|
@@ -4,10 +4,10 @@ import { readdir, readFile, rm, stat } from 'node:fs/promises';
|
|
|
4
4
|
import { existsSync } from 'node:fs';
|
|
5
5
|
import { execFile } from 'node:child_process';
|
|
6
6
|
import { promisify } from 'node:util';
|
|
7
|
-
import { join, relative } from 'node:path';
|
|
7
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
8
8
|
import { atomicWriteFile } from '../safety/atomic-write-file.js';
|
|
9
9
|
import { safeUnlink } from './git-aware-fs.js';
|
|
10
|
-
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalReleaseFile, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
10
|
+
import { PLANU_CANONICAL_POLICY, canonicalContractText, isCanonicalPlanuRootDir, isCanonicalPlanuRootFile, isCanonicalReleaseFile, isCanonicalSpecDir, isCanonicalSpecFile, mustMergeBeforeDeleteSpecFile, } from './planu-canonical-policy.js';
|
|
11
11
|
const execFileAsync = promisify(execFile);
|
|
12
12
|
async function pathIsDirectory(path) {
|
|
13
13
|
try {
|
|
@@ -125,7 +125,9 @@ async function walkSpecDirectory(projectPath, specDir, result) {
|
|
|
125
125
|
}
|
|
126
126
|
continue;
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
const isDir = await pathIsDirectory(full);
|
|
129
|
+
if (entry === 'reference' ||
|
|
130
|
+
(isDir ? !isCanonicalSpecDir(entry) : !isCanonicalSpecFile(entry))) {
|
|
129
131
|
await removePath(projectPath, full);
|
|
130
132
|
result.deleted.push(relative(projectPath, full));
|
|
131
133
|
}
|
|
@@ -173,33 +175,46 @@ export async function runStrictPlanuCleanup(projectPath) {
|
|
|
173
175
|
result.gitignoreUpdated = await updateGitignore(projectPath);
|
|
174
176
|
return result;
|
|
175
177
|
}
|
|
176
|
-
|
|
178
|
+
function resolveSpecDirsForValidation(projectPath, specPath) {
|
|
179
|
+
if (!specPath?.trim()) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
const resolved = isAbsolute(specPath) ? specPath : join(projectPath, specPath);
|
|
183
|
+
return [dirname(resolved)];
|
|
184
|
+
}
|
|
185
|
+
export async function validateStrictPlanuLayout(projectPath, options = {}) {
|
|
177
186
|
const offenders = [];
|
|
178
187
|
const planuDir = join(projectPath, 'planu');
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(
|
|
185
|
-
|
|
188
|
+
if (options.includeRoot !== false) {
|
|
189
|
+
const entries = await readdir(planuDir).catch(() => []);
|
|
190
|
+
for (const entry of entries) {
|
|
191
|
+
const full = join(planuDir, entry);
|
|
192
|
+
const isDir = await pathIsDirectory(full);
|
|
193
|
+
if ((isDir && !isCanonicalPlanuRootDir(entry)) ||
|
|
194
|
+
(!isDir && !isCanonicalPlanuRootFile(entry))) {
|
|
195
|
+
offenders.push(relative(projectPath, full));
|
|
196
|
+
}
|
|
186
197
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
198
|
+
for (const entry of await readdir(join(planuDir, 'releases')).catch(() => [])) {
|
|
199
|
+
const rel = `releases/${entry}`;
|
|
200
|
+
if (!isCanonicalReleaseFile(rel)) {
|
|
201
|
+
offenders.push(`planu/${rel}`);
|
|
202
|
+
}
|
|
192
203
|
}
|
|
193
204
|
}
|
|
194
|
-
|
|
195
|
-
|
|
205
|
+
const scopedSpecDirs = resolveSpecDirsForValidation(projectPath, options.specPath) ??
|
|
206
|
+
(await readdir(join(planuDir, 'specs')).catch(() => [])).map((specDir) => join(planuDir, 'specs', specDir));
|
|
207
|
+
for (const full of scopedSpecDirs) {
|
|
208
|
+
const specDir = relative(join(planuDir, 'specs'), full);
|
|
196
209
|
if (!(await pathIsDirectory(full)) || specDir === 'data') {
|
|
197
210
|
offenders.push(relative(projectPath, full));
|
|
198
211
|
continue;
|
|
199
212
|
}
|
|
200
213
|
for (const entry of await readdir(full).catch(() => [])) {
|
|
201
|
-
|
|
202
|
-
|
|
214
|
+
const entryPath = join(full, entry);
|
|
215
|
+
const isDir = await pathIsDirectory(entryPath);
|
|
216
|
+
if (isDir ? !isCanonicalSpecDir(entry) : !isCanonicalSpecFile(entry)) {
|
|
217
|
+
offenders.push(relative(projectPath, entryPath));
|
|
203
218
|
}
|
|
204
219
|
}
|
|
205
220
|
}
|
package/dist/tools/validate.js
CHANGED
|
@@ -73,9 +73,11 @@ export async function handleValidate(args, server) {
|
|
|
73
73
|
const projectPath = knowledge.projectPath;
|
|
74
74
|
// SPEC-1017: fail closed when planu/ contains non-canonical artifacts.
|
|
75
75
|
try {
|
|
76
|
-
const {
|
|
77
|
-
await
|
|
78
|
-
|
|
76
|
+
const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
|
|
77
|
+
const layout = await validateStrictPlanuLayout(projectPath, {
|
|
78
|
+
specPath: spec.specPath,
|
|
79
|
+
includeRoot: false,
|
|
80
|
+
});
|
|
79
81
|
if (!layout.ok) {
|
|
80
82
|
return {
|
|
81
83
|
content: [
|
|
@@ -54,6 +54,7 @@ export interface PlanuCanonicalPathPolicy {
|
|
|
54
54
|
readonly canonicalRootFiles: readonly string[];
|
|
55
55
|
readonly canonicalRootDirs: readonly string[];
|
|
56
56
|
readonly canonicalSpecFiles: readonly string[];
|
|
57
|
+
readonly canonicalSpecDirs: readonly string[];
|
|
57
58
|
readonly forbiddenHostAssetRootDirs: readonly string[];
|
|
58
59
|
readonly generatedRuntimePatterns: readonly string[];
|
|
59
60
|
readonly legacyMergeBeforeDeleteFiles: readonly string[];
|
|
@@ -68,6 +69,10 @@ export interface StrictPlanuValidationResult {
|
|
|
68
69
|
offenders: string[];
|
|
69
70
|
contract: string;
|
|
70
71
|
}
|
|
72
|
+
export interface StrictPlanuValidationOptions {
|
|
73
|
+
specPath?: string;
|
|
74
|
+
includeRoot?: boolean;
|
|
75
|
+
}
|
|
71
76
|
export interface UpdateStatusBatchInput {
|
|
72
77
|
specIds: string[];
|
|
73
78
|
status: SpecStatus;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.4.
|
|
3
|
+
"version": "4.4.1",
|
|
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.4.
|
|
38
|
-
"@planu/core-darwin-x64": "4.4.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.4.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.4.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.4.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.4.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.4.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.4.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.4.1",
|
|
38
|
+
"@planu/core-darwin-x64": "4.4.1",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.4.1",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.4.1",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.4.1",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.4.1",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.4.1",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.4.1"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -185,12 +185,12 @@
|
|
|
185
185
|
"@types/node": "^25.9.1",
|
|
186
186
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
187
187
|
"@vitest/coverage-v8": "^4.1.8",
|
|
188
|
-
"@vue/test-utils": "^2.4.
|
|
188
|
+
"@vue/test-utils": "^2.4.11",
|
|
189
189
|
"eslint": "^10.4.1",
|
|
190
190
|
"eslint-config-prettier": "^10.1.8",
|
|
191
191
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
192
192
|
"eslint-plugin-import": "^2.32.0",
|
|
193
|
-
"happy-dom": "^20.
|
|
193
|
+
"happy-dom": "^20.10.1",
|
|
194
194
|
"husky": "^9.1.7",
|
|
195
195
|
"javascript-obfuscator": "^5.4.3",
|
|
196
196
|
"knip": "^6.15.0",
|
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.4.
|
|
5
|
+
"version": "4.4.1",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|