@planu/cli 4.7.1 → 4.7.2
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 -1
- package/dist/config/minimal-implementation-gate.json +110 -0
- package/dist/engine/handoff-artifacts/schemas.d.ts +112 -0
- package/dist/engine/handoff-artifacts/schemas.js +40 -0
- package/dist/engine/minimality/analyzer.d.ts +3 -0
- package/dist/engine/minimality/analyzer.js +140 -0
- package/dist/engine/minimality/formatter.d.ts +3 -0
- package/dist/engine/minimality/formatter.js +25 -0
- package/dist/engine/minimality/index.d.ts +4 -0
- package/dist/engine/minimality/index.js +4 -0
- package/dist/engine/minimality/policy-loader.d.ts +3 -0
- package/dist/engine/minimality/policy-loader.js +133 -0
- package/dist/engine/validator/validation-report-writer.d.ts +2 -0
- package/dist/engine/validator/validation-report-writer.js +19 -0
- package/dist/tools/challenge-spec.js +25 -0
- package/dist/tools/package-handoff.js +23 -1
- package/dist/tools/update-status/dod-gates.js +9 -0
- package/dist/tools/validate.js +34 -0
- package/dist/types/handoff-artifacts.d.ts +2 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/minimal-implementation-gate.d.ts +92 -0
- package/dist/types/minimal-implementation-gate.js +2 -0
- package/package.json +17 -17
- package/planu-native.json +8 -29
- package/planu-plugin.json +7 -35
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
## [4.7.2] - 2026-06-16
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
- feat(SPEC-1088): add policy-driven minimal implementation gate
|
|
5
|
+
|
|
6
|
+
### Chores
|
|
7
|
+
- chore(deps): update patch/minor dependencies
|
|
8
|
+
|
|
9
|
+
|
|
1
10
|
## [4.7.1] - 2026-06-12
|
|
2
11
|
|
|
3
12
|
### Chores
|
|
@@ -4085,4 +4094,4 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) · Versioning:
|
|
|
4085
4094
|
- Mermaid diagram generation (architecture, sequence, state machine, ER, data flow)
|
|
4086
4095
|
- Multi-language i18n (EN/ES/PT) for generated specs
|
|
4087
4096
|
- Clean Architecture (hexagonal) — engine, tools, storage, types layers
|
|
4088
|
-
- 10,857 tests with ≥95% coverage
|
|
4097
|
+
- 10,857 tests with ≥95% coverage
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"enabled": true,
|
|
4
|
+
"report": {
|
|
5
|
+
"maxFindings": 12,
|
|
6
|
+
"maxMarkdownLines": 12,
|
|
7
|
+
"maxEvidenceChars": 180
|
|
8
|
+
},
|
|
9
|
+
"tags": {
|
|
10
|
+
"delete": { "description": "Remove avoidable code or files." },
|
|
11
|
+
"stdlib": { "description": "Use a standard library capability before adding custom code." },
|
|
12
|
+
"native": { "description": "Use an existing platform or Planu-native capability." },
|
|
13
|
+
"installed-dependency": { "description": "Use an already-installed dependency instead of adding another." },
|
|
14
|
+
"yagni": { "description": "Avoid speculative code that is not required by the approved spec." },
|
|
15
|
+
"shrink": { "description": "Reduce an oversized abstraction or workflow." },
|
|
16
|
+
"dependency": { "description": "Avoid an unnecessary new package or dependency path." },
|
|
17
|
+
"file-scope": { "description": "Keep implementation inside the approved file scope." },
|
|
18
|
+
"debt-marker": { "description": "Record accepted shortcuts as structured debt evidence." }
|
|
19
|
+
},
|
|
20
|
+
"rules": [
|
|
21
|
+
{
|
|
22
|
+
"id": "minimality.delete.dead-compat",
|
|
23
|
+
"tag": "delete",
|
|
24
|
+
"severity": "warning",
|
|
25
|
+
"confidence": "medium",
|
|
26
|
+
"patterns": ["legacy compatibility", "deprecated fallback", "unused fallback"],
|
|
27
|
+
"replacementGuidance": "Remove the compatibility path unless the approved spec explicitly requires backward compatibility."
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"id": "minimality.stdlib.custom-parser",
|
|
31
|
+
"tag": "stdlib",
|
|
32
|
+
"severity": "warning",
|
|
33
|
+
"confidence": "medium",
|
|
34
|
+
"patterns": ["custom parser", "manual parser", "hand rolled parser"],
|
|
35
|
+
"replacementGuidance": "Use an existing parser from the runtime or current dependency set before writing parsing code."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"id": "minimality.native.existing-planu-surface",
|
|
39
|
+
"tag": "native",
|
|
40
|
+
"severity": "warning",
|
|
41
|
+
"confidence": "high",
|
|
42
|
+
"patterns": ["new public mcp tool", "new command", "new cli command"],
|
|
43
|
+
"replacementGuidance": "Prefer existing Planu SDD surfaces unless the spec proves a new public surface is required."
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
"id": "minimality.installed-dependency.add-package",
|
|
47
|
+
"tag": "installed-dependency",
|
|
48
|
+
"severity": "warning",
|
|
49
|
+
"confidence": "medium",
|
|
50
|
+
"patterns": ["add dependency", "new dependency", "install package"],
|
|
51
|
+
"replacementGuidance": "Check package.json first and reuse an already-installed dependency when possible."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"id": "minimality.yagni.future-proofing",
|
|
55
|
+
"tag": "yagni",
|
|
56
|
+
"severity": "blocker",
|
|
57
|
+
"confidence": "high",
|
|
58
|
+
"patterns": ["future-proof", "just in case", "might need later", "eventually support"],
|
|
59
|
+
"replacementGuidance": "Remove speculative scope unless it is directly required by an acceptance criterion.",
|
|
60
|
+
"blockWhenRiskAtLeast": "medium"
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "minimality.shrink.over-architecture",
|
|
64
|
+
"tag": "shrink",
|
|
65
|
+
"severity": "warning",
|
|
66
|
+
"confidence": "medium",
|
|
67
|
+
"patterns": ["framework", "orchestration layer", "plugin architecture", "abstract factory"],
|
|
68
|
+
"replacementGuidance": "Shrink the design to the smallest correct module/function that satisfies the spec."
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
"id": "minimality.dependency.new-runtime",
|
|
72
|
+
"tag": "dependency",
|
|
73
|
+
"severity": "blocker",
|
|
74
|
+
"confidence": "high",
|
|
75
|
+
"patterns": ["npm install", "pnpm add", "yarn add"],
|
|
76
|
+
"replacementGuidance": "Avoid adding runtime dependencies unless the approved spec explicitly requires one.",
|
|
77
|
+
"blockWhenRiskAtLeast": "low"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "minimality.file-scope.unapproved-area",
|
|
81
|
+
"tag": "file-scope",
|
|
82
|
+
"severity": "warning",
|
|
83
|
+
"confidence": "medium",
|
|
84
|
+
"patterns": ["touch unrelated", "broad refactor", "repo-wide refactor"],
|
|
85
|
+
"replacementGuidance": "Keep edits inside the approved technical references and tests."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "minimality.debt-marker.comment-only",
|
|
89
|
+
"tag": "debt-marker",
|
|
90
|
+
"severity": "warning",
|
|
91
|
+
"confidence": "medium",
|
|
92
|
+
"patterns": ["todo debt", "temporary hack", "shortcut accepted"],
|
|
93
|
+
"replacementGuidance": "Record accepted shortcuts as structured debt evidence with ceiling and upgrade trigger."
|
|
94
|
+
}
|
|
95
|
+
],
|
|
96
|
+
"safetyExclusions": [
|
|
97
|
+
"security",
|
|
98
|
+
"accessibility",
|
|
99
|
+
"data-loss",
|
|
100
|
+
"compliance",
|
|
101
|
+
"money",
|
|
102
|
+
"access-control",
|
|
103
|
+
"explicitly requested",
|
|
104
|
+
"trust boundary"
|
|
105
|
+
],
|
|
106
|
+
"excludedPathPatterns": ["dist/**", "coverage/**", "node_modules/**", "pnpm-lock.yaml", "*.snap"],
|
|
107
|
+
"debtEvidence": {
|
|
108
|
+
"requiredFields": ["specId", "files", "ceiling", "upgradeTrigger", "reviewRationale"]
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -96,6 +96,62 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
|
|
|
96
96
|
evidence: z.ZodArray<z.ZodString>;
|
|
97
97
|
}, z.core.$strip>>;
|
|
98
98
|
}, z.core.$strip>>;
|
|
99
|
+
minimalityReport: z.ZodOptional<z.ZodObject<{
|
|
100
|
+
enabled: z.ZodBoolean;
|
|
101
|
+
blocked: z.ZodBoolean;
|
|
102
|
+
findings: z.ZodArray<z.ZodObject<{
|
|
103
|
+
ruleId: z.ZodString;
|
|
104
|
+
tag: z.ZodString;
|
|
105
|
+
severity: z.ZodEnum<{
|
|
106
|
+
blocker: "blocker";
|
|
107
|
+
warning: "warning";
|
|
108
|
+
info: "info";
|
|
109
|
+
}>;
|
|
110
|
+
confidence: z.ZodEnum<{
|
|
111
|
+
low: "low";
|
|
112
|
+
medium: "medium";
|
|
113
|
+
high: "high";
|
|
114
|
+
}>;
|
|
115
|
+
target: z.ZodString;
|
|
116
|
+
line: z.ZodOptional<z.ZodNumber>;
|
|
117
|
+
evidence: z.ZodString;
|
|
118
|
+
replacementGuidance: z.ZodString;
|
|
119
|
+
blocksDone: z.ZodBoolean;
|
|
120
|
+
evidenceSource: z.ZodArray<z.ZodObject<{
|
|
121
|
+
source: z.ZodEnum<{
|
|
122
|
+
spec: "spec";
|
|
123
|
+
runtime: "runtime";
|
|
124
|
+
policy: "policy";
|
|
125
|
+
"project-config": "project-config";
|
|
126
|
+
}>;
|
|
127
|
+
key: z.ZodString;
|
|
128
|
+
value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
129
|
+
}, z.core.$strip>>;
|
|
130
|
+
}, z.core.$strip>>;
|
|
131
|
+
safetyExceptions: z.ZodArray<z.ZodObject<{
|
|
132
|
+
target: z.ZodString;
|
|
133
|
+
reason: z.ZodString;
|
|
134
|
+
evidence: z.ZodString;
|
|
135
|
+
}, z.core.$strip>>;
|
|
136
|
+
debtEvidence: z.ZodArray<z.ZodObject<{
|
|
137
|
+
specId: z.ZodString;
|
|
138
|
+
files: z.ZodArray<z.ZodString>;
|
|
139
|
+
ceiling: z.ZodString;
|
|
140
|
+
upgradeTrigger: z.ZodString;
|
|
141
|
+
reviewRationale: z.ZodString;
|
|
142
|
+
}, z.core.$strip>>;
|
|
143
|
+
evidence: z.ZodArray<z.ZodObject<{
|
|
144
|
+
source: z.ZodEnum<{
|
|
145
|
+
spec: "spec";
|
|
146
|
+
runtime: "runtime";
|
|
147
|
+
policy: "policy";
|
|
148
|
+
"project-config": "project-config";
|
|
149
|
+
}>;
|
|
150
|
+
key: z.ZodString;
|
|
151
|
+
value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
152
|
+
}, z.core.$strip>>;
|
|
153
|
+
markdown: z.ZodString;
|
|
154
|
+
}, z.core.$strip>>;
|
|
99
155
|
score: z.ZodOptional<z.ZodNumber>;
|
|
100
156
|
completedAt: z.ZodISODateTime;
|
|
101
157
|
}, z.core.$strip>;
|
|
@@ -194,6 +250,62 @@ export declare const ARTIFACT_SCHEMAS: {
|
|
|
194
250
|
evidence: z.ZodArray<z.ZodString>;
|
|
195
251
|
}, z.core.$strip>>;
|
|
196
252
|
}, z.core.$strip>>;
|
|
253
|
+
minimalityReport: z.ZodOptional<z.ZodObject<{
|
|
254
|
+
enabled: z.ZodBoolean;
|
|
255
|
+
blocked: z.ZodBoolean;
|
|
256
|
+
findings: z.ZodArray<z.ZodObject<{
|
|
257
|
+
ruleId: z.ZodString;
|
|
258
|
+
tag: z.ZodString;
|
|
259
|
+
severity: z.ZodEnum<{
|
|
260
|
+
blocker: "blocker";
|
|
261
|
+
warning: "warning";
|
|
262
|
+
info: "info";
|
|
263
|
+
}>;
|
|
264
|
+
confidence: z.ZodEnum<{
|
|
265
|
+
low: "low";
|
|
266
|
+
medium: "medium";
|
|
267
|
+
high: "high";
|
|
268
|
+
}>;
|
|
269
|
+
target: z.ZodString;
|
|
270
|
+
line: z.ZodOptional<z.ZodNumber>;
|
|
271
|
+
evidence: z.ZodString;
|
|
272
|
+
replacementGuidance: z.ZodString;
|
|
273
|
+
blocksDone: z.ZodBoolean;
|
|
274
|
+
evidenceSource: z.ZodArray<z.ZodObject<{
|
|
275
|
+
source: z.ZodEnum<{
|
|
276
|
+
spec: "spec";
|
|
277
|
+
runtime: "runtime";
|
|
278
|
+
policy: "policy";
|
|
279
|
+
"project-config": "project-config";
|
|
280
|
+
}>;
|
|
281
|
+
key: z.ZodString;
|
|
282
|
+
value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
283
|
+
}, z.core.$strip>>;
|
|
284
|
+
}, z.core.$strip>>;
|
|
285
|
+
safetyExceptions: z.ZodArray<z.ZodObject<{
|
|
286
|
+
target: z.ZodString;
|
|
287
|
+
reason: z.ZodString;
|
|
288
|
+
evidence: z.ZodString;
|
|
289
|
+
}, z.core.$strip>>;
|
|
290
|
+
debtEvidence: z.ZodArray<z.ZodObject<{
|
|
291
|
+
specId: z.ZodString;
|
|
292
|
+
files: z.ZodArray<z.ZodString>;
|
|
293
|
+
ceiling: z.ZodString;
|
|
294
|
+
upgradeTrigger: z.ZodString;
|
|
295
|
+
reviewRationale: z.ZodString;
|
|
296
|
+
}, z.core.$strip>>;
|
|
297
|
+
evidence: z.ZodArray<z.ZodObject<{
|
|
298
|
+
source: z.ZodEnum<{
|
|
299
|
+
spec: "spec";
|
|
300
|
+
runtime: "runtime";
|
|
301
|
+
policy: "policy";
|
|
302
|
+
"project-config": "project-config";
|
|
303
|
+
}>;
|
|
304
|
+
key: z.ZodString;
|
|
305
|
+
value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
|
|
306
|
+
}, z.core.$strip>>;
|
|
307
|
+
markdown: z.ZodString;
|
|
308
|
+
}, z.core.$strip>>;
|
|
197
309
|
score: z.ZodOptional<z.ZodNumber>;
|
|
198
310
|
completedAt: z.ZodISODateTime;
|
|
199
311
|
}, z.core.$strip>;
|
|
@@ -73,6 +73,46 @@ export const ValidationReportV1Schema = z.object({
|
|
|
73
73
|
})),
|
|
74
74
|
})
|
|
75
75
|
.optional(),
|
|
76
|
+
minimalityReport: z
|
|
77
|
+
.object({
|
|
78
|
+
enabled: z.boolean(),
|
|
79
|
+
blocked: z.boolean(),
|
|
80
|
+
findings: z.array(z.object({
|
|
81
|
+
ruleId: z.string(),
|
|
82
|
+
tag: z.string(),
|
|
83
|
+
severity: z.enum(['info', 'warning', 'blocker']),
|
|
84
|
+
confidence: z.enum(['low', 'medium', 'high']),
|
|
85
|
+
target: z.string(),
|
|
86
|
+
line: z.number().optional(),
|
|
87
|
+
evidence: z.string(),
|
|
88
|
+
replacementGuidance: z.string(),
|
|
89
|
+
blocksDone: z.boolean(),
|
|
90
|
+
evidenceSource: z.array(z.object({
|
|
91
|
+
source: z.enum(['policy', 'project-config', 'runtime', 'spec']),
|
|
92
|
+
key: z.string(),
|
|
93
|
+
value: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
94
|
+
})),
|
|
95
|
+
})),
|
|
96
|
+
safetyExceptions: z.array(z.object({
|
|
97
|
+
target: z.string(),
|
|
98
|
+
reason: z.string(),
|
|
99
|
+
evidence: z.string(),
|
|
100
|
+
})),
|
|
101
|
+
debtEvidence: z.array(z.object({
|
|
102
|
+
specId: z.string(),
|
|
103
|
+
files: z.array(z.string()),
|
|
104
|
+
ceiling: z.string(),
|
|
105
|
+
upgradeTrigger: z.string(),
|
|
106
|
+
reviewRationale: z.string(),
|
|
107
|
+
})),
|
|
108
|
+
evidence: z.array(z.object({
|
|
109
|
+
source: z.enum(['policy', 'project-config', 'runtime', 'spec']),
|
|
110
|
+
key: z.string(),
|
|
111
|
+
value: z.union([z.string(), z.number(), z.boolean()]).optional(),
|
|
112
|
+
})),
|
|
113
|
+
markdown: z.string(),
|
|
114
|
+
})
|
|
115
|
+
.optional(),
|
|
76
116
|
score: z.number().optional(),
|
|
77
117
|
completedAt: z.iso.datetime(),
|
|
78
118
|
});
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MinimalImplementationInput, MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function analyzeMinimalImplementation(input: MinimalImplementationInput): MinimalImplementationReport;
|
|
3
|
+
//# sourceMappingURL=analyzer.d.ts.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { formatMinimalImplementationReport } from './formatter.js';
|
|
2
|
+
const RISK_ORDER = ['low', 'medium', 'high', 'max'];
|
|
3
|
+
function riskRank(risk) {
|
|
4
|
+
const index = RISK_ORDER.indexOf(risk);
|
|
5
|
+
return index >= 0 ? index : 1;
|
|
6
|
+
}
|
|
7
|
+
function isExcludedPath(path, policy) {
|
|
8
|
+
return policy.excludedPathPatterns.some((pattern) => {
|
|
9
|
+
const escaped = pattern
|
|
10
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
11
|
+
.replace(/\*\*/g, '.*')
|
|
12
|
+
.replace(/\*/g, '[^/]*');
|
|
13
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function firstMatchingSafetyExclusion(text, policy) {
|
|
17
|
+
const lower = text.toLowerCase();
|
|
18
|
+
return policy.safetyExclusions.find((term) => lower.includes(term.toLowerCase())) ?? null;
|
|
19
|
+
}
|
|
20
|
+
function trimEvidence(value, maxChars) {
|
|
21
|
+
const compact = value.replace(/\s+/g, ' ').trim();
|
|
22
|
+
return compact.length > maxChars ? `${compact.slice(0, maxChars - 1)}...` : compact;
|
|
23
|
+
}
|
|
24
|
+
function lineForMatch(content, index) {
|
|
25
|
+
return content.slice(0, index).split('\n').length;
|
|
26
|
+
}
|
|
27
|
+
function ruleBlocksDone(rule, specRisk) {
|
|
28
|
+
if (rule.severity !== 'blocker') {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
if (rule.blockWhenRiskAtLeast === undefined) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return riskRank(specRisk) >= riskRank(rule.blockWhenRiskAtLeast);
|
|
35
|
+
}
|
|
36
|
+
function makeEvidence(input, rule) {
|
|
37
|
+
return [...(input.evidence ?? []), { source: 'policy', key: rule.id }];
|
|
38
|
+
}
|
|
39
|
+
function scanText(args) {
|
|
40
|
+
for (const pattern of args.rule.patterns) {
|
|
41
|
+
const index = args.content.toLowerCase().indexOf(pattern.toLowerCase());
|
|
42
|
+
if (index < 0) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const evidence = trimEvidence(args.content.slice(index, index + 220), args.policy.report.maxEvidenceChars);
|
|
46
|
+
const safetyTerm = firstMatchingSafetyExclusion(args.content, args.policy);
|
|
47
|
+
if (safetyTerm !== null) {
|
|
48
|
+
return {
|
|
49
|
+
safety: {
|
|
50
|
+
target: args.target,
|
|
51
|
+
reason: `Matched safety exclusion: ${safetyTerm}`,
|
|
52
|
+
evidence,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
finding: {
|
|
58
|
+
ruleId: args.rule.id,
|
|
59
|
+
tag: args.rule.tag,
|
|
60
|
+
severity: args.rule.severity,
|
|
61
|
+
confidence: args.rule.confidence,
|
|
62
|
+
target: args.target,
|
|
63
|
+
line: args.target.includes(':') ? undefined : lineForMatch(args.content, index),
|
|
64
|
+
evidence,
|
|
65
|
+
replacementGuidance: args.rule.replacementGuidance,
|
|
66
|
+
blocksDone: ruleBlocksDone(args.rule, args.input.specRisk),
|
|
67
|
+
evidenceSource: makeEvidence(args.input, args.rule),
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return {};
|
|
72
|
+
}
|
|
73
|
+
function collectSources(input) {
|
|
74
|
+
const virtual = [];
|
|
75
|
+
if (input.specText) {
|
|
76
|
+
virtual.push({ path: 'spec.md', content: input.specText });
|
|
77
|
+
}
|
|
78
|
+
if (input.handoffText) {
|
|
79
|
+
virtual.push({ path: 'handoff', content: input.handoffText });
|
|
80
|
+
}
|
|
81
|
+
if (input.packageManifestText) {
|
|
82
|
+
virtual.push({ path: 'package.json', content: input.packageManifestText });
|
|
83
|
+
}
|
|
84
|
+
return [...virtual, ...(input.files ?? [])];
|
|
85
|
+
}
|
|
86
|
+
export function analyzeMinimalImplementation(input) {
|
|
87
|
+
const { policy } = input;
|
|
88
|
+
if (!policy.enabled) {
|
|
89
|
+
return {
|
|
90
|
+
enabled: false,
|
|
91
|
+
blocked: false,
|
|
92
|
+
findings: [],
|
|
93
|
+
safetyExceptions: [],
|
|
94
|
+
debtEvidence: input.acceptedDebt ?? [],
|
|
95
|
+
evidence: input.evidence ?? [],
|
|
96
|
+
markdown: '',
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
const findings = [];
|
|
100
|
+
const safetyExceptions = [];
|
|
101
|
+
for (const source of collectSources(input)) {
|
|
102
|
+
if (isExcludedPath(source.path, policy) || source.content === undefined) {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const rule of policy.rules) {
|
|
106
|
+
const result = scanText({
|
|
107
|
+
target: source.path,
|
|
108
|
+
content: source.content,
|
|
109
|
+
policy,
|
|
110
|
+
input,
|
|
111
|
+
rule,
|
|
112
|
+
});
|
|
113
|
+
if (result.safety) {
|
|
114
|
+
safetyExceptions.push(result.safety);
|
|
115
|
+
}
|
|
116
|
+
if (result.finding) {
|
|
117
|
+
findings.push(result.finding);
|
|
118
|
+
}
|
|
119
|
+
if (findings.length >= policy.report.maxFindings) {
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (findings.length >= policy.report.maxFindings) {
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const report = {
|
|
128
|
+
enabled: true,
|
|
129
|
+
blocked: findings.some((finding) => finding.blocksDone),
|
|
130
|
+
findings,
|
|
131
|
+
safetyExceptions,
|
|
132
|
+
debtEvidence: input.acceptedDebt ?? [],
|
|
133
|
+
evidence: input.evidence ?? [],
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
...report,
|
|
137
|
+
markdown: formatMinimalImplementationReport({ ...report, markdown: '' }, policy),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=analyzer.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { MinimalImplementationPolicy, MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function formatMinimalImplementationReport(report: MinimalImplementationReport, policy?: MinimalImplementationPolicy): string;
|
|
3
|
+
//# sourceMappingURL=formatter.d.ts.map
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export function formatMinimalImplementationReport(report, policy) {
|
|
2
|
+
if (!report.enabled) {
|
|
3
|
+
return '';
|
|
4
|
+
}
|
|
5
|
+
const maxLines = policy?.report.maxMarkdownLines ?? 12;
|
|
6
|
+
const lines = ['## Minimal Implementation', ''];
|
|
7
|
+
if (report.findings.length === 0) {
|
|
8
|
+
lines.push('- No avoidable complexity findings from the current policy.');
|
|
9
|
+
}
|
|
10
|
+
else {
|
|
11
|
+
for (const finding of report.findings.slice(0, Math.max(1, maxLines - 4))) {
|
|
12
|
+
const location = finding.line !== undefined ? `${finding.target}:${String(finding.line)}` : finding.target;
|
|
13
|
+
const marker = finding.blocksDone ? 'BLOCK' : finding.severity.toUpperCase();
|
|
14
|
+
lines.push(`- [${marker}] ${finding.tag} ${location}: ${finding.replacementGuidance}`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
if (report.safetyExceptions.length > 0) {
|
|
18
|
+
lines.push(`- Safety exceptions preserved: ${String(report.safetyExceptions.length)} required safeguard(s).`);
|
|
19
|
+
}
|
|
20
|
+
if (report.debtEvidence.length > 0) {
|
|
21
|
+
lines.push(`- Structured debt evidence recorded: ${String(report.debtEvidence.length)} item(s).`);
|
|
22
|
+
}
|
|
23
|
+
return lines.slice(0, maxLines).join('\n');
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=formatter.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { LoadedMinimalImplementationPolicy, MinimalImplementationPolicyLoadOptions } from '../../types/minimal-implementation-gate.js';
|
|
2
|
+
export declare function loadMinimalImplementationPolicy(projectPath?: string, options?: MinimalImplementationPolicyLoadOptions): Promise<LoadedMinimalImplementationPolicy>;
|
|
3
|
+
//# sourceMappingURL=policy-loader.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
function configPath() {
|
|
5
|
+
return join(dirname(fileURLToPath(import.meta.url)), '../../config/minimal-implementation-gate.json');
|
|
6
|
+
}
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
function mergeDeep(base, override) {
|
|
11
|
+
const result = { ...base };
|
|
12
|
+
for (const [key, value] of Object.entries(override)) {
|
|
13
|
+
const current = result[key];
|
|
14
|
+
result[key] = isRecord(current) && isRecord(value) ? mergeDeep(current, value) : value;
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
function assertString(value, path) {
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: string is required`);
|
|
21
|
+
}
|
|
22
|
+
if (value === null) {
|
|
23
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: null is not allowed`);
|
|
24
|
+
}
|
|
25
|
+
if (typeof value !== 'string') {
|
|
26
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected string`);
|
|
27
|
+
}
|
|
28
|
+
if (value.length === 0) {
|
|
29
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: empty string is not allowed`);
|
|
30
|
+
}
|
|
31
|
+
if (value.trim().length === 0) {
|
|
32
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: whitespace-only string is not allowed`);
|
|
33
|
+
}
|
|
34
|
+
if (value.length > 500) {
|
|
35
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: string is too long`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function assertStringArray(value, path) {
|
|
39
|
+
if (!Array.isArray(value)) {
|
|
40
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected string[]`);
|
|
41
|
+
}
|
|
42
|
+
value.forEach((item, index) => {
|
|
43
|
+
assertString(item, `${path}.${String(index)}`);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function validateRule(value, path) {
|
|
47
|
+
if (!isRecord(value)) {
|
|
48
|
+
throw new Error(`Invalid minimal implementation policy at ${path}: expected object`);
|
|
49
|
+
}
|
|
50
|
+
assertString(value.id, `${path}.id`);
|
|
51
|
+
assertString(value.tag, `${path}.tag`);
|
|
52
|
+
if (!['info', 'warning', 'blocker'].includes(String(value.severity))) {
|
|
53
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.severity`);
|
|
54
|
+
}
|
|
55
|
+
if (!['low', 'medium', 'high'].includes(String(value.confidence))) {
|
|
56
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.confidence`);
|
|
57
|
+
}
|
|
58
|
+
assertStringArray(value.patterns, `${path}.patterns`);
|
|
59
|
+
assertString(value.replacementGuidance, `${path}.replacementGuidance`);
|
|
60
|
+
if (value.blockWhenRiskAtLeast !== undefined &&
|
|
61
|
+
(typeof value.blockWhenRiskAtLeast !== 'string' ||
|
|
62
|
+
!['low', 'medium', 'high', 'max'].includes(value.blockWhenRiskAtLeast))) {
|
|
63
|
+
throw new Error(`Invalid minimal implementation policy at ${path}.blockWhenRiskAtLeast`);
|
|
64
|
+
}
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
function validatePolicy(value) {
|
|
68
|
+
if (!isRecord(value)) {
|
|
69
|
+
throw new Error('Invalid minimal implementation policy: expected object');
|
|
70
|
+
}
|
|
71
|
+
if (value.version !== 1) {
|
|
72
|
+
throw new Error('Invalid minimal implementation policy: version must be 1');
|
|
73
|
+
}
|
|
74
|
+
const policy = value;
|
|
75
|
+
if (typeof policy.enabled !== 'boolean') {
|
|
76
|
+
throw new Error('Invalid minimal implementation policy at enabled: expected boolean');
|
|
77
|
+
}
|
|
78
|
+
if (!isRecord(policy.report)) {
|
|
79
|
+
throw new Error('Invalid minimal implementation policy at report: expected object');
|
|
80
|
+
}
|
|
81
|
+
for (const key of ['maxFindings', 'maxMarkdownLines', 'maxEvidenceChars']) {
|
|
82
|
+
if (!Number.isFinite(policy.report[key]) || policy.report[key] < 1) {
|
|
83
|
+
throw new Error(`Invalid minimal implementation policy at report.${key}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!isRecord(policy.tags)) {
|
|
87
|
+
throw new Error('Invalid minimal implementation policy at tags: expected object');
|
|
88
|
+
}
|
|
89
|
+
for (const [tag, definition] of Object.entries(policy.tags)) {
|
|
90
|
+
assertString(tag, `tags.${tag}`);
|
|
91
|
+
if (!isRecord(definition)) {
|
|
92
|
+
throw new Error(`Invalid minimal implementation policy at tags.${tag}: expected object`);
|
|
93
|
+
}
|
|
94
|
+
assertString(definition.description, `tags.${tag}.description`);
|
|
95
|
+
}
|
|
96
|
+
if (!Array.isArray(policy.rules) || policy.rules.length === 0) {
|
|
97
|
+
throw new Error('Invalid minimal implementation policy at rules: expected non-empty array');
|
|
98
|
+
}
|
|
99
|
+
policy.rules.forEach((rule, index) => validateRule(rule, `rules.${String(index)}`));
|
|
100
|
+
assertStringArray(policy.safetyExclusions, 'safetyExclusions');
|
|
101
|
+
assertStringArray(policy.excludedPathPatterns, 'excludedPathPatterns');
|
|
102
|
+
if (!isRecord(policy.debtEvidence)) {
|
|
103
|
+
throw new Error('Invalid minimal implementation policy at debtEvidence: expected object');
|
|
104
|
+
}
|
|
105
|
+
assertStringArray(policy.debtEvidence.requiredFields, 'debtEvidence.requiredFields');
|
|
106
|
+
return policy;
|
|
107
|
+
}
|
|
108
|
+
async function readJson(path) {
|
|
109
|
+
return JSON.parse(await readFile(path, 'utf-8'));
|
|
110
|
+
}
|
|
111
|
+
export async function loadMinimalImplementationPolicy(projectPath, options = {}) {
|
|
112
|
+
const defaultPolicyPath = options.defaultPolicyPath ?? configPath();
|
|
113
|
+
const defaultPolicy = validatePolicy(await readJson(defaultPolicyPath));
|
|
114
|
+
const evidence = [
|
|
115
|
+
{ source: 'policy', key: defaultPolicyPath },
|
|
116
|
+
];
|
|
117
|
+
let merged = defaultPolicy;
|
|
118
|
+
if (projectPath !== undefined) {
|
|
119
|
+
const planuConfigPath = join(projectPath, 'planu.json');
|
|
120
|
+
try {
|
|
121
|
+
const config = await readJson(planuConfigPath);
|
|
122
|
+
if (isRecord(config) && isRecord(config.minimalImplementationGate)) {
|
|
123
|
+
merged = validatePolicy(mergeDeep(defaultPolicy, config.minimalImplementationGate));
|
|
124
|
+
evidence.push({ source: 'project-config', key: 'planu.json.minimalImplementationGate' });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
// Missing or malformed project config does not block default policy loading.
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { policy: merged, evidence };
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=policy-loader.js.map
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Spec } from '../../types/index.js';
|
|
2
2
|
import type { ValidationReportV1 } from '../../types/handoff-artifacts.js';
|
|
3
|
+
import type { MinimalImplementationReport } from '../../types/minimal-implementation-gate.js';
|
|
3
4
|
export declare function writeImplementationReviewReport(input: {
|
|
4
5
|
projectId: string;
|
|
5
6
|
specId: string;
|
|
@@ -8,6 +9,7 @@ export declare function writeImplementationReviewReport(input: {
|
|
|
8
9
|
score: number | null;
|
|
9
10
|
lintPassed?: boolean;
|
|
10
11
|
conventionRegression?: boolean;
|
|
12
|
+
minimalityReport?: MinimalImplementationReport;
|
|
11
13
|
}): Promise<{
|
|
12
14
|
written: boolean;
|
|
13
15
|
path?: string;
|
|
@@ -7,6 +7,7 @@ export async function writeImplementationReviewReport(input) {
|
|
|
7
7
|
lintPassed: input.lintPassed ?? true,
|
|
8
8
|
conventionRegression: input.conventionRegression ?? false,
|
|
9
9
|
specCompliance,
|
|
10
|
+
minimalityReport: input.minimalityReport,
|
|
10
11
|
});
|
|
11
12
|
const passed = gates.every((gate) => gate.passed);
|
|
12
13
|
const reviewer = {
|
|
@@ -22,6 +23,7 @@ export async function writeImplementationReviewReport(input) {
|
|
|
22
23
|
gates,
|
|
23
24
|
reviewer,
|
|
24
25
|
specCompliance: reportCompliance,
|
|
26
|
+
minimalityReport: input.minimalityReport,
|
|
25
27
|
score: input.score ?? undefined,
|
|
26
28
|
completedAt: new Date().toISOString(),
|
|
27
29
|
};
|
|
@@ -73,8 +75,25 @@ function buildGates(args) {
|
|
|
73
75
|
reason: args.conventionRegression ? 'Convention baseline regression detected.' : undefined,
|
|
74
76
|
},
|
|
75
77
|
buildSpecComplianceGate(args.specCompliance),
|
|
78
|
+
buildMinimalityGate(args.minimalityReport),
|
|
76
79
|
];
|
|
77
80
|
}
|
|
81
|
+
function buildMinimalityGate(report) {
|
|
82
|
+
if (report?.enabled !== true) {
|
|
83
|
+
return {
|
|
84
|
+
name: 'minimal-implementation',
|
|
85
|
+
passed: true,
|
|
86
|
+
reason: 'Minimal implementation report unavailable or disabled.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
name: 'minimal-implementation',
|
|
91
|
+
passed: !report.blocked,
|
|
92
|
+
reason: report.blocked
|
|
93
|
+
? `Blocking minimality findings: ${String(report.findings.filter((finding) => finding.blocksDone).length)}.`
|
|
94
|
+
: undefined,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
78
97
|
function buildSpecComplianceGate(result) {
|
|
79
98
|
if (result.perScenario.length === 0) {
|
|
80
99
|
return {
|
|
@@ -24,6 +24,7 @@ import { generateResilienceChallengeScenarios } from './challenge-spec/resilienc
|
|
|
24
24
|
import { detectContradictions as detectDecisionContradictions, searchPriorDecisions, } from '../engine/prior-decisions/index.js';
|
|
25
25
|
import { getDecisions } from '../storage/decision-store.js';
|
|
26
26
|
import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-budget/index.js';
|
|
27
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
27
28
|
const ALL_FOCUS_AREAS = [
|
|
28
29
|
'failures',
|
|
29
30
|
'concurrency',
|
|
@@ -127,6 +128,30 @@ export async function handleChallengeSpec(args, server) {
|
|
|
127
128
|
const focusAreas = focus && focus.length > 0 ? focus : ALL_FOCUS_AREAS;
|
|
128
129
|
// 5. Generate failure scenarios
|
|
129
130
|
const failureScenarios = [];
|
|
131
|
+
try {
|
|
132
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
|
|
133
|
+
const minimality = analyzeMinimalImplementation({
|
|
134
|
+
policy,
|
|
135
|
+
specId,
|
|
136
|
+
specRisk: spec.risk,
|
|
137
|
+
specText: specContent,
|
|
138
|
+
evidence,
|
|
139
|
+
});
|
|
140
|
+
for (const finding of minimality.findings.slice(0, 5)) {
|
|
141
|
+
failureScenarios.push({
|
|
142
|
+
scenario: `Minimal implementation concern: ${finding.tag}`,
|
|
143
|
+
probability: finding.confidence === 'high' ? 'high' : 'medium',
|
|
144
|
+
impact: finding.blocksDone ? 'high' : 'low',
|
|
145
|
+
currentHandling: `${finding.target}${finding.line ? `:${String(finding.line)}` : ''} — ${finding.evidence}`,
|
|
146
|
+
requiredHandling: finding.replacementGuidance,
|
|
147
|
+
dataConsistency: 'Scope control risk',
|
|
148
|
+
userExperience: 'Avoidable implementation complexity can increase token use and maintenance cost',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Minimality challenge is best-effort and must not hide resilience findings.
|
|
154
|
+
}
|
|
130
155
|
if (focusAreas.includes('failures')) {
|
|
131
156
|
failureScenarios.push(...generateFailureScenarios(spec, specContent, knowledge));
|
|
132
157
|
}
|
|
@@ -6,6 +6,7 @@ import { detectParadigms } from '../engine/paradigm-detector.js';
|
|
|
6
6
|
import { analyzeContextPreflight, buildTokenWasteReport, formatTokenWasteReport, loadTokenWastePolicy, recommendRelevantTools, toolsFromPolicyGroups, } from '../engine/token-optimizer/index.js';
|
|
7
7
|
import { appendTransitionEvent } from '../storage/transition-log.js';
|
|
8
8
|
import { formatProjectGraphContext, queryProjectGraphSlice, } from '../engine/project-graph/index.js';
|
|
9
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
9
10
|
// ── Formatting helpers ───────────────────────────────────────────────────────
|
|
10
11
|
function formatHandoff(pkg) {
|
|
11
12
|
const lines = [];
|
|
@@ -196,6 +197,22 @@ async function buildGraphContextSection(args) {
|
|
|
196
197
|
return null;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
async function buildMinimalImplementationReport(pkg, knowledge, spec, handoffText) {
|
|
201
|
+
try {
|
|
202
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(knowledge.projectPath);
|
|
203
|
+
return analyzeMinimalImplementation({
|
|
204
|
+
policy,
|
|
205
|
+
specId: spec.id,
|
|
206
|
+
specRisk: spec.risk,
|
|
207
|
+
handoffText,
|
|
208
|
+
files: [...pkg.filesToModify, ...pkg.filesToCreate].map((path) => ({ path, content: path })),
|
|
209
|
+
evidence,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
199
216
|
// ── Handler ──────────────────────────────────────────────────────────────────
|
|
200
217
|
export async function handlePackageHandoff(args) {
|
|
201
218
|
const { projectId, specId } = args;
|
|
@@ -256,9 +273,13 @@ export async function handlePackageHandoff(args) {
|
|
|
256
273
|
projectPath: knowledge.projectPath,
|
|
257
274
|
specId,
|
|
258
275
|
});
|
|
259
|
-
const
|
|
276
|
+
const baseHandoffText = graphContext !== null
|
|
260
277
|
? `${formatHandoff(pkgWithScore)}\n${graphContext.text}`
|
|
261
278
|
: formatHandoff(pkgWithScore);
|
|
279
|
+
const minimalityReport = await buildMinimalImplementationReport(pkgWithScore, knowledge, spec, baseHandoffText);
|
|
280
|
+
const handoffText = minimalityReport !== null && minimalityReport.markdown.length > 0
|
|
281
|
+
? `${baseHandoffText}\n${minimalityReport.markdown}`
|
|
282
|
+
: baseHandoffText;
|
|
262
283
|
const formatted = tokenWasteReport !== null
|
|
263
284
|
? `${handoffText}\n${formatTokenWasteReport(tokenWasteReport)}`
|
|
264
285
|
: handoffText;
|
|
@@ -282,6 +303,7 @@ export async function handlePackageHandoff(args) {
|
|
|
282
303
|
blockers: pkgWithScore.blockers,
|
|
283
304
|
handoffPath: pkgWithScore.handoffPath,
|
|
284
305
|
contextHash: pkgWithScore.contextHash,
|
|
306
|
+
...(minimalityReport !== null ? { minimality: minimalityReport } : {}),
|
|
285
307
|
...(tokenWasteReport !== null ? { tokenWaste: tokenWasteReport } : {}),
|
|
286
308
|
...(graphContext !== null ? { graphContext: graphContext.structured } : {}),
|
|
287
309
|
},
|
|
@@ -299,6 +299,15 @@ export async function checkValidationReportGate(specId, projectId, force) {
|
|
|
299
299
|
fixHint: 'Fix failing gates and re-run validate before marking done.',
|
|
300
300
|
});
|
|
301
301
|
}
|
|
302
|
+
if (result.payload.minimalityReport?.blocked) {
|
|
303
|
+
return validationReportGateError({
|
|
304
|
+
specId,
|
|
305
|
+
error: 'minimality_findings_block_done',
|
|
306
|
+
message: 'Validation report contains blocking minimal implementation findings. Remove avoidable complexity or provide an audited forceStatusReason.',
|
|
307
|
+
gates: result.payload.gates,
|
|
308
|
+
fixHint: 'Fix blocking minimality findings and re-run validate before marking done. Use forceStatus only with an audited reason for accepted complexity.',
|
|
309
|
+
});
|
|
310
|
+
}
|
|
302
311
|
if (result.payload.reviewer.verdict !== 'approved' ||
|
|
303
312
|
result.payload.reviewer.agent.trim().length === 0) {
|
|
304
313
|
return validationReportGateError({
|
package/dist/tools/validate.js
CHANGED
|
@@ -18,6 +18,7 @@ import { compareWithBaseline } from '../storage/convention-baseline.js';
|
|
|
18
18
|
import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
|
|
19
19
|
import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
|
|
20
20
|
import { queryProjectGraphSlice } from '../engine/project-graph/index.js';
|
|
21
|
+
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
21
22
|
// Re-export for external use (SPEC-018)
|
|
22
23
|
export { validateContractCompliance };
|
|
23
24
|
function graphCoverageGaps(slice) {
|
|
@@ -47,6 +48,25 @@ function formatGraphCoverageText(report) {
|
|
|
47
48
|
? `\nGRAPH ${String(report.gaps.length)} graph-backed coverage gap(s)`
|
|
48
49
|
: '';
|
|
49
50
|
}
|
|
51
|
+
async function buildMinimalityReport(args) {
|
|
52
|
+
try {
|
|
53
|
+
const { policy, evidence } = await loadMinimalImplementationPolicy(args.projectPath);
|
|
54
|
+
return analyzeMinimalImplementation({
|
|
55
|
+
policy,
|
|
56
|
+
specId: args.specId,
|
|
57
|
+
specRisk: args.risk,
|
|
58
|
+
specText: [args.title ?? '', ...args.missing, ...args.extra].join('\n'),
|
|
59
|
+
files: args.qualityIssues.map((issue) => ({
|
|
60
|
+
path: issue.file,
|
|
61
|
+
content: [issue.rule, issue.message, issue.suggestion ?? ''].join('\n'),
|
|
62
|
+
})),
|
|
63
|
+
evidence,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
50
70
|
async function validateStrictLayoutOrError(args) {
|
|
51
71
|
try {
|
|
52
72
|
const { validateStrictPlanuLayout } = await import('../engine/spec-migrator/index.js');
|
|
@@ -176,6 +196,15 @@ export async function handleValidate(args, server) {
|
|
|
176
196
|
// 7. Lint check (best-effort — non-blocking)
|
|
177
197
|
const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
|
|
178
198
|
const assuranceGates = runAssuranceGates(projectPath);
|
|
199
|
+
const minimalityReport = await buildMinimalityReport({
|
|
200
|
+
projectPath,
|
|
201
|
+
specId,
|
|
202
|
+
risk: spec.risk,
|
|
203
|
+
title: spec.title,
|
|
204
|
+
missing: result.missing,
|
|
205
|
+
extra: result.extra,
|
|
206
|
+
qualityIssues: result.qualityIssues,
|
|
207
|
+
});
|
|
179
208
|
// SPEC-1050: Generate a mandatory implementation-review artifact.
|
|
180
209
|
const validationReport = await writeImplementationReviewReport({
|
|
181
210
|
projectId,
|
|
@@ -185,6 +214,7 @@ export async function handleValidate(args, server) {
|
|
|
185
214
|
score: result.score,
|
|
186
215
|
lintPassed: lintCheck.passed,
|
|
187
216
|
conventionRegression: regressionDetected,
|
|
217
|
+
minimalityReport: minimalityReport ?? undefined,
|
|
188
218
|
});
|
|
189
219
|
// 8. Build output
|
|
190
220
|
const output = {
|
|
@@ -262,6 +292,7 @@ export async function handleValidate(args, server) {
|
|
|
262
292
|
lintCheck,
|
|
263
293
|
assuranceGates,
|
|
264
294
|
validationReport,
|
|
295
|
+
minimalityReport,
|
|
265
296
|
graphCoverage,
|
|
266
297
|
};
|
|
267
298
|
// SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
|
|
@@ -312,6 +343,9 @@ export async function handleValidate(args, server) {
|
|
|
312
343
|
if (!assuranceGates.newCode.passed) {
|
|
313
344
|
suggestions.push(`Assurance gate failed: ${String(assuranceGates.newCode.findings.length)} new-code domain inference issue(s) found.`);
|
|
314
345
|
}
|
|
346
|
+
if (minimalityReport?.blocked) {
|
|
347
|
+
suggestions.push(`${minimalityReport.findings.filter((finding) => finding.blocksDone).length} blocking minimality finding(s) detected. Remove avoidable complexity or record accepted debt before marking done.`);
|
|
348
|
+
}
|
|
315
349
|
const outputWithSuggestions = compactObj({
|
|
316
350
|
...output,
|
|
317
351
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
@@ -72,6 +72,7 @@ export interface ValidationReportV1 {
|
|
|
72
72
|
evidence: string[];
|
|
73
73
|
}[];
|
|
74
74
|
};
|
|
75
|
+
minimalityReport?: MinimalImplementationReport;
|
|
75
76
|
score?: number;
|
|
76
77
|
completedAt: string;
|
|
77
78
|
}
|
|
@@ -83,4 +84,5 @@ export interface ArtifactPayloadMap {
|
|
|
83
84
|
'validation-report': ValidationReportV1;
|
|
84
85
|
}
|
|
85
86
|
export type ArtifactPayload<K extends ArtifactKind> = ArtifactPayloadMap[K];
|
|
87
|
+
import type { MinimalImplementationReport } from './minimal-implementation-gate.js';
|
|
86
88
|
//# sourceMappingURL=handoff-artifacts.d.ts.map
|
package/dist/types/index.d.ts
CHANGED
|
@@ -80,6 +80,7 @@ export * from './workers.js';
|
|
|
80
80
|
export * from './orchestration-runtime.js';
|
|
81
81
|
export * from './token-optimization.js';
|
|
82
82
|
export * from './token-waste-autopilot.js';
|
|
83
|
+
export * from './minimal-implementation-gate.js';
|
|
83
84
|
export * from './llm-providers.js';
|
|
84
85
|
export * from './plugins.js';
|
|
85
86
|
export * from './github.js';
|
package/dist/types/index.js
CHANGED
|
@@ -81,6 +81,7 @@ export * from './workers.js';
|
|
|
81
81
|
export * from './orchestration-runtime.js';
|
|
82
82
|
export * from './token-optimization.js';
|
|
83
83
|
export * from './token-waste-autopilot.js';
|
|
84
|
+
export * from './minimal-implementation-gate.js';
|
|
84
85
|
export * from './llm-providers.js';
|
|
85
86
|
export * from './plugins.js';
|
|
86
87
|
export * from './github.js';
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
export type MinimalImplementationTag = string;
|
|
2
|
+
export type MinimalImplementationSeverity = 'info' | 'warning' | 'blocker';
|
|
3
|
+
export type MinimalImplementationConfidence = 'low' | 'medium' | 'high';
|
|
4
|
+
export type MinimalImplementationSource = 'policy' | 'project-config' | 'runtime' | 'spec';
|
|
5
|
+
export interface MinimalImplementationPolicyLoadOptions {
|
|
6
|
+
defaultPolicyPath?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface MinimalImplementationEvidence {
|
|
9
|
+
source: MinimalImplementationSource;
|
|
10
|
+
key: string;
|
|
11
|
+
value?: string | number | boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface MinimalImplementationRule {
|
|
14
|
+
id: string;
|
|
15
|
+
tag: MinimalImplementationTag;
|
|
16
|
+
severity: MinimalImplementationSeverity;
|
|
17
|
+
confidence: MinimalImplementationConfidence;
|
|
18
|
+
patterns: string[];
|
|
19
|
+
replacementGuidance: string;
|
|
20
|
+
blockWhenRiskAtLeast?: 'low' | 'medium' | 'high' | 'max';
|
|
21
|
+
}
|
|
22
|
+
export interface MinimalImplementationPolicy {
|
|
23
|
+
version: 1;
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
report: {
|
|
26
|
+
maxFindings: number;
|
|
27
|
+
maxMarkdownLines: number;
|
|
28
|
+
maxEvidenceChars: number;
|
|
29
|
+
};
|
|
30
|
+
tags: Record<string, {
|
|
31
|
+
description: string;
|
|
32
|
+
}>;
|
|
33
|
+
rules: MinimalImplementationRule[];
|
|
34
|
+
safetyExclusions: string[];
|
|
35
|
+
excludedPathPatterns: string[];
|
|
36
|
+
debtEvidence: {
|
|
37
|
+
requiredFields: string[];
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
export interface LoadedMinimalImplementationPolicy {
|
|
41
|
+
policy: MinimalImplementationPolicy;
|
|
42
|
+
evidence: MinimalImplementationEvidence[];
|
|
43
|
+
}
|
|
44
|
+
export interface MinimalImplementationFileSignal {
|
|
45
|
+
path: string;
|
|
46
|
+
content?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface MinimalImplementationInput {
|
|
49
|
+
policy: MinimalImplementationPolicy;
|
|
50
|
+
specId?: string;
|
|
51
|
+
specRisk?: string;
|
|
52
|
+
specText?: string;
|
|
53
|
+
handoffText?: string;
|
|
54
|
+
files?: MinimalImplementationFileSignal[];
|
|
55
|
+
packageManifestText?: string;
|
|
56
|
+
acceptedDebt?: MinimalImplementationDebtEvidence[];
|
|
57
|
+
evidence?: MinimalImplementationEvidence[];
|
|
58
|
+
}
|
|
59
|
+
export interface MinimalImplementationFinding {
|
|
60
|
+
ruleId: string;
|
|
61
|
+
tag: MinimalImplementationTag;
|
|
62
|
+
severity: MinimalImplementationSeverity;
|
|
63
|
+
confidence: MinimalImplementationConfidence;
|
|
64
|
+
target: string;
|
|
65
|
+
line?: number;
|
|
66
|
+
evidence: string;
|
|
67
|
+
replacementGuidance: string;
|
|
68
|
+
blocksDone: boolean;
|
|
69
|
+
evidenceSource: MinimalImplementationEvidence[];
|
|
70
|
+
}
|
|
71
|
+
export interface MinimalImplementationSafetyException {
|
|
72
|
+
target: string;
|
|
73
|
+
reason: string;
|
|
74
|
+
evidence: string;
|
|
75
|
+
}
|
|
76
|
+
export interface MinimalImplementationDebtEvidence {
|
|
77
|
+
specId: string;
|
|
78
|
+
files: string[];
|
|
79
|
+
ceiling: string;
|
|
80
|
+
upgradeTrigger: string;
|
|
81
|
+
reviewRationale: string;
|
|
82
|
+
}
|
|
83
|
+
export interface MinimalImplementationReport {
|
|
84
|
+
enabled: boolean;
|
|
85
|
+
blocked: boolean;
|
|
86
|
+
findings: MinimalImplementationFinding[];
|
|
87
|
+
safetyExceptions: MinimalImplementationSafetyException[];
|
|
88
|
+
debtEvidence: MinimalImplementationDebtEvidence[];
|
|
89
|
+
evidence: MinimalImplementationEvidence[];
|
|
90
|
+
markdown: string;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=minimal-implementation-gate.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.7.
|
|
3
|
+
"version": "4.7.2",
|
|
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.7.
|
|
38
|
-
"@planu/core-darwin-x64": "4.7.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.7.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.7.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.7.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.7.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.7.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.7.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.7.2",
|
|
38
|
+
"@planu/core-darwin-x64": "4.7.2",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.7.2",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.7.2",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.7.2",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.7.2",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.7.2",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.7.2"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
|
@@ -129,7 +129,7 @@
|
|
|
129
129
|
],
|
|
130
130
|
"license": "SEE LICENSE IN LICENSE",
|
|
131
131
|
"dependencies": {
|
|
132
|
-
"@anthropic-ai/sdk": "^0.104.
|
|
132
|
+
"@anthropic-ai/sdk": "^0.104.2",
|
|
133
133
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
134
134
|
"glob": "^13.0.6",
|
|
135
135
|
"yaml": "^2.9.0",
|
|
@@ -171,7 +171,7 @@
|
|
|
171
171
|
"@commitlint/cli": "^21.0.2",
|
|
172
172
|
"@commitlint/config-conventional": "^21.0.2",
|
|
173
173
|
"@eslint/js": "^10.0.1",
|
|
174
|
-
"@napi-rs/cli": "^3.7.
|
|
174
|
+
"@napi-rs/cli": "^3.7.2",
|
|
175
175
|
"@secretlint/secretlint-rule-no-homedir": "^13.0.2",
|
|
176
176
|
"@secretlint/secretlint-rule-preset-recommend": "^13.0.2",
|
|
177
177
|
"@semantic-release/changelog": "^6.0.3",
|
|
@@ -182,19 +182,19 @@
|
|
|
182
182
|
"@semantic-release/release-notes-generator": "^14.1.1",
|
|
183
183
|
"@stryker-mutator/core": "^9.6.1",
|
|
184
184
|
"@stryker-mutator/vitest-runner": "^9.6.1",
|
|
185
|
-
"@supabase/supabase-js": "^2.108.
|
|
185
|
+
"@supabase/supabase-js": "^2.108.2",
|
|
186
186
|
"@types/node": "^25.9.3",
|
|
187
187
|
"@vitejs/plugin-vue": "^6.0.7",
|
|
188
|
-
"@vitest/coverage-v8": "^4.1.
|
|
188
|
+
"@vitest/coverage-v8": "^4.1.9",
|
|
189
189
|
"@vue/test-utils": "^2.4.11",
|
|
190
190
|
"eslint": "^10.5.0",
|
|
191
191
|
"eslint-config-prettier": "^10.1.8",
|
|
192
192
|
"eslint-import-resolver-typescript": "^4.4.5",
|
|
193
193
|
"eslint-plugin-import": "^2.32.0",
|
|
194
|
-
"happy-dom": "^20.10.
|
|
194
|
+
"happy-dom": "^20.10.5",
|
|
195
195
|
"husky": "^9.1.7",
|
|
196
196
|
"javascript-obfuscator": "^5.4.3",
|
|
197
|
-
"knip": "^6.
|
|
197
|
+
"knip": "^6.17.1",
|
|
198
198
|
"lint-staged": "^17.0.7",
|
|
199
199
|
"madge": "^8.0.0",
|
|
200
200
|
"prettier": "^3.8.4",
|
|
@@ -203,9 +203,9 @@
|
|
|
203
203
|
"tsc-alias": "^1.8.17",
|
|
204
204
|
"type-coverage": "^2.29.7",
|
|
205
205
|
"typescript": "^6.0.3",
|
|
206
|
-
"typescript-eslint": "^8.61.
|
|
206
|
+
"typescript-eslint": "^8.61.1",
|
|
207
207
|
"vite": "^8.0.16",
|
|
208
|
-
"vitest": "^4.1.
|
|
208
|
+
"vitest": "^4.1.9",
|
|
209
209
|
"vue": "^3.5.38"
|
|
210
210
|
}
|
|
211
211
|
}
|
package/planu-native.json
CHANGED
|
@@ -1,26 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dev.planu.native",
|
|
3
3
|
"displayName": "Planu Native Lightweight Surface",
|
|
4
|
-
"version": "4.7.
|
|
4
|
+
"version": "4.7.2",
|
|
5
5
|
"packageName": "@planu/cli",
|
|
6
6
|
"modes": {
|
|
7
7
|
"lightweight": {
|
|
8
8
|
"requiresMcp": false,
|
|
9
9
|
"requiresDaemon": false,
|
|
10
|
-
"hosts": [
|
|
11
|
-
"codex",
|
|
12
|
-
"claude-code"
|
|
13
|
-
],
|
|
10
|
+
"hosts": ["codex", "claude-code"],
|
|
14
11
|
"commands": [
|
|
15
12
|
{
|
|
16
13
|
"id": "planu.status",
|
|
17
14
|
"title": "Project status",
|
|
18
15
|
"description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
|
|
19
16
|
"invocation": "planu status",
|
|
20
|
-
"hosts": [
|
|
21
|
-
"codex",
|
|
22
|
-
"claude-code"
|
|
23
|
-
],
|
|
17
|
+
"hosts": ["codex", "claude-code"],
|
|
24
18
|
"requiresMcp": false,
|
|
25
19
|
"requiresDaemon": false,
|
|
26
20
|
"mapsTo": "handlePlanStatus"
|
|
@@ -30,10 +24,7 @@
|
|
|
30
24
|
"title": "Create spec",
|
|
31
25
|
"description": "Create a new spec through the CLI-backed SDD contract.",
|
|
32
26
|
"invocation": "planu spec create \"<title>\"",
|
|
33
|
-
"hosts": [
|
|
34
|
-
"codex",
|
|
35
|
-
"claude-code"
|
|
36
|
-
],
|
|
27
|
+
"hosts": ["codex", "claude-code"],
|
|
37
28
|
"requiresMcp": false,
|
|
38
29
|
"requiresDaemon": false,
|
|
39
30
|
"mapsTo": "handleCreateSpec"
|
|
@@ -43,10 +34,7 @@
|
|
|
43
34
|
"title": "List specs",
|
|
44
35
|
"description": "List specs in the current project with optional status/type filters.",
|
|
45
36
|
"invocation": "planu spec list",
|
|
46
|
-
"hosts": [
|
|
47
|
-
"codex",
|
|
48
|
-
"claude-code"
|
|
49
|
-
],
|
|
37
|
+
"hosts": ["codex", "claude-code"],
|
|
50
38
|
"requiresMcp": false,
|
|
51
39
|
"requiresDaemon": false,
|
|
52
40
|
"mapsTo": "handleListSpecs"
|
|
@@ -56,10 +44,7 @@
|
|
|
56
44
|
"title": "Validate spec",
|
|
57
45
|
"description": "Validate a spec against the current codebase from the native CLI surface.",
|
|
58
46
|
"invocation": "planu spec validate SPEC-001",
|
|
59
|
-
"hosts": [
|
|
60
|
-
"codex",
|
|
61
|
-
"claude-code"
|
|
62
|
-
],
|
|
47
|
+
"hosts": ["codex", "claude-code"],
|
|
63
48
|
"requiresMcp": false,
|
|
64
49
|
"requiresDaemon": false,
|
|
65
50
|
"mapsTo": "handleValidate"
|
|
@@ -69,10 +54,7 @@
|
|
|
69
54
|
"title": "Audit technical debt",
|
|
70
55
|
"description": "Run the read-only project audit path for lightweight debt checks.",
|
|
71
56
|
"invocation": "planu audit debt",
|
|
72
|
-
"hosts": [
|
|
73
|
-
"codex",
|
|
74
|
-
"claude-code"
|
|
75
|
-
],
|
|
57
|
+
"hosts": ["codex", "claude-code"],
|
|
76
58
|
"requiresMcp": false,
|
|
77
59
|
"requiresDaemon": false,
|
|
78
60
|
"mapsTo": "handleAudit"
|
|
@@ -82,10 +64,7 @@
|
|
|
82
64
|
"title": "Check release readiness",
|
|
83
65
|
"description": "Check local branch cleanliness and main/develop/release sync readiness.",
|
|
84
66
|
"invocation": "planu release check",
|
|
85
|
-
"hosts": [
|
|
86
|
-
"codex",
|
|
87
|
-
"claude-code"
|
|
88
|
-
],
|
|
67
|
+
"hosts": ["codex", "claude-code"],
|
|
89
68
|
"requiresMcp": false,
|
|
90
69
|
"requiresDaemon": false,
|
|
91
70
|
"mapsTo": "releaseCommand"
|
package/planu-plugin.json
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
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.7.
|
|
5
|
+
"version": "4.7.2",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
|
-
"command": [
|
|
8
|
-
"npx",
|
|
9
|
-
"@planu/cli@latest"
|
|
10
|
-
],
|
|
7
|
+
"command": ["npx", "@planu/cli@latest"],
|
|
11
8
|
"packageName": "@planu/cli",
|
|
12
9
|
"capabilities": {
|
|
13
10
|
"tools": [
|
|
@@ -26,42 +23,17 @@
|
|
|
26
23
|
"create_skill",
|
|
27
24
|
"skill_search"
|
|
28
25
|
],
|
|
29
|
-
"resources": [
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"planu://project/status",
|
|
33
|
-
"planu://roadmap"
|
|
34
|
-
],
|
|
35
|
-
"prompts": [
|
|
36
|
-
"create-spec-from-idea",
|
|
37
|
-
"review-spec-readiness",
|
|
38
|
-
"generate-implementation-plan"
|
|
39
|
-
],
|
|
40
|
-
"subagents": [
|
|
41
|
-
"sdd-orchestrator",
|
|
42
|
-
"spec-challenger",
|
|
43
|
-
"test-generator"
|
|
44
|
-
]
|
|
26
|
+
"resources": ["planu://specs/list", "planu://specs/{id}", "planu://project/status", "planu://roadmap"],
|
|
27
|
+
"prompts": ["create-spec-from-idea", "review-spec-readiness", "generate-implementation-plan"],
|
|
28
|
+
"subagents": ["sdd-orchestrator", "spec-challenger", "test-generator"]
|
|
45
29
|
},
|
|
46
30
|
"compatibility": {
|
|
47
31
|
"minimumHostVersion": "1.0.0",
|
|
48
|
-
"requiredFeatures": [
|
|
49
|
-
"mcp-tools",
|
|
50
|
-
"file-editing"
|
|
51
|
-
]
|
|
32
|
+
"requiredFeatures": ["mcp-tools", "file-editing"]
|
|
52
33
|
},
|
|
53
34
|
"repository": "https://github.com/planu-dev/planu",
|
|
54
35
|
"author": "Planu",
|
|
55
36
|
"license": "MIT",
|
|
56
37
|
"homepage": "https://planu.dev",
|
|
57
|
-
"keywords": [
|
|
58
|
-
"sdd",
|
|
59
|
-
"spec-driven-development",
|
|
60
|
-
"mcp",
|
|
61
|
-
"specs",
|
|
62
|
-
"planning",
|
|
63
|
-
"ai",
|
|
64
|
-
"bdd",
|
|
65
|
-
"tdd"
|
|
66
|
-
]
|
|
38
|
+
"keywords": ["sdd", "spec-driven-development", "mcp", "specs", "planning", "ai", "bdd", "tdd"]
|
|
67
39
|
}
|