@planu/cli 4.10.2 → 4.10.4
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-spec-generator.js +0 -5
- package/dist/engine/validator/analyzer.d.ts +2 -1
- package/dist/engine/validator/analyzer.js +15 -11
- package/dist/engine/validator.js +24 -4
- package/dist/storage/base-store.d.ts +16 -0
- package/dist/storage/base-store.js +38 -0
- package/dist/storage/knowledge-store/knowledge.js +20 -5
- package/dist/storage/spec-store.js +9 -3
- package/dist/tools/design-schema-sql/migrations.js +2 -1
- package/dist/tools/design-schema-sql/tables.js +16 -7
- package/dist/tools/init-project/handler.js +4 -1
- package/dist/tools/init-project/migration-runner.d.ts +8 -0
- package/dist/tools/init-project/migration-runner.js +88 -24
- package/dist/tools/init-project/result-builder.js +20 -0
- package/dist/tools/testimonial-handler.js +13 -12
- package/dist/tools/update-status/dod-gates.d.ts +1 -1
- package/dist/tools/update-status/dod-gates.js +4 -3
- package/dist/tools/update-status/index.js +18 -1
- package/dist/tools/validate-helpers.d.ts +1 -0
- package/dist/tools/validate-helpers.js +19 -1
- package/dist/tools/validate.js +87 -46
- package/dist/types/analysis.d.ts +6 -0
- package/dist/types/project/planu-config.d.ts +11 -0
- package/dist/types/schema.d.ts +1 -0
- package/package.json +11 -43
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
## [4.10.4] - 2026-07-06
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
- fix: resolve Supabase testimonials and Planu bug backlog
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
- chore(deps): update tsc-alias
|
|
8
|
+
- chore(planu): clear released pending specs
|
|
9
|
+
- chore(format): apply prettier baseline
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
## [4.10.3] - 2026-07-02
|
|
13
|
+
|
|
14
|
+
### Bug Fixes
|
|
15
|
+
- fix(SPEC-1106): reduce validate response duplication
|
|
16
|
+
- fix(SPEC-1106): harden critical fallback paths
|
|
17
|
+
|
|
18
|
+
|
|
1
19
|
## [4.10.2] - 2026-07-02
|
|
2
20
|
|
|
3
21
|
### Bug Fixes
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
// Output: ~30-50 lines. No generic sections, no OWASP, no STRIDE, no resilience criteria.
|
|
3
3
|
import { parseBddScenarios, renderBddScenariosYaml, convertCheckboxToBdd } from './bdd-parser.js';
|
|
4
4
|
import { deriveModelBudget } from './model-budget-deriver.js';
|
|
5
|
-
import { renderHistoryYaml } from '../spec-versioning/render-history.js';
|
|
6
5
|
import { renderGroundingFrontmatter, renderTechnicalReferenceGroundingFrontmatter, } from '../spec-grounding/contract.js';
|
|
7
6
|
/** Strip redundant "SPEC-XXX — " or "SPEC-XXX: " prefix from spec titles.
|
|
8
7
|
* Titles must not repeat the ID since it is already stored in the `id` field. */
|
|
@@ -60,15 +59,11 @@ export function generateLeanSpecContent(input) {
|
|
|
60
59
|
else {
|
|
61
60
|
acLines = buildCheckboxLines(description, extraCriteria, input.criteriaOverride);
|
|
62
61
|
}
|
|
63
|
-
// SPEC-717: render empty version history for new specs
|
|
64
|
-
const historyYaml = renderHistoryYaml([]);
|
|
65
62
|
const lines = [
|
|
66
63
|
...frontmatterBase,
|
|
67
64
|
...acLines,
|
|
68
65
|
'history:',
|
|
69
66
|
...history.map((h) => ` - date: ${h.date}\n event: ${h.event}`),
|
|
70
|
-
// SPEC-717: version history (empty for new specs)
|
|
71
|
-
historyYaml,
|
|
72
67
|
'---',
|
|
73
68
|
'',
|
|
74
69
|
description.trim(),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Spec, AuditFinding, CodeState, VerifyBlock } from '../../types/index.js';
|
|
1
|
+
import type { Spec, AuditFinding, CodeState, VerifyBlock, CriterionEvidenceResult } from '../../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Scan the project directory to build a CodeState for the given spec.
|
|
4
4
|
*/
|
|
@@ -12,6 +12,7 @@ export declare function scanCodeForSpec(spec: Spec, projectPath: string): Promis
|
|
|
12
12
|
* buildFileContentsCache from criterion-matcher) to avoid a redundant
|
|
13
13
|
* glob+readFile pass when checking multiple criteria in the same call.
|
|
14
14
|
*/
|
|
15
|
+
export declare function checkCriterionEvidence(criterion: string, projectPath: string, codeState: CodeState, verifyBlock?: VerifyBlock, sharedFileContents?: Map<string, string>): Promise<CriterionEvidenceResult>;
|
|
15
16
|
export declare function checkCriterion(criterion: string, projectPath: string, codeState: CodeState, verifyBlock?: VerifyBlock, sharedFileContents?: Map<string, string>): Promise<boolean>;
|
|
16
17
|
/**
|
|
17
18
|
* Classify the severity of a missing criterion based on its content.
|
|
@@ -164,16 +164,16 @@ export async function scanCodeForSpec(spec, projectPath) {
|
|
|
164
164
|
* buildFileContentsCache from criterion-matcher) to avoid a redundant
|
|
165
165
|
* glob+readFile pass when checking multiple criteria in the same call.
|
|
166
166
|
*/
|
|
167
|
-
export async function
|
|
167
|
+
export async function checkCriterionEvidence(criterion, projectPath, codeState, verifyBlock, sharedFileContents) {
|
|
168
168
|
// Check for file existence criteria
|
|
169
169
|
const fileMatch = /(?:create|add|implement)\s+(?:file\s+)?["']?([^\s"']+\.\w+)["']?/i.exec(criterion);
|
|
170
170
|
if (fileMatch?.[1]) {
|
|
171
171
|
try {
|
|
172
172
|
await stat(join(projectPath, fileMatch[1]));
|
|
173
|
-
return
|
|
173
|
+
return { status: 'proven' };
|
|
174
174
|
}
|
|
175
175
|
catch {
|
|
176
|
-
return
|
|
176
|
+
return { status: 'missing' };
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
179
|
// Check for component/function existence in code
|
|
@@ -183,7 +183,7 @@ export async function checkCriterion(criterion, projectPath, codeState, verifyBl
|
|
|
183
183
|
// Search in already-loaded codeState contents first (zero extra I/O)
|
|
184
184
|
for (const [, content] of codeState.fileContents) {
|
|
185
185
|
if (content.includes(name)) {
|
|
186
|
-
return
|
|
186
|
+
return { status: 'proven' };
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
// Broader search: reuse sharedFileContents when provided to avoid a
|
|
@@ -192,17 +192,17 @@ export async function checkCriterion(criterion, projectPath, codeState, verifyBl
|
|
|
192
192
|
const broadContents = sharedFileContents ?? (await buildBroadCache(projectPath));
|
|
193
193
|
for (const [, content] of broadContents) {
|
|
194
194
|
if (content.includes(name)) {
|
|
195
|
-
return
|
|
195
|
+
return { status: 'proven' };
|
|
196
196
|
}
|
|
197
197
|
}
|
|
198
|
-
return
|
|
198
|
+
return { status: 'missing' };
|
|
199
199
|
}
|
|
200
200
|
// Check for API endpoint criteria
|
|
201
201
|
const endpointMatch = /(?:endpoint|route|api)\s+(?:for\s+)?["']?(\w+)["']?/i.exec(criterion);
|
|
202
202
|
if (endpointMatch?.[1]) {
|
|
203
203
|
for (const [, content] of codeState.fileContents) {
|
|
204
204
|
if (content.toLowerCase().includes(endpointMatch[1].toLowerCase())) {
|
|
205
|
-
return
|
|
205
|
+
return { status: 'proven' };
|
|
206
206
|
}
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -212,13 +212,17 @@ export async function checkCriterion(criterion, projectPath, codeState, verifyBl
|
|
|
212
212
|
// For manual checks (type === 'manual'), we fall back to the optimistic heuristic.
|
|
213
213
|
const deepResult = await deepCheckCriterion(criterion, projectPath, verifyBlock);
|
|
214
214
|
if (deepResult.checkType !== 'manual') {
|
|
215
|
-
return deepResult.passed;
|
|
215
|
+
return { status: deepResult.passed ? 'proven' : 'missing' };
|
|
216
216
|
}
|
|
217
|
-
//
|
|
217
|
+
// Affected files alone show scope, not proof. Keep this visible so validate
|
|
218
|
+
// cannot report score 100 for manual/unclassified criteria without evidence.
|
|
218
219
|
if (codeState.affectedFiles.length > 0) {
|
|
219
|
-
return
|
|
220
|
+
return { status: 'indeterminate' };
|
|
220
221
|
}
|
|
221
|
-
return
|
|
222
|
+
return { status: 'missing' };
|
|
223
|
+
}
|
|
224
|
+
export async function checkCriterion(criterion, projectPath, codeState, verifyBlock, sharedFileContents) {
|
|
225
|
+
return ((await checkCriterionEvidence(criterion, projectPath, codeState, verifyBlock, sharedFileContents)).status === 'proven');
|
|
222
226
|
}
|
|
223
227
|
/**
|
|
224
228
|
* Classify the severity of a missing criterion based on its content.
|
package/dist/engine/validator.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// All imports from '../engine/validator.js' continue to work unchanged.
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
5
|
import { extractCriteria, extractVerifyBlocks } from './validator/extractors.js';
|
|
6
|
-
import { scanCodeForSpec,
|
|
6
|
+
import { scanCodeForSpec, checkCriterionEvidence, classifyDriftSeverity, quickQualityCheck, } from './validator/analyzer.js';
|
|
7
7
|
import { resolveApplicableDimensions } from './validator/scope-resolver.js';
|
|
8
8
|
import { buildHolisticReportFromFlatScore } from './validator/holistic-report.js';
|
|
9
9
|
import { readEvidenceArtifacts } from './evidence-gates/artifact-reader.js';
|
|
@@ -71,6 +71,7 @@ export async function validateSpec(spec, projectPath) {
|
|
|
71
71
|
return {
|
|
72
72
|
matches: [],
|
|
73
73
|
missing: [],
|
|
74
|
+
indeterminate: [],
|
|
74
75
|
extra: [],
|
|
75
76
|
fieldsImplemented: 0,
|
|
76
77
|
fieldsTotal: 0,
|
|
@@ -93,15 +94,22 @@ export async function validateSpec(spec, projectPath) {
|
|
|
93
94
|
}
|
|
94
95
|
const matches = [];
|
|
95
96
|
const missing = [];
|
|
97
|
+
const indeterminate = [];
|
|
96
98
|
const extra = [];
|
|
97
99
|
const qualityIssues = [];
|
|
98
100
|
for (const criterion of criteria) {
|
|
99
101
|
const verifyBlock = verifyBlockMap.get(criterion);
|
|
100
102
|
const evidenceRow = traceabilityEvidence.get(normalizeCriterionForEvidence(criterion));
|
|
101
|
-
|
|
102
|
-
(await checkCriterion(criterion, projectPath, codeState, verifyBlock));
|
|
103
|
-
if (found) {
|
|
103
|
+
if (evidenceRow !== undefined) {
|
|
104
104
|
matches.push(criterion);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const evidence = await checkCriterionEvidence(criterion, projectPath, codeState, verifyBlock);
|
|
108
|
+
if (evidence.status === 'proven') {
|
|
109
|
+
matches.push(criterion);
|
|
110
|
+
}
|
|
111
|
+
else if (evidence.status === 'indeterminate') {
|
|
112
|
+
indeterminate.push(criterion);
|
|
105
113
|
}
|
|
106
114
|
else {
|
|
107
115
|
missing.push(criterion);
|
|
@@ -131,6 +139,7 @@ export async function validateSpec(spec, projectPath) {
|
|
|
131
139
|
return {
|
|
132
140
|
matches,
|
|
133
141
|
missing,
|
|
142
|
+
indeterminate,
|
|
134
143
|
extra,
|
|
135
144
|
fieldsImplemented,
|
|
136
145
|
fieldsTotal,
|
|
@@ -157,6 +166,17 @@ export async function detectDrift(spec, projectPath, _mode = 'full', threshold =
|
|
|
157
166
|
autoFixable: false,
|
|
158
167
|
});
|
|
159
168
|
}
|
|
169
|
+
for (const criterion of validation.indeterminate ?? []) {
|
|
170
|
+
drifts.push({
|
|
171
|
+
file: '',
|
|
172
|
+
specCriterion: criterion,
|
|
173
|
+
expected: 'Proven implementation evidence',
|
|
174
|
+
actual: 'Indeterminate',
|
|
175
|
+
severity: classifyDriftSeverity(criterion),
|
|
176
|
+
autoFixable: false,
|
|
177
|
+
suggestedFix: 'Add explicit validation evidence or implement a recognizable check.',
|
|
178
|
+
});
|
|
179
|
+
}
|
|
160
180
|
// Quality issues as drift items
|
|
161
181
|
for (const issue of validation.qualityIssues) {
|
|
162
182
|
drifts.push({
|
|
@@ -14,6 +14,13 @@
|
|
|
14
14
|
* JSON_PARSE_UNSAFE_CAST.
|
|
15
15
|
*/
|
|
16
16
|
export declare function safeJsonParse<T>(raw: string, fallback: T): T;
|
|
17
|
+
export declare class CriticalJsonReadError extends Error {
|
|
18
|
+
readonly filePath: string;
|
|
19
|
+
readonly reason: string;
|
|
20
|
+
constructor(filePath: string, reason: string, options?: {
|
|
21
|
+
cause?: unknown;
|
|
22
|
+
});
|
|
23
|
+
}
|
|
17
24
|
/**
|
|
18
25
|
* Hash a project path into a deterministic projectId using SHA-256.
|
|
19
26
|
*/
|
|
@@ -23,6 +30,15 @@ export declare function hashProjectPath(projectPath: string): string;
|
|
|
23
30
|
* default value when the file does not exist.
|
|
24
31
|
*/
|
|
25
32
|
export declare function readJson<T>(filePath: string, defaultValue: T): Promise<T>;
|
|
33
|
+
/**
|
|
34
|
+
* Read a critical JSON store. Missing files still return the initialization
|
|
35
|
+
* default, but malformed JSON or wrong top-level shape throw an observable
|
|
36
|
+
* recoverable error instead of silently replacing state with an empty value.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readCriticalJson<T>(filePath: string, defaultValue: T, options?: {
|
|
39
|
+
validate?: (value: unknown) => value is T;
|
|
40
|
+
expectedDescription?: string;
|
|
41
|
+
}): Promise<T>;
|
|
26
42
|
/**
|
|
27
43
|
* Write a value to a JSON file, creating parent directories as needed.
|
|
28
44
|
*/
|
|
@@ -32,6 +32,16 @@ export function safeJsonParse(raw, fallback) {
|
|
|
32
32
|
return fallback;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
export class CriticalJsonReadError extends Error {
|
|
36
|
+
filePath;
|
|
37
|
+
reason;
|
|
38
|
+
constructor(filePath, reason, options) {
|
|
39
|
+
super(`[Planu] Critical JSON store is unreadable at ${filePath}: ${reason}`, options);
|
|
40
|
+
this.filePath = filePath;
|
|
41
|
+
this.reason = reason;
|
|
42
|
+
this.name = 'CriticalJsonReadError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Hash a project path into a deterministic projectId using SHA-256.
|
|
37
47
|
*/
|
|
@@ -60,6 +70,34 @@ export async function readJson(filePath, defaultValue) {
|
|
|
60
70
|
throw err;
|
|
61
71
|
}
|
|
62
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Read a critical JSON store. Missing files still return the initialization
|
|
75
|
+
* default, but malformed JSON or wrong top-level shape throw an observable
|
|
76
|
+
* recoverable error instead of silently replacing state with an empty value.
|
|
77
|
+
*/
|
|
78
|
+
export async function readCriticalJson(filePath, defaultValue, options = {}) {
|
|
79
|
+
let raw;
|
|
80
|
+
try {
|
|
81
|
+
raw = await readFile(filePath, 'utf-8');
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
if (isNodeError(err) && err.code === 'ENOENT') {
|
|
85
|
+
return defaultValue;
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
let parsed;
|
|
90
|
+
try {
|
|
91
|
+
parsed = JSON.parse(raw);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
throw new CriticalJsonReadError(filePath, 'malformed JSON', { cause: err });
|
|
95
|
+
}
|
|
96
|
+
if (options.validate && !options.validate(parsed)) {
|
|
97
|
+
throw new CriticalJsonReadError(filePath, `expected ${options.expectedDescription ?? 'valid critical JSON shape'}`);
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
63
101
|
/**
|
|
64
102
|
* Write a value to a JSON file, creating parent directories as needed.
|
|
65
103
|
*/
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readCriticalJson, writeJson, projectDataDir } from '../base-store.js';
|
|
2
2
|
import { withFileLock } from '../file-mutex.js';
|
|
3
3
|
import { discoverProjectDataFile } from '../data-dir-discovery.js';
|
|
4
4
|
import { migrateLegacyDataDir } from '../migrations/cwd-to-absolute.js';
|
|
5
5
|
function knowledgeFile(projectId) {
|
|
6
6
|
return `${projectDataDir(projectId)}/knowledge.json`;
|
|
7
7
|
}
|
|
8
|
+
function isKnowledgeOrNull(value) {
|
|
9
|
+
return value === null || (typeof value === 'object' && !Array.isArray(value));
|
|
10
|
+
}
|
|
8
11
|
/**
|
|
9
12
|
* Get project knowledge. Returns `null` when no knowledge has been stored yet.
|
|
10
13
|
*
|
|
@@ -16,7 +19,10 @@ function knowledgeFile(projectId) {
|
|
|
16
19
|
export async function getKnowledge(projectId) {
|
|
17
20
|
// SPEC-1007: migrate legacy <cwd>/data/projects/{id} to absolute home once.
|
|
18
21
|
await migrateLegacyDataDir(projectId);
|
|
19
|
-
const direct = await
|
|
22
|
+
const direct = await readCriticalJson(knowledgeFile(projectId), null, {
|
|
23
|
+
validate: isKnowledgeOrNull,
|
|
24
|
+
expectedDescription: 'a project knowledge object or null',
|
|
25
|
+
});
|
|
20
26
|
if (direct !== null) {
|
|
21
27
|
return direct;
|
|
22
28
|
}
|
|
@@ -24,7 +30,10 @@ export async function getKnowledge(projectId) {
|
|
|
24
30
|
if (discovered === null) {
|
|
25
31
|
return null;
|
|
26
32
|
}
|
|
27
|
-
return
|
|
33
|
+
return readCriticalJson(discovered, null, {
|
|
34
|
+
validate: isKnowledgeOrNull,
|
|
35
|
+
expectedDescription: 'a project knowledge object or null',
|
|
36
|
+
});
|
|
28
37
|
}
|
|
29
38
|
/**
|
|
30
39
|
* Save or overwrite the full project knowledge object.
|
|
@@ -41,7 +50,10 @@ export async function saveKnowledge(projectId, knowledge) {
|
|
|
41
50
|
*/
|
|
42
51
|
export async function updateKnowledge(projectId, updates) {
|
|
43
52
|
return withFileLock(knowledgeFile(projectId), async () => {
|
|
44
|
-
const existing = await
|
|
53
|
+
const existing = await readCriticalJson(knowledgeFile(projectId), null, {
|
|
54
|
+
validate: isKnowledgeOrNull,
|
|
55
|
+
expectedDescription: 'a project knowledge object or null',
|
|
56
|
+
});
|
|
45
57
|
if (!existing) {
|
|
46
58
|
throw new Error(`No knowledge found for project "${projectId}". Use saveKnowledge first.`);
|
|
47
59
|
}
|
|
@@ -60,7 +72,10 @@ export async function updateKnowledge(projectId, updates) {
|
|
|
60
72
|
*/
|
|
61
73
|
export async function deleteKnowledge(projectId) {
|
|
62
74
|
return withFileLock(knowledgeFile(projectId), async () => {
|
|
63
|
-
const existing = await
|
|
75
|
+
const existing = await readCriticalJson(knowledgeFile(projectId), null, {
|
|
76
|
+
validate: isKnowledgeOrNull,
|
|
77
|
+
expectedDescription: 'a project knowledge object or null',
|
|
78
|
+
});
|
|
64
79
|
if (!existing) {
|
|
65
80
|
return false;
|
|
66
81
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readJson, writeJson, projectDataDir } from './base-store.js';
|
|
1
|
+
import { readCriticalJson, readJson, writeJson, projectDataDir } from './base-store.js';
|
|
2
2
|
import { migrateLegacyDataDir } from './migrations/cwd-to-absolute.js';
|
|
3
3
|
import { withFileLock } from './file-mutex.js';
|
|
4
4
|
import { discoverProjectDataFile } from './data-dir-discovery.js';
|
|
@@ -60,11 +60,17 @@ async function loadAll(projectId) {
|
|
|
60
60
|
// SPEC-1007: migrate legacy <cwd>/data/projects/{id} to absolute home once.
|
|
61
61
|
await migrateLegacyDataDir(projectId);
|
|
62
62
|
const directFile = specsFile(projectId);
|
|
63
|
-
let raw = await
|
|
63
|
+
let raw = await readCriticalJson(directFile, [], {
|
|
64
|
+
validate: (value) => Array.isArray(value),
|
|
65
|
+
expectedDescription: 'an array of specs',
|
|
66
|
+
});
|
|
64
67
|
if (raw.length === 0) {
|
|
65
68
|
const discovered = await discoverProjectDataFile(projectId, 'specs.json');
|
|
66
69
|
if (discovered !== null && discovered !== directFile) {
|
|
67
|
-
raw = await
|
|
70
|
+
raw = await readCriticalJson(discovered, [], {
|
|
71
|
+
validate: (value) => Array.isArray(value),
|
|
72
|
+
expectedDescription: 'an array of specs',
|
|
73
|
+
});
|
|
68
74
|
}
|
|
69
75
|
}
|
|
70
76
|
const specs = raw.map(normalizeSpec);
|
|
@@ -62,11 +62,12 @@ export function generateMigrations(tables, indexes, rlsPolicies, engine) {
|
|
|
62
62
|
});
|
|
63
63
|
}
|
|
64
64
|
for (const policy of rlsPolicies) {
|
|
65
|
+
const rolesClause = policy.roles?.length ? ` TO ${policy.roles.join(', ')}` : '';
|
|
65
66
|
const usingClause = `USING (${policy.using})`;
|
|
66
67
|
const withCheckClause = policy.withCheck ? ` WITH CHECK (${policy.withCheck})` : '';
|
|
67
68
|
migrations.push({
|
|
68
69
|
order: order++,
|
|
69
|
-
sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation} ${usingClause}${withCheckClause};`,
|
|
70
|
+
sql: `CREATE POLICY ${policy.name} ON ${policy.table} FOR ${policy.operation}${rolesClause} ${usingClause}${withCheckClause};`,
|
|
70
71
|
reversible: true,
|
|
71
72
|
reverseSql: `DROP POLICY IF EXISTS ${policy.name} ON ${policy.table};`,
|
|
72
73
|
dataLoss: false,
|
|
@@ -170,33 +170,41 @@ export function generateIndexes(tables, _engine) {
|
|
|
170
170
|
export function generateRLSPolicies(tables) {
|
|
171
171
|
const policies = [];
|
|
172
172
|
for (const table of tables) {
|
|
173
|
+
const ownerCol = table.columns.find((c) => c.name === 'user_id' || c.name === 'owner_id' || c.name === 'created_by');
|
|
174
|
+
const ownerPredicate = ownerCol ? `(select auth.uid()) = ${ownerCol.name}` : 'true';
|
|
173
175
|
policies.push({
|
|
174
176
|
name: `${table.name}_select_authenticated`,
|
|
175
177
|
table: table.name,
|
|
176
178
|
operation: 'SELECT',
|
|
177
|
-
|
|
178
|
-
|
|
179
|
+
roles: ['authenticated'],
|
|
180
|
+
using: ownerPredicate,
|
|
181
|
+
justification: ownerCol
|
|
182
|
+
? 'Authenticated users can only read records they own'
|
|
183
|
+
: 'Only authenticated users can read records',
|
|
179
184
|
});
|
|
180
|
-
const ownerCol = table.columns.find((c) => c.name === 'user_id' || c.name === 'owner_id' || c.name === 'created_by');
|
|
181
185
|
if (ownerCol) {
|
|
182
186
|
policies.push({
|
|
183
187
|
name: `${table.name}_insert_owner`,
|
|
184
188
|
table: table.name,
|
|
185
189
|
operation: 'INSERT',
|
|
190
|
+
roles: ['authenticated'],
|
|
186
191
|
using: 'true',
|
|
187
|
-
withCheck:
|
|
192
|
+
withCheck: ownerPredicate,
|
|
188
193
|
justification: 'Users can only insert records they own',
|
|
189
194
|
}, {
|
|
190
195
|
name: `${table.name}_update_owner`,
|
|
191
196
|
table: table.name,
|
|
192
197
|
operation: 'UPDATE',
|
|
193
|
-
|
|
198
|
+
roles: ['authenticated'],
|
|
199
|
+
using: ownerPredicate,
|
|
200
|
+
withCheck: ownerPredicate,
|
|
194
201
|
justification: 'Users can only update their own records',
|
|
195
202
|
}, {
|
|
196
203
|
name: `${table.name}_delete_owner`,
|
|
197
204
|
table: table.name,
|
|
198
205
|
operation: 'DELETE',
|
|
199
|
-
|
|
206
|
+
roles: ['authenticated'],
|
|
207
|
+
using: ownerPredicate,
|
|
200
208
|
justification: 'Users can only delete their own records',
|
|
201
209
|
});
|
|
202
210
|
}
|
|
@@ -205,7 +213,8 @@ export function generateRLSPolicies(tables) {
|
|
|
205
213
|
name: `${table.name}_all_authenticated`,
|
|
206
214
|
table: table.name,
|
|
207
215
|
operation: 'ALL',
|
|
208
|
-
|
|
216
|
+
roles: ['authenticated'],
|
|
217
|
+
using: 'true',
|
|
209
218
|
justification: 'All CRUD operations require authentication',
|
|
210
219
|
});
|
|
211
220
|
}
|
|
@@ -271,7 +271,7 @@ export async function handleInitProject(params, server) {
|
|
|
271
271
|
const configResult = await buildProjectConfig(projectPath, projectId, knowledge, workMode);
|
|
272
272
|
const { planuConfigPath, planuConfigGenerated, whatsNew } = configResult;
|
|
273
273
|
// Run spec migrations (v0.30 -> v0.40, legacy paths, prefix normalization)
|
|
274
|
-
const { discoveryResult, migrationResult, folderMigrationResult } = await runSpecMigrations(projectPath, projectId, knowledge);
|
|
274
|
+
const { discoveryResult, migrationResult, folderMigrationResult, criticalMigrationFailures, nonCriticalWarnings: nonCriticalMigrationWarnings, migrationReportPath, } = await runSpecMigrations(projectPath, projectId, knowledge);
|
|
275
275
|
// Auto-fetch recommended skills based on detected stack
|
|
276
276
|
const recommendedSkills = await fetchRecommendedSkills(knowledge);
|
|
277
277
|
// Auto-install or queue skills (opt-out via planu.json or input param, SPEC-185)
|
|
@@ -434,6 +434,9 @@ export async function handleInitProject(params, server) {
|
|
|
434
434
|
migrationResult,
|
|
435
435
|
discoveryResult,
|
|
436
436
|
folderMigrationResult,
|
|
437
|
+
criticalMigrationFailures,
|
|
438
|
+
nonCriticalMigrationWarnings,
|
|
439
|
+
migrationReportPath,
|
|
437
440
|
hooksInstalled,
|
|
438
441
|
gitFlowType,
|
|
439
442
|
gitignoreUpdated,
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { ProjectKnowledge, Spec } from '../../types/index.js';
|
|
2
|
+
export interface MigrationRunIssue {
|
|
3
|
+
phase: string;
|
|
4
|
+
severity: 'critical' | 'nonCritical';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
2
7
|
export interface MigrationRunResult {
|
|
3
8
|
discoveryResult: {
|
|
4
9
|
flattenedCount: number;
|
|
@@ -14,6 +19,9 @@ export interface MigrationRunResult {
|
|
|
14
19
|
migratedCount: number;
|
|
15
20
|
errors: string[];
|
|
16
21
|
} | null;
|
|
22
|
+
criticalMigrationFailures: MigrationRunIssue[];
|
|
23
|
+
nonCriticalWarnings: MigrationRunIssue[];
|
|
24
|
+
migrationReportPath: string | null;
|
|
17
25
|
}
|
|
18
26
|
/**
|
|
19
27
|
* Run all spec migration passes during init_project:
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
// tools/init-project/migration-runner.ts — Spec migration orchestration during init_project
|
|
2
2
|
import { specStore, knowledgeStore } from '../../storage/index.js';
|
|
3
|
+
import { projectDataDir, writeJson } from '../../storage/base-store.js';
|
|
3
4
|
import { filterLegacySpecs, migrateSpecs, filterUnprefixedSpecs, migrateSpecFolderNames, discoverAndFlattenSpecs, reconcileSpecPaths, migrateAllSpecsToLean, } from '../../engine/spec-migrator.js';
|
|
5
|
+
function issueFromError(phase, severity, err) {
|
|
6
|
+
return {
|
|
7
|
+
phase,
|
|
8
|
+
severity,
|
|
9
|
+
message: err instanceof Error ? err.message : String(err),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function issuesFromMessages(phase, severity, messages) {
|
|
13
|
+
return (messages ?? []).map((message) => ({ phase, severity, message }));
|
|
14
|
+
}
|
|
4
15
|
/**
|
|
5
16
|
* Run all spec migration passes during init_project:
|
|
6
17
|
* 1. Discover and flatten specs from subcategory folders (v0.30 → v0.40)
|
|
@@ -11,23 +22,38 @@ import { filterLegacySpecs, migrateSpecs, filterUnprefixedSpecs, migrateSpecFold
|
|
|
11
22
|
export async function runSpecMigrations(projectPath, projectId, knowledge) {
|
|
12
23
|
// Auto-discover and flatten specs in subcategory folders (v0.30 → v0.40 migration)
|
|
13
24
|
let discoveryResult = null;
|
|
25
|
+
const criticalMigrationFailures = [];
|
|
26
|
+
const nonCriticalWarnings = [];
|
|
14
27
|
try {
|
|
15
28
|
discoveryResult = await discoverAndFlattenSpecs(projectPath);
|
|
29
|
+
criticalMigrationFailures.push(...issuesFromMessages('discover-and-flatten-specs', 'critical', discoveryResult.errors));
|
|
16
30
|
}
|
|
17
|
-
catch {
|
|
18
|
-
|
|
31
|
+
catch (err) {
|
|
32
|
+
criticalMigrationFailures.push(issueFromError('discover-and-flatten-specs', 'critical', err));
|
|
19
33
|
}
|
|
20
34
|
// Auto-migrate legacy specs if detected (docs/sdd/specs/ → planu/specs/)
|
|
21
|
-
|
|
35
|
+
let allSpecs = [];
|
|
36
|
+
try {
|
|
37
|
+
allSpecs = await specStore.listSpecs(projectId);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
criticalMigrationFailures.push(issueFromError('list-specs-before-migration', 'critical', err));
|
|
41
|
+
}
|
|
22
42
|
const legacySpecs = filterLegacySpecs(allSpecs);
|
|
23
43
|
let migrationResult = null;
|
|
24
44
|
if (legacySpecs.length > 0) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
try {
|
|
46
|
+
migrationResult = await migrateSpecs(projectPath, legacySpecs, {
|
|
47
|
+
listSpecs: specStore.listSpecs,
|
|
48
|
+
updateSpec: specStore.updateSpec,
|
|
49
|
+
});
|
|
50
|
+
criticalMigrationFailures.push(...issuesFromMessages('legacy-spec-migration', 'critical', migrationResult.errors));
|
|
51
|
+
// Update knowledge specLocation after migration (create new object to avoid param mutation)
|
|
52
|
+
await knowledgeStore.saveKnowledge(projectId, { ...knowledge, specLocation: 'planu/specs' });
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
criticalMigrationFailures.push(issueFromError('legacy-spec-migration', 'critical', err));
|
|
56
|
+
}
|
|
31
57
|
}
|
|
32
58
|
// Reconcile specs.json paths with actual filesystem (fixes stale paths after rename/flatten)
|
|
33
59
|
try {
|
|
@@ -36,42 +62,80 @@ export async function runSpecMigrations(projectPath, projectId, knowledge) {
|
|
|
36
62
|
updateSpec: specStore.updateSpec,
|
|
37
63
|
}, projectId);
|
|
38
64
|
}
|
|
39
|
-
catch {
|
|
40
|
-
|
|
65
|
+
catch (err) {
|
|
66
|
+
criticalMigrationFailures.push(issueFromError('reconcile-spec-paths', 'critical', err));
|
|
41
67
|
}
|
|
42
68
|
// Auto-migrate spec folders without SPEC-XXX- prefix
|
|
43
|
-
|
|
69
|
+
let specsAfterReconcile = [];
|
|
70
|
+
try {
|
|
71
|
+
specsAfterReconcile = await specStore.listSpecs(projectId);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
criticalMigrationFailures.push(issueFromError('list-specs-before-folder-migration', 'critical', err));
|
|
75
|
+
}
|
|
76
|
+
const unprefixedSpecs = filterUnprefixedSpecs(specsAfterReconcile);
|
|
44
77
|
let folderMigrationResult = null;
|
|
45
78
|
if (unprefixedSpecs.length > 0) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
79
|
+
try {
|
|
80
|
+
folderMigrationResult = await migrateSpecFolderNames(projectPath, unprefixedSpecs, {
|
|
81
|
+
listSpecs: specStore.listSpecs,
|
|
82
|
+
updateSpec: specStore.updateSpec,
|
|
83
|
+
});
|
|
84
|
+
criticalMigrationFailures.push(...issuesFromMessages('spec-folder-migration', 'critical', folderMigrationResult.errors));
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
criticalMigrationFailures.push(issueFromError('spec-folder-migration', 'critical', err));
|
|
88
|
+
}
|
|
50
89
|
}
|
|
51
90
|
// SPEC-461 Phase 2: Auto-migrate old verbose specs to lean format
|
|
52
91
|
try {
|
|
53
92
|
await migrateAllSpecsToLean(projectPath);
|
|
54
93
|
}
|
|
55
|
-
catch {
|
|
56
|
-
|
|
94
|
+
catch (err) {
|
|
95
|
+
criticalMigrationFailures.push(issueFromError('lean-spec-migration', 'critical', err));
|
|
57
96
|
}
|
|
58
97
|
// SPEC-630: Merge separate technical.md into unified spec.md
|
|
59
98
|
try {
|
|
60
99
|
const { migrateAllSpecsToUnified } = await import('../../engine/spec-migrator/index.js');
|
|
61
|
-
await migrateAllSpecsToUnified(projectPath)
|
|
100
|
+
await migrateAllSpecsToUnified(projectPath);
|
|
62
101
|
}
|
|
63
|
-
catch {
|
|
64
|
-
|
|
102
|
+
catch (err) {
|
|
103
|
+
criticalMigrationFailures.push(issueFromError('unified-spec-migration', 'critical', err));
|
|
65
104
|
}
|
|
66
105
|
// SPEC-1017: strict managed planu/ cleanup after all legacy migrations.
|
|
67
106
|
try {
|
|
68
107
|
const { runStrictPlanuCleanup } = await import('../../engine/spec-migrator/index.js');
|
|
69
108
|
await runStrictPlanuCleanup(projectPath);
|
|
70
109
|
}
|
|
71
|
-
catch {
|
|
72
|
-
|
|
110
|
+
catch (err) {
|
|
111
|
+
criticalMigrationFailures.push(issueFromError('strict-planu-cleanup', 'critical', err));
|
|
112
|
+
}
|
|
113
|
+
let migrationReportPath = null;
|
|
114
|
+
try {
|
|
115
|
+
const reportPath = `${projectDataDir(projectId)}/reports/init-migrations/latest.json`;
|
|
116
|
+
await writeJson(reportPath, {
|
|
117
|
+
version: 1,
|
|
118
|
+
projectId,
|
|
119
|
+
generatedAt: new Date().toISOString(),
|
|
120
|
+
discoveryResult,
|
|
121
|
+
migrationResult,
|
|
122
|
+
folderMigrationResult,
|
|
123
|
+
criticalMigrationFailures,
|
|
124
|
+
nonCriticalWarnings,
|
|
125
|
+
});
|
|
126
|
+
migrationReportPath = reportPath;
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
nonCriticalWarnings.push(issueFromError('write-migration-report', 'nonCritical', err));
|
|
73
130
|
}
|
|
74
|
-
return {
|
|
131
|
+
return {
|
|
132
|
+
discoveryResult,
|
|
133
|
+
migrationResult,
|
|
134
|
+
folderMigrationResult,
|
|
135
|
+
criticalMigrationFailures,
|
|
136
|
+
nonCriticalWarnings,
|
|
137
|
+
migrationReportPath,
|
|
138
|
+
};
|
|
75
139
|
}
|
|
76
140
|
/** Return all specs for the project (needed by caller for health check refs). */
|
|
77
141
|
export async function listProjectSpecs(projectId) {
|
|
@@ -149,6 +149,9 @@ export function buildInitProjectResult(input) {
|
|
|
149
149
|
specMigration: input.migrationResult,
|
|
150
150
|
specDiscovery: input.discoveryResult,
|
|
151
151
|
specFolderMigration: input.folderMigrationResult,
|
|
152
|
+
criticalMigrationFailures: input.criticalMigrationFailures ?? [],
|
|
153
|
+
bestEffortWarnings: input.nonCriticalMigrationWarnings ?? [],
|
|
154
|
+
migrationReportPath: input.migrationReportPath ?? null,
|
|
152
155
|
gitAutomation: {
|
|
153
156
|
hooksInstalled: input.hooksInstalled,
|
|
154
157
|
gitFlowType: input.gitFlowType,
|
|
@@ -178,6 +181,8 @@ export function buildInitProjectResult(input) {
|
|
|
178
181
|
healthScore: input.healthReport.score,
|
|
179
182
|
healthSummary: input.healthReport.summary,
|
|
180
183
|
items: input.healthReport.items,
|
|
184
|
+
conventionScanStatus: input.conventionScanStatus ?? 'skipped',
|
|
185
|
+
conventionWarning: buildConventionHealthWarning(input),
|
|
181
186
|
}
|
|
182
187
|
: null,
|
|
183
188
|
feedbackHint: 'Found a bug or have a suggestion? Just say "I want to report a bug" or "I have a suggestion" and Planu will track it for you.',
|
|
@@ -196,6 +201,10 @@ export function buildInitProjectResult(input) {
|
|
|
196
201
|
const narrative = ti('tools.init_project.success', { projectPath: result.projectPath }) +
|
|
197
202
|
`\n\nDetected: **${result.language}** / ${framework} — project ${action}. ` +
|
|
198
203
|
`Run \`create_spec\` to start planning your first feature.` +
|
|
204
|
+
((input.criticalMigrationFailures?.length ?? 0) > 0
|
|
205
|
+
? `\n\n⚠️ Critical spec migration warnings: ${String(input.criticalMigrationFailures?.length ?? 0)} issue(s). ` +
|
|
206
|
+
`See migrationReportPath in structuredContent${input.migrationReportPath ? ` (${input.migrationReportPath})` : ''}.`
|
|
207
|
+
: '') +
|
|
199
208
|
telemetryNote +
|
|
200
209
|
trialNote +
|
|
201
210
|
tokenTip;
|
|
@@ -249,4 +258,15 @@ export function buildInitProjectResult(input) {
|
|
|
249
258
|
},
|
|
250
259
|
};
|
|
251
260
|
}
|
|
261
|
+
function buildConventionHealthWarning(input) {
|
|
262
|
+
const status = input.conventionScanStatus ?? 'skipped';
|
|
263
|
+
if (status === 'failed') {
|
|
264
|
+
return 'Convention scan failed; project health may not include all convention violations.';
|
|
265
|
+
}
|
|
266
|
+
const conventionItems = input.healthReport?.items.filter((item) => /convention|rule/i.test(item.category) || /convention|rule/i.test(item.message)) ?? [];
|
|
267
|
+
if (conventionItems.length === 0) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return `${String(conventionItems.length)} convention-related health warning(s) detected.`;
|
|
271
|
+
}
|
|
252
272
|
//# sourceMappingURL=result-builder.js.map
|
|
@@ -13,6 +13,9 @@ function formatTestimonial(t) {
|
|
|
13
13
|
const github = t.authorGithub ? ` (@${t.authorGithub})` : '';
|
|
14
14
|
return `- "${t.quote}" — ${t.authorName}${github}, ${t.authorRole}${featured} (${t.rating}/5, ${t.locale})`;
|
|
15
15
|
}
|
|
16
|
+
function nonBlank(value) {
|
|
17
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
18
|
+
}
|
|
16
19
|
export async function handleApproveTestimonial(input) {
|
|
17
20
|
const config = await getSupabaseConfig(input.projectPath);
|
|
18
21
|
if (!config) {
|
|
@@ -26,23 +29,21 @@ export async function handleApproveTestimonial(input) {
|
|
|
26
29
|
isError: true,
|
|
27
30
|
};
|
|
28
31
|
}
|
|
29
|
-
|
|
30
|
-
const hasManualData = Boolean(input.authorName) && Boolean(input.quote);
|
|
31
|
-
if (!hasFeedbackId && !hasManualData) {
|
|
32
|
+
if (!nonBlank(input.authorName) || !nonBlank(input.quote)) {
|
|
32
33
|
return {
|
|
33
34
|
content: [
|
|
34
35
|
{
|
|
35
36
|
type: 'text',
|
|
36
|
-
text: 'Validation failed:
|
|
37
|
+
text: 'Validation failed: authorName and quote are required before approving a testimonial. feedbackId-only approval is not supported yet.',
|
|
37
38
|
},
|
|
38
39
|
],
|
|
39
40
|
isError: true,
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
43
|
const payload = {
|
|
43
|
-
author_name: input.authorName
|
|
44
|
-
author_role: input.authorRole ?? '',
|
|
45
|
-
quote: input.quote
|
|
44
|
+
author_name: input.authorName.trim(),
|
|
45
|
+
author_role: input.authorRole?.trim() ?? '',
|
|
46
|
+
quote: input.quote.trim(),
|
|
46
47
|
rating: input.rating ?? 5,
|
|
47
48
|
source: input.source ?? 'manual',
|
|
48
49
|
approved: true,
|
|
@@ -50,19 +51,19 @@ export async function handleApproveTestimonial(input) {
|
|
|
50
51
|
locale: input.locale ?? 'en',
|
|
51
52
|
};
|
|
52
53
|
if (input.authorAvatarUrl !== undefined) {
|
|
53
|
-
payload.author_avatar_url = input.authorAvatarUrl;
|
|
54
|
+
payload.author_avatar_url = input.authorAvatarUrl.trim();
|
|
54
55
|
}
|
|
55
56
|
if (input.authorGithub !== undefined) {
|
|
56
|
-
payload.author_github = input.authorGithub;
|
|
57
|
+
payload.author_github = input.authorGithub.trim();
|
|
57
58
|
}
|
|
58
59
|
if (input.feedbackId !== undefined) {
|
|
59
|
-
payload.feedback_id = input.feedbackId;
|
|
60
|
+
payload.feedback_id = input.feedbackId.trim();
|
|
60
61
|
}
|
|
61
62
|
if (input.projectType !== undefined) {
|
|
62
|
-
payload.project_type = input.projectType;
|
|
63
|
+
payload.project_type = input.projectType.trim();
|
|
63
64
|
}
|
|
64
65
|
if (input.companySize !== undefined) {
|
|
65
|
-
payload.company_size = input.companySize;
|
|
66
|
+
payload.company_size = input.companySize.trim();
|
|
66
67
|
}
|
|
67
68
|
const response = await fetch(TESTIMONIALS_URL(config.projectUrl), {
|
|
68
69
|
method: 'POST',
|
|
@@ -87,7 +87,7 @@ export declare function checkSpecReviewGate(specId: string, projectId: string, _
|
|
|
87
87
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
88
88
|
* the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
|
|
89
89
|
*/
|
|
90
|
-
export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, force: boolean | undefined): Promise<DoneGateResult>;
|
|
90
|
+
export declare function checkDoneGates(spec: Spec, specId: string, projectId: string, projectPath: string | undefined, force: boolean | undefined, forceReason?: string): Promise<DoneGateResult>;
|
|
91
91
|
export interface ComplianceGateResult {
|
|
92
92
|
skipped: boolean;
|
|
93
93
|
score: number | null;
|
|
@@ -240,7 +240,7 @@ function forceDoneBypassLogPath(projectId) {
|
|
|
240
240
|
* Writes an audit record when force=true bypasses DoD gates on 'done' transition.
|
|
241
241
|
* Never throws — audit failures must not block transitions.
|
|
242
242
|
*/
|
|
243
|
-
async function recordForceDoneBypass(specId, projectId, failingItems, warningItems) {
|
|
243
|
+
async function recordForceDoneBypass(specId, projectId, reason, failingItems, warningItems) {
|
|
244
244
|
try {
|
|
245
245
|
const logPath = forceDoneBypassLogPath(projectId);
|
|
246
246
|
const entries = await readJson(logPath, []);
|
|
@@ -250,6 +250,7 @@ async function recordForceDoneBypass(specId, projectId, failingItems, warningIte
|
|
|
250
250
|
action: 'force_done_bypass',
|
|
251
251
|
specId,
|
|
252
252
|
projectId,
|
|
253
|
+
reason,
|
|
253
254
|
failingItems,
|
|
254
255
|
warningItems,
|
|
255
256
|
});
|
|
@@ -448,7 +449,7 @@ function specReviewGateError(args) {
|
|
|
448
449
|
* When force=true, gates are not blocking but failing items are recorded in
|
|
449
450
|
* the project's force-bypass-log.json for traceability (guard: done ≠ implemented).
|
|
450
451
|
*/
|
|
451
|
-
export async function checkDoneGates(spec, specId, projectId, projectPath, force) {
|
|
452
|
+
export async function checkDoneGates(spec, specId, projectId, projectPath, force, forceReason = 'No force reason provided') {
|
|
452
453
|
if (!force) {
|
|
453
454
|
const dodError = await checkDodGate(spec, specId, projectId, projectPath, false);
|
|
454
455
|
if (dodError) {
|
|
@@ -472,7 +473,7 @@ export async function checkDoneGates(spec, specId, projectId, projectPath, force
|
|
|
472
473
|
.map((i) => i.description);
|
|
473
474
|
if (failingItems.length > 0) {
|
|
474
475
|
// Record bypass in audit log (fire-and-forget)
|
|
475
|
-
void recordForceDoneBypass(specId, projectId, failingItems, warningItems);
|
|
476
|
+
void recordForceDoneBypass(specId, projectId, forceReason, failingItems, warningItems);
|
|
476
477
|
forcedBypassWarning =
|
|
477
478
|
`⚠️ force bypass: ${String(failingItems.length)} DoD item(s) were failing ` +
|
|
478
479
|
`(${failingItems.slice(0, 2).join('; ')}${failingItems.length > 2 ? '…' : ''}). ` +
|
|
@@ -38,6 +38,7 @@ import { detectHost } from '../../engine/host-detection/detect-host.js';
|
|
|
38
38
|
import { shellHygieneReminder } from '../../hosts/claude-code/coach.js';
|
|
39
39
|
import { buildTransitionEvidenceMeta, checkSddModelRoutingGate, } from '../../engine/sdd-model-routing.js';
|
|
40
40
|
import { acquireLock, releaseLock, isLocked as isCrossProcessLocked, LockBusyError, } from '../../engine/safety/cross-process-lock.js';
|
|
41
|
+
import { validateStrictLayoutOrError } from '../validate-helpers.js';
|
|
41
42
|
/**
|
|
42
43
|
* SPEC-280: Silently traverse intermediate states so callers can jump forward
|
|
43
44
|
* (e.g. draft → approved) without receiving an error.
|
|
@@ -231,6 +232,9 @@ function shouldSkipSddRoutingGateForLegacyTestHarness() {
|
|
|
231
232
|
function shouldSkipEvidenceGateForLegacyTestHarness() {
|
|
232
233
|
return process.env.VITEST === 'true' && process.env.PLANU_TEST_STRICT_EVIDENCE_GATES !== 'true';
|
|
233
234
|
}
|
|
235
|
+
function shouldSkipStrictLayoutGateForLegacyTestHarness() {
|
|
236
|
+
return process.env.VITEST === 'true' && process.env.PLANU_TEST_STRICT_LAYOUT_GATE !== 'true';
|
|
237
|
+
}
|
|
234
238
|
/* eslint-disable complexity, max-lines-per-function */
|
|
235
239
|
export async function handleUpdateStatus(params, server) {
|
|
236
240
|
return trackCost(params.projectPath ?? '', 'update_status', async () => {
|
|
@@ -477,7 +481,7 @@ export async function handleUpdateStatus(params, server) {
|
|
|
477
481
|
: Promise.resolve(null),
|
|
478
482
|
// Done gates: only relevant for 'done'
|
|
479
483
|
newStatus === 'done'
|
|
480
|
-
? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force)
|
|
484
|
+
? checkDoneGates(spec, specId, projectId, effectiveGatePath, params.force, params.forceStatusReason ?? params.reason ?? 'No force reason provided')
|
|
481
485
|
: Promise.resolve(null),
|
|
482
486
|
]);
|
|
483
487
|
// Process code reality result
|
|
@@ -545,6 +549,19 @@ export async function handleUpdateStatus(params, server) {
|
|
|
545
549
|
return validationReportError;
|
|
546
550
|
}
|
|
547
551
|
}
|
|
552
|
+
if (newStatus === 'done' &&
|
|
553
|
+
effectiveGatePath &&
|
|
554
|
+
!shouldSkipStrictLayoutGateForLegacyTestHarness()) {
|
|
555
|
+
const strictLayoutError = await validateStrictLayoutOrError({
|
|
556
|
+
projectPath: effectiveGatePath,
|
|
557
|
+
specId,
|
|
558
|
+
specPath: spec.specPath,
|
|
559
|
+
failClosedOnCrash: true,
|
|
560
|
+
});
|
|
561
|
+
if (strictLayoutError) {
|
|
562
|
+
return strictLayoutError;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
548
565
|
// Process crash shield result — record run timestamp on success (SPEC-628)
|
|
549
566
|
/* c8 ignore next */
|
|
550
567
|
if (crashRisksReport !== null) {
|
|
@@ -4,6 +4,7 @@ export declare function validateStrictLayoutOrError(args: {
|
|
|
4
4
|
projectPath: string;
|
|
5
5
|
specId: string;
|
|
6
6
|
specPath?: string;
|
|
7
|
+
failClosedOnCrash?: boolean;
|
|
7
8
|
}): Promise<ToolResult | null>;
|
|
8
9
|
export declare function buildValidateFailureFallback(failedCount: number): InteractiveQuestion;
|
|
9
10
|
//# sourceMappingURL=validate-helpers.d.ts.map
|
|
@@ -25,7 +25,25 @@ export async function validateStrictLayoutOrError(args) {
|
|
|
25
25
|
},
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
-
catch {
|
|
28
|
+
catch (err) {
|
|
29
|
+
if (args.failClosedOnCrash) {
|
|
30
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
31
|
+
return {
|
|
32
|
+
content: [
|
|
33
|
+
{
|
|
34
|
+
type: 'text',
|
|
35
|
+
text: `Strict Planu layout validation crashed for ${args.specId}.\n\n` +
|
|
36
|
+
`The done transition is blocked because canonical layout could not be verified.\n\n` +
|
|
37
|
+
`Error: ${message}`,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
isError: true,
|
|
41
|
+
structuredContent: {
|
|
42
|
+
error: 'strict_planu_layout_check_crashed',
|
|
43
|
+
message,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
29
47
|
return null;
|
|
30
48
|
}
|
|
31
49
|
}
|
package/dist/tools/validate.js
CHANGED
|
@@ -76,30 +76,7 @@ export async function handleValidate(args, server) {
|
|
|
76
76
|
const implementationQualityScore = calcQualityScore(result.qualityIssues);
|
|
77
77
|
const auditedFiles = [...new Set(result.qualityIssues.map((i) => i.file))];
|
|
78
78
|
// 6. SPEC-190: Convention compliance check
|
|
79
|
-
|
|
80
|
-
let regressionDetected = false;
|
|
81
|
-
try {
|
|
82
|
-
const rules = parseConventions(projectPath);
|
|
83
|
-
if (rules.length > 0) {
|
|
84
|
-
conventionViolations = scanConventions(projectPath, rules);
|
|
85
|
-
// Check regression against baseline
|
|
86
|
-
const comparison = await compareWithBaseline(projectId, {
|
|
87
|
-
rules,
|
|
88
|
-
violations: conventionViolations,
|
|
89
|
-
summary: {
|
|
90
|
-
totalRules: rules.length,
|
|
91
|
-
totalViolations: conventionViolations.length,
|
|
92
|
-
bySeverity: {},
|
|
93
|
-
byCategory: {},
|
|
94
|
-
},
|
|
95
|
-
scannedAt: new Date().toISOString(),
|
|
96
|
-
});
|
|
97
|
-
regressionDetected = comparison.regressionDetected;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
/* best-effort — don't fail validation */
|
|
102
|
-
}
|
|
79
|
+
const { conventionViolations, regressionDetected } = await scanProjectConventions(projectId, projectPath);
|
|
103
80
|
// 7. Lint check (best-effort — non-blocking)
|
|
104
81
|
const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
|
|
105
82
|
const assuranceGates = runAssuranceGates(projectPath);
|
|
@@ -123,6 +100,14 @@ export async function handleValidate(args, server) {
|
|
|
123
100
|
conventionRegression: regressionDetected,
|
|
124
101
|
minimalityReport: minimalityReport ?? undefined,
|
|
125
102
|
});
|
|
103
|
+
const qualityGateSummary = buildQualityGateSummary({
|
|
104
|
+
conventionViolations,
|
|
105
|
+
regressionDetected,
|
|
106
|
+
lintPassed: lintCheck.passed,
|
|
107
|
+
assurancePassed: assuranceGates.newCode.passed,
|
|
108
|
+
minimalityBlocked: minimalityReport?.blocked === true,
|
|
109
|
+
score: result.score,
|
|
110
|
+
});
|
|
126
111
|
// 8. Build output
|
|
127
112
|
const output = {
|
|
128
113
|
specId,
|
|
@@ -198,6 +183,7 @@ export async function handleValidate(args, server) {
|
|
|
198
183
|
},
|
|
199
184
|
lintCheck,
|
|
200
185
|
assuranceGates,
|
|
186
|
+
qualityGateSummary,
|
|
201
187
|
validationReport,
|
|
202
188
|
minimalityReport,
|
|
203
189
|
graphCoverage,
|
|
@@ -303,25 +289,22 @@ export async function handleValidate(args, server) {
|
|
|
303
289
|
const totalCriteria = result.fieldsTotal;
|
|
304
290
|
const passedCount = result.fieldsImplemented;
|
|
305
291
|
const failedCount = totalCriteria - passedCount;
|
|
292
|
+
const indeterminate = result.indeterminate ?? [];
|
|
293
|
+
const effectiveScore = qualityGateSummary.effectiveScore ?? scoreValue;
|
|
306
294
|
const compactText = buildCompactValidateText({
|
|
307
|
-
score:
|
|
295
|
+
score: effectiveScore,
|
|
308
296
|
passedCount,
|
|
309
297
|
failedCount,
|
|
310
298
|
totalCriteria,
|
|
311
299
|
missing: result.missing,
|
|
300
|
+
indeterminate,
|
|
312
301
|
commitReminder,
|
|
313
302
|
planuReminder,
|
|
314
303
|
});
|
|
315
304
|
const graphText = formatGraphCoverageText(graphCoverage);
|
|
316
305
|
// SPEC-512: Compact structuredContent — essential fields only at top level
|
|
317
|
-
const compactSummary =
|
|
318
|
-
|
|
319
|
-
score: result.score,
|
|
320
|
-
passed: passedCount,
|
|
321
|
-
failed: failedCount,
|
|
322
|
-
total: totalCriteria,
|
|
323
|
-
status: failedCount === 0 ? 'passing' : 'failing',
|
|
324
|
-
};
|
|
306
|
+
const compactSummary = buildCompactSummary(specId, result.score === null ? null : effectiveScore, passedCount, failedCount + (qualityGateSummary.passed ? 0 : 1), totalCriteria);
|
|
307
|
+
const structuredBase = { ...outputWithSuggestions, summary: compactSummary, indeterminate };
|
|
325
308
|
// SPEC-595: Elicit next action when validation fails and a server is available
|
|
326
309
|
if (server !== undefined && failedCount > 0) {
|
|
327
310
|
const { field, property } = buildEnumSchema('action', ['re-implement', 'ignore', 'mark-manual'], ['Fix failing criteria (Recommended)', 'Ignore failures', 'Mark as manually reviewed'], 'Next action', 're-implement');
|
|
@@ -339,8 +322,7 @@ export async function handleValidate(args, server) {
|
|
|
339
322
|
},
|
|
340
323
|
],
|
|
341
324
|
structuredContent: {
|
|
342
|
-
...
|
|
343
|
-
summary: compactSummary,
|
|
325
|
+
...structuredBase,
|
|
344
326
|
interactiveQuestions: outcome.interactiveQuestions,
|
|
345
327
|
},
|
|
346
328
|
};
|
|
@@ -352,8 +334,7 @@ export async function handleValidate(args, server) {
|
|
|
352
334
|
phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
|
|
353
335
|
content: [{ type: 'text', text: compactText + graphText }],
|
|
354
336
|
structuredContent: {
|
|
355
|
-
...
|
|
356
|
-
summary: compactSummary,
|
|
337
|
+
...structuredBase,
|
|
357
338
|
elicitedAction,
|
|
358
339
|
},
|
|
359
340
|
};
|
|
@@ -361,14 +342,65 @@ export async function handleValidate(args, server) {
|
|
|
361
342
|
return {
|
|
362
343
|
phaseEvent: createBasicPhaseEvent('validate', 'completed', 100, 'validating'),
|
|
363
344
|
content: [{ type: 'text', text: compactText + graphText }],
|
|
364
|
-
structuredContent:
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
345
|
+
structuredContent: structuredBase,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function scanProjectConventions(projectId, projectPath) {
|
|
349
|
+
try {
|
|
350
|
+
const rules = parseConventions(projectPath);
|
|
351
|
+
if (rules.length === 0) {
|
|
352
|
+
return { conventionViolations: [], regressionDetected: false };
|
|
353
|
+
}
|
|
354
|
+
const conventionViolations = scanConventions(projectPath, rules);
|
|
355
|
+
const comparison = await compareWithBaseline(projectId, {
|
|
356
|
+
rules,
|
|
357
|
+
violations: conventionViolations,
|
|
358
|
+
summary: {
|
|
359
|
+
totalRules: rules.length,
|
|
360
|
+
totalViolations: conventionViolations.length,
|
|
361
|
+
bySeverity: {},
|
|
362
|
+
byCategory: {},
|
|
363
|
+
},
|
|
364
|
+
scannedAt: new Date().toISOString(),
|
|
365
|
+
});
|
|
366
|
+
return { conventionViolations, regressionDetected: comparison.regressionDetected };
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
return { conventionViolations: [], regressionDetected: false };
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function buildQualityGateSummary(input) {
|
|
373
|
+
const blockingConventionViolations = input.conventionViolations.filter((violation) => violation.severity === 'critical');
|
|
374
|
+
const failures = [
|
|
375
|
+
...(input.regressionDetected ? ['convention-regression'] : []),
|
|
376
|
+
...(blockingConventionViolations.length > 0 ? ['blocking-convention-violations'] : []),
|
|
377
|
+
...(!input.lintPassed ? ['lint'] : []),
|
|
378
|
+
...(!input.assurancePassed ? ['assurance'] : []),
|
|
379
|
+
...(input.minimalityBlocked ? ['minimality'] : []),
|
|
380
|
+
];
|
|
381
|
+
return {
|
|
382
|
+
passed: failures.length === 0,
|
|
383
|
+
effectiveScore: failures.length === 0 ? input.score : Math.min(input.score ?? 0, 99),
|
|
384
|
+
failures,
|
|
385
|
+
conventionRegression: input.regressionDetected,
|
|
386
|
+
blockingConventionViolations: blockingConventionViolations.length,
|
|
387
|
+
lintPassed: input.lintPassed,
|
|
388
|
+
assurancePassed: input.assurancePassed,
|
|
389
|
+
minimalityPassed: !input.minimalityBlocked,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
function buildCompactSummary(specId, score, passedCount, failedCount, totalCriteria) {
|
|
393
|
+
return {
|
|
394
|
+
specId,
|
|
395
|
+
score,
|
|
396
|
+
passed: passedCount,
|
|
397
|
+
failed: failedCount,
|
|
398
|
+
total: totalCriteria,
|
|
399
|
+
status: failedCount === 0 ? 'passing' : 'failing',
|
|
368
400
|
};
|
|
369
401
|
}
|
|
370
402
|
function buildCompactValidateText(args) {
|
|
371
|
-
const { score, passedCount, failedCount, totalCriteria, missing, commitReminder, planuReminder } = args;
|
|
403
|
+
const { score, passedCount, failedCount, totalCriteria, missing, indeterminate, commitReminder, planuReminder, } = args;
|
|
372
404
|
if (failedCount === 0) {
|
|
373
405
|
// All passing — single line
|
|
374
406
|
return (`✅ Validate: ${String(score)}/100 — ${String(passedCount)}/${String(totalCriteria)} criteria passing` +
|
|
@@ -377,11 +409,20 @@ function buildCompactValidateText(args) {
|
|
|
377
409
|
}
|
|
378
410
|
// Failures exist — list only the failing ones (max 5)
|
|
379
411
|
const MAX_FAILURES = 5;
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
412
|
+
const labelledFailures = [
|
|
413
|
+
...missing.map((criterion) => ({ label: 'missing', criterion })),
|
|
414
|
+
...indeterminate.map((criterion) => ({ label: 'indeterminate', criterion })),
|
|
415
|
+
];
|
|
416
|
+
const shownFailures = labelledFailures.slice(0, MAX_FAILURES);
|
|
417
|
+
const remainingCount = labelledFailures.length - shownFailures.length;
|
|
418
|
+
const failureList = shownFailures
|
|
419
|
+
.map((f) => ` - ${f.label === 'missing' ? f.criterion : `${f.label}: ${f.criterion}`}`)
|
|
420
|
+
.join('\n');
|
|
383
421
|
const moreLine = remainingCount > 0 ? `\n ... and ${String(remainingCount)} more` : '';
|
|
384
|
-
const failNames = shownFailures
|
|
422
|
+
const failNames = shownFailures
|
|
423
|
+
.slice(0, 3)
|
|
424
|
+
.map((failure) => `${failure.label}: ${failure.criterion}`)
|
|
425
|
+
.join(', ');
|
|
385
426
|
const summaryLine = `❌ Validate: ${String(score)}/100 — ${String(failedCount)} failing: [${failNames}${remainingCount + shownFailures.length > 3 ? '...' : ''}]`;
|
|
386
427
|
return summaryLine + '\n' + failureList + moreLine + commitReminder + planuReminder;
|
|
387
428
|
}
|
package/dist/types/analysis.d.ts
CHANGED
|
@@ -183,6 +183,8 @@ export interface HolisticValidateReport {
|
|
|
183
183
|
export interface ValidateResult {
|
|
184
184
|
matches: string[];
|
|
185
185
|
missing: string[];
|
|
186
|
+
/** Criteria that could not be proven or disproven with available evidence. */
|
|
187
|
+
indeterminate?: string[];
|
|
186
188
|
extra: string[];
|
|
187
189
|
fieldsImplemented: number;
|
|
188
190
|
fieldsTotal: number;
|
|
@@ -198,6 +200,10 @@ export interface ValidateResult {
|
|
|
198
200
|
*/
|
|
199
201
|
holisticReport?: HolisticValidateReport;
|
|
200
202
|
}
|
|
203
|
+
export type CriterionEvidenceStatus = 'proven' | 'missing' | 'indeterminate';
|
|
204
|
+
export interface CriterionEvidenceResult {
|
|
205
|
+
status: CriterionEvidenceStatus;
|
|
206
|
+
}
|
|
201
207
|
export interface CodeState {
|
|
202
208
|
affectedFiles: string[];
|
|
203
209
|
unexpectedFiles: string[];
|
|
@@ -79,6 +79,17 @@ export interface InitProjectResultInput {
|
|
|
79
79
|
migratedCount: number;
|
|
80
80
|
errors: string[];
|
|
81
81
|
} | null;
|
|
82
|
+
criticalMigrationFailures?: {
|
|
83
|
+
phase: string;
|
|
84
|
+
severity: 'critical' | 'nonCritical';
|
|
85
|
+
message: string;
|
|
86
|
+
}[];
|
|
87
|
+
nonCriticalMigrationWarnings?: {
|
|
88
|
+
phase: string;
|
|
89
|
+
severity: 'critical' | 'nonCritical';
|
|
90
|
+
message: string;
|
|
91
|
+
}[];
|
|
92
|
+
migrationReportPath?: string | null;
|
|
82
93
|
hooksInstalled: string[];
|
|
83
94
|
gitFlowType: string;
|
|
84
95
|
gitignoreUpdated: boolean;
|
package/dist/types/schema.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.10.
|
|
3
|
+
"version": "4.10.4",
|
|
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,19 +34,19 @@
|
|
|
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.4",
|
|
38
|
+
"@planu/core-darwin-x64": "4.10.4",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.10.4",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.10.4",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.10.4",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.10.4",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.10.4",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.10.4"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
48
48
|
},
|
|
49
|
-
"packageManager": "pnpm@
|
|
49
|
+
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
|
50
50
|
"scripts": {
|
|
51
51
|
"build:rust": "bash scripts/build-rust-local.sh --host",
|
|
52
52
|
"build:rust:all": "bash scripts/build-rust-local.sh --all",
|
|
@@ -135,38 +135,6 @@
|
|
|
135
135
|
"yaml": "^2.9.0",
|
|
136
136
|
"zod": "^4.4.3"
|
|
137
137
|
},
|
|
138
|
-
"pnpm": {
|
|
139
|
-
"overrides": {
|
|
140
|
-
"flatted": ">=3.4.2",
|
|
141
|
-
"path-to-regexp": ">=8.4.0",
|
|
142
|
-
"brace-expansion": ">=5.0.5",
|
|
143
|
-
"handlebars": ">=4.7.9",
|
|
144
|
-
"picomatch": ">=4.0.4",
|
|
145
|
-
"yaml": ">=2.8.3",
|
|
146
|
-
"lodash": ">=4.18.0",
|
|
147
|
-
"lodash-es": ">=4.18.0",
|
|
148
|
-
"hono": ">=4.12.14",
|
|
149
|
-
"postcss": ">=8.5.10",
|
|
150
|
-
"esbuild": "0.28.1",
|
|
151
|
-
"fast-uri": ">=3.1.2",
|
|
152
|
-
"qs": ">=6.15.2"
|
|
153
|
-
},
|
|
154
|
-
"supportedArchitectures": {
|
|
155
|
-
"os": [
|
|
156
|
-
"darwin",
|
|
157
|
-
"linux",
|
|
158
|
-
"win32"
|
|
159
|
-
],
|
|
160
|
-
"cpu": [
|
|
161
|
-
"arm64",
|
|
162
|
-
"x64"
|
|
163
|
-
],
|
|
164
|
-
"libc": [
|
|
165
|
-
"glibc",
|
|
166
|
-
"musl"
|
|
167
|
-
]
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
138
|
"devDependencies": {
|
|
171
139
|
"@commitlint/cli": "^21.2.0",
|
|
172
140
|
"@commitlint/config-conventional": "^21.2.0",
|
|
@@ -200,7 +168,7 @@
|
|
|
200
168
|
"prettier": "^3.9.4",
|
|
201
169
|
"secretlint": "^13.0.2",
|
|
202
170
|
"semantic-release": "^25.0.5",
|
|
203
|
-
"tsc-alias": "^1.
|
|
171
|
+
"tsc-alias": "^1.9.0",
|
|
204
172
|
"type-coverage": "^2.29.7",
|
|
205
173
|
"typescript": "^6.0.3",
|
|
206
174
|
"typescript-eslint": "^8.62.1",
|
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.4",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": [
|
|
8
8
|
"npx",
|