@planu/cli 4.3.17 → 4.3.18
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/dist/engine/ai-assurance/index.d.ts +4 -0
- package/dist/engine/ai-assurance/index.js +3 -0
- package/dist/engine/ai-assurance/new-code-gate.d.ts +3 -0
- package/dist/engine/ai-assurance/new-code-gate.js +39 -0
- package/dist/engine/ai-assurance/spec-assurance.d.ts +3 -0
- package/dist/engine/ai-assurance/spec-assurance.js +138 -0
- package/dist/engine/quality-gates/gate-catalog.js +24 -0
- package/dist/tools/validate.js +43 -0
- package/dist/types/ai-assurance.d.ts +31 -0
- package/dist/types/ai-assurance.js +2 -0
- package/package.json +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const SPEC_LOGIC_PATH_RE = /(?:src\/tools\/create-spec\/|src\/engine\/.*(?:assurance|validator|quality|elicitation)\/|src\/tools\/validate\.ts$)/;
|
|
2
|
+
const INCIDENTAL_DOMAIN_INFERENCE_RE = /\.(?:includes|match|test)\(\s*['"`](?:billing|payment|payments|auth|database|supabase|ui|frontend)['"`]\s*\)/i;
|
|
3
|
+
const STACK_ONLY_TRIGGER_RE = /\b(?:framework|stack|language|projectDna|dna|detectedStack)\b[^;\n]*(?:billing|payment|payments|auth|database|supabase|ui|frontend)/i;
|
|
4
|
+
export function evaluateNewCodeGate(files) {
|
|
5
|
+
const findings = files.flatMap((file) => scanFile(file));
|
|
6
|
+
return {
|
|
7
|
+
passed: findings.length === 0,
|
|
8
|
+
findings,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function scanFile(file) {
|
|
12
|
+
if (!SPEC_LOGIC_PATH_RE.test(file.path)) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
const findings = [];
|
|
16
|
+
const lines = file.content.split(/\r?\n/);
|
|
17
|
+
lines.forEach((line, index) => {
|
|
18
|
+
if (STACK_ONLY_TRIGGER_RE.test(line)) {
|
|
19
|
+
findings.push({
|
|
20
|
+
rule: 'new-code-stack-only-domain-trigger',
|
|
21
|
+
file: file.path,
|
|
22
|
+
line: index + 1,
|
|
23
|
+
message: 'New code appears to trigger product-domain requirements from detected stack metadata alone.',
|
|
24
|
+
evidence: line.trim(),
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (INCIDENTAL_DOMAIN_INFERENCE_RE.test(line)) {
|
|
28
|
+
findings.push({
|
|
29
|
+
rule: 'new-code-incidental-domain-inference',
|
|
30
|
+
file: file.path,
|
|
31
|
+
line: index + 1,
|
|
32
|
+
message: 'New code appears to infer product-domain requirements from incidental strings such as folder names.',
|
|
33
|
+
evidence: line.trim(),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return findings;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=new-code-gate.js.map
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
const GUARDED_DOMAINS = [
|
|
2
|
+
{
|
|
3
|
+
name: 'billing',
|
|
4
|
+
requestPattern: /\b(billing|payment|payments|paid|checkout|stripe|subscription|pricing)\b/i,
|
|
5
|
+
specPattern: /\b(billing|payment|payments|checkout|stripe|subscription|pricing)\b/i,
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
name: 'authentication',
|
|
9
|
+
requestPattern: /\b(auth|authentication|authorization|login|jwt|oauth|session)\b/i,
|
|
10
|
+
specPattern: /\b(auth|authentication|authorization|login|jwt|oauth|session|401|403)\b/i,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'database',
|
|
14
|
+
requestPattern: /\b(database|schema|rls|rpc|persistence|persist|query|migration)\b/i,
|
|
15
|
+
specPattern: /\b(database|schema|rls|rpc|persistence|persist|query|migration)\b/i,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'ui',
|
|
19
|
+
requestPattern: /\b(ui|frontend|component|screen|page|form|layout|style|styling)\b/i,
|
|
20
|
+
specPattern: /\b(ui|frontend|component|screen|page|form|layout|style|styling)\b/i,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
const PATH_LIKE_DOMAIN_RE = /\b(?:stores|store|modules|features|apps|packages|src)\/([a-z][\w-]*)/gi;
|
|
24
|
+
const INVENTED_STATUS_RE = /\b(?:(?:endpoints?|api|routes?|response|status)\b[^.\n]*(?:401|403|404|500)|(?:401|403|404|500)\b[^.\n]*(?:endpoints?|api|routes?|response|status))\b/i;
|
|
25
|
+
const GENERIC_COVERAGE_RE = /\b(?:coverage|test coverage)\b/i;
|
|
26
|
+
export function evaluateSpecAssurance(input) {
|
|
27
|
+
const findings = [];
|
|
28
|
+
const request = input.originalRequest;
|
|
29
|
+
const spec = input.generatedSpec;
|
|
30
|
+
findings.push(...findUnrequestedDomains(request, spec));
|
|
31
|
+
findings.push(...findInventedEndpointRequirements(request, spec));
|
|
32
|
+
findings.push(...findGenericCoverageRequirement(request, spec));
|
|
33
|
+
findings.push(...findPathDerivedAssumptions(request, spec));
|
|
34
|
+
return {
|
|
35
|
+
passed: findings.every((finding) => finding.severity !== 'error'),
|
|
36
|
+
findings,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function findUnrequestedDomains(request, spec) {
|
|
40
|
+
const findings = [];
|
|
41
|
+
for (const domain of GUARDED_DOMAINS) {
|
|
42
|
+
if (isDomainRequested(domain, request) || !domain.specPattern.test(spec)) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
findings.push({
|
|
46
|
+
rule: `ai-spec-unrequested-${domain.name}`,
|
|
47
|
+
severity: 'error',
|
|
48
|
+
message: `Generated spec introduced ${domain.name} requirements that are not present in the user request.`,
|
|
49
|
+
evidence: firstMatch(spec, domain.specPattern),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
return findings;
|
|
53
|
+
}
|
|
54
|
+
function findInventedEndpointRequirements(request, spec) {
|
|
55
|
+
if (/\b(api|endpoint|route|http|status|401|403|404|500)\b/i.test(request)) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
if (!INVENTED_STATUS_RE.test(spec)) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
rule: 'ai-spec-invented-endpoint-status',
|
|
64
|
+
severity: 'error',
|
|
65
|
+
message: 'Generated spec introduced endpoint/status-code behavior not present in the user request.',
|
|
66
|
+
evidence: firstMatch(spec, INVENTED_STATUS_RE),
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
function findGenericCoverageRequirement(request, spec) {
|
|
71
|
+
if (GENERIC_COVERAGE_RE.test(request) || !GENERIC_COVERAGE_RE.test(spec)) {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
return [
|
|
75
|
+
{
|
|
76
|
+
rule: 'ai-spec-generic-coverage',
|
|
77
|
+
severity: 'error',
|
|
78
|
+
message: 'Generated spec introduced a generic coverage requirement not tied to the requested behavior.',
|
|
79
|
+
evidence: firstMatch(spec, GENERIC_COVERAGE_RE),
|
|
80
|
+
},
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
function findPathDerivedAssumptions(request, spec) {
|
|
84
|
+
const findings = [];
|
|
85
|
+
const pathTokens = new Set();
|
|
86
|
+
for (const match of request.matchAll(PATH_LIKE_DOMAIN_RE)) {
|
|
87
|
+
const token = match[1]?.toLowerCase();
|
|
88
|
+
if (token) {
|
|
89
|
+
pathTokens.add(token);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (pathTokens.size === 0) {
|
|
93
|
+
return findings;
|
|
94
|
+
}
|
|
95
|
+
for (const token of pathTokens) {
|
|
96
|
+
const domain = GUARDED_DOMAINS.find((candidate) => candidate.name === token);
|
|
97
|
+
if (!domain || isDomainRequested(domain, stripPathLikeSegments(request))) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!domain.specPattern.test(spec)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
findings.push({
|
|
104
|
+
rule: 'ai-spec-path-derived-assumption',
|
|
105
|
+
severity: 'error',
|
|
106
|
+
message: `Generated spec appears to derive ${token} requirements from an incidental repository path.`,
|
|
107
|
+
evidence: firstMatch(request, PATH_LIKE_DOMAIN_RE),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
return findings;
|
|
111
|
+
}
|
|
112
|
+
function isDomainRequested(domain, request) {
|
|
113
|
+
const requestWithoutPaths = stripPathLikeSegments(request);
|
|
114
|
+
if (!domain.requestPattern.test(requestWithoutPaths)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const domainTerms = domain.specPattern.source
|
|
118
|
+
.replaceAll('\\b', '')
|
|
119
|
+
.replace(/[()?:]/g, '')
|
|
120
|
+
.split('|')
|
|
121
|
+
.filter((term) => /^[a-z]+$/i.test(term));
|
|
122
|
+
for (const term of domainTerms) {
|
|
123
|
+
const negated = new RegExp(`\\b${term}\\b[^.\\n]*(?:not in scope|out of scope|not required|not part of scope|negative example)`, 'i');
|
|
124
|
+
if (negated.test(requestWithoutPaths)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
function stripPathLikeSegments(value) {
|
|
131
|
+
return value.replace(PATH_LIKE_DOMAIN_RE, ' ');
|
|
132
|
+
}
|
|
133
|
+
function firstMatch(value, pattern) {
|
|
134
|
+
const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
|
|
135
|
+
const match = new RegExp(pattern.source, flags).exec(value);
|
|
136
|
+
return match?.[0]?.trim() ?? '';
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=spec-assurance.js.map
|
|
@@ -210,6 +210,30 @@ export const GATE_CATALOG = [
|
|
|
210
210
|
riskLevels: ['low', 'medium', 'high'],
|
|
211
211
|
specTypes: ['all'],
|
|
212
212
|
},
|
|
213
|
+
{
|
|
214
|
+
id: 'arch-ai-spec-assurance',
|
|
215
|
+
category: 'architecture',
|
|
216
|
+
title: 'AI-generated specs are checked against original user intent before implementation',
|
|
217
|
+
stacks: ['typescript', 'generic'],
|
|
218
|
+
riskLevels: ['low', 'medium', 'high'],
|
|
219
|
+
specTypes: ['feature', 'bugfix', 'infra', 'all'],
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
id: 'arch-new-code-domain-inference',
|
|
223
|
+
category: 'architecture',
|
|
224
|
+
title: 'New code does not infer product-domain requirements from incidental paths or stack presence',
|
|
225
|
+
stacks: ['typescript', 'generic'],
|
|
226
|
+
riskLevels: ['low', 'medium', 'high'],
|
|
227
|
+
specTypes: ['feature', 'bugfix', 'infra', 'all'],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'arch-mcp-startup-assurance',
|
|
231
|
+
category: 'architecture',
|
|
232
|
+
title: 'MCP startup verifies official tools in first tools/list and excludes bootstrap-only tools',
|
|
233
|
+
stacks: ['typescript', 'generic'],
|
|
234
|
+
riskLevels: ['low', 'medium', 'high'],
|
|
235
|
+
specTypes: ['feature', 'bugfix', 'infra', 'all'],
|
|
236
|
+
},
|
|
213
237
|
{
|
|
214
238
|
id: 'arch-single-responsibility',
|
|
215
239
|
category: 'architecture',
|
package/dist/tools/validate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// tools/validate.ts — Compare spec vs implementation (SPEC-595: elicitation on failures)
|
|
2
2
|
// Uses the validator engine to check coverage, missing items, quality issues.
|
|
3
3
|
import { execSync, execFileSync } from 'node:child_process';
|
|
4
|
+
import { readFileSync } from 'node:fs';
|
|
4
5
|
import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
|
|
5
6
|
import { compactObj } from '../engine/compact-obj.js';
|
|
6
7
|
import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js';
|
|
@@ -15,6 +16,7 @@ import { parseConventions, scanConventions } from '../engine/convention-scanner/
|
|
|
15
16
|
import { validateScopeCompliance } from '../engine/scope-boundaries/index.js';
|
|
16
17
|
import { compareWithBaseline } from '../storage/convention-baseline.js';
|
|
17
18
|
import { writeImplementationReviewReport } from '../engine/validator/validation-report-writer.js';
|
|
19
|
+
import { evaluateNewCodeGate } from '../engine/ai-assurance/index.js';
|
|
18
20
|
// Re-export for external use (SPEC-018)
|
|
19
21
|
export { validateContractCompliance };
|
|
20
22
|
/** Build the fallback interactiveQuestion for validation failures. */
|
|
@@ -131,6 +133,7 @@ export async function handleValidate(args, server) {
|
|
|
131
133
|
}
|
|
132
134
|
// 7. Lint check (best-effort — non-blocking)
|
|
133
135
|
const lintCheck = runLintCheck(projectPath, knowledge.lintCommand ?? null);
|
|
136
|
+
const assuranceGates = runAssuranceGates(projectPath);
|
|
134
137
|
// SPEC-1050: Generate a mandatory implementation-review artifact.
|
|
135
138
|
const validationReport = await writeImplementationReviewReport({
|
|
136
139
|
projectId,
|
|
@@ -215,6 +218,7 @@ export async function handleValidate(args, server) {
|
|
|
215
218
|
})),
|
|
216
219
|
},
|
|
217
220
|
lintCheck,
|
|
221
|
+
assuranceGates,
|
|
218
222
|
validationReport,
|
|
219
223
|
};
|
|
220
224
|
// SPEC-612: Scope boundary validation — warn if impl files match outOfScope items
|
|
@@ -259,6 +263,9 @@ export async function handleValidate(args, server) {
|
|
|
259
263
|
if (!lintCheck.passed) {
|
|
260
264
|
suggestions.push(`Lint check failed: ${lintCheck.issueCount} issue(s) found via \`${lintCheck.command}\`. Fix before marking as done.`);
|
|
261
265
|
}
|
|
266
|
+
if (!assuranceGates.newCode.passed) {
|
|
267
|
+
suggestions.push(`Assurance gate failed: ${String(assuranceGates.newCode.findings.length)} new-code domain inference issue(s) found.`);
|
|
268
|
+
}
|
|
262
269
|
const outputWithSuggestions = compactObj({
|
|
263
270
|
...output,
|
|
264
271
|
suggestions: suggestions.length > 0 ? suggestions : undefined,
|
|
@@ -417,6 +424,42 @@ function commandOutput(err) {
|
|
|
417
424
|
.join('\n')
|
|
418
425
|
.trim();
|
|
419
426
|
}
|
|
427
|
+
function runAssuranceGates(projectPath) {
|
|
428
|
+
return {
|
|
429
|
+
newCode: evaluateNewCodeGate(readChangedSpecLogicFiles(projectPath)),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function readChangedSpecLogicFiles(projectPath) {
|
|
433
|
+
let changedFiles;
|
|
434
|
+
try {
|
|
435
|
+
const output = execFileSync('git', [
|
|
436
|
+
'-C',
|
|
437
|
+
projectPath,
|
|
438
|
+
'diff',
|
|
439
|
+
'--name-only',
|
|
440
|
+
'HEAD',
|
|
441
|
+
'--',
|
|
442
|
+
'src/tools/create-spec',
|
|
443
|
+
'src/tools/validate.ts',
|
|
444
|
+
'src/engine',
|
|
445
|
+
], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
446
|
+
changedFiles = output
|
|
447
|
+
.split('\n')
|
|
448
|
+
.map((line) => line.trim())
|
|
449
|
+
.filter((line) => line.endsWith('.ts'));
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return [];
|
|
453
|
+
}
|
|
454
|
+
return changedFiles.flatMap((path) => {
|
|
455
|
+
try {
|
|
456
|
+
return [{ path, content: readFileSync(`${projectPath}/${path}`, 'utf-8') }];
|
|
457
|
+
}
|
|
458
|
+
catch {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
420
463
|
function runLintCheck(projectPath, lintCommand) {
|
|
421
464
|
if (lintCommand !== null && !isSafeCommand(lintCommand)) {
|
|
422
465
|
console.warn(`[Planu] validate: lintCommand contains unsafe characters — skipping execution`);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export type SpecAssuranceSeverity = 'error' | 'warning';
|
|
2
|
+
export interface SpecAssuranceFinding {
|
|
3
|
+
rule: string;
|
|
4
|
+
severity: SpecAssuranceSeverity;
|
|
5
|
+
message: string;
|
|
6
|
+
evidence: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SpecAssuranceResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
findings: SpecAssuranceFinding[];
|
|
11
|
+
}
|
|
12
|
+
export interface SpecAssuranceInput {
|
|
13
|
+
originalRequest: string;
|
|
14
|
+
generatedSpec: string;
|
|
15
|
+
}
|
|
16
|
+
export interface NewCodeFile {
|
|
17
|
+
path: string;
|
|
18
|
+
content: string;
|
|
19
|
+
}
|
|
20
|
+
export interface NewCodeGateFinding {
|
|
21
|
+
rule: string;
|
|
22
|
+
file: string;
|
|
23
|
+
line: number;
|
|
24
|
+
message: string;
|
|
25
|
+
evidence: string;
|
|
26
|
+
}
|
|
27
|
+
export interface NewCodeGateResult {
|
|
28
|
+
passed: boolean;
|
|
29
|
+
findings: NewCodeGateFinding[];
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=ai-assurance.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planu/cli",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.18",
|
|
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.3.
|
|
38
|
-
"@planu/core-darwin-x64": "4.3.
|
|
39
|
-
"@planu/core-linux-arm64-gnu": "4.3.
|
|
40
|
-
"@planu/core-linux-arm64-musl": "4.3.
|
|
41
|
-
"@planu/core-linux-x64-gnu": "4.3.
|
|
42
|
-
"@planu/core-linux-x64-musl": "4.3.
|
|
43
|
-
"@planu/core-win32-arm64-msvc": "4.3.
|
|
44
|
-
"@planu/core-win32-x64-msvc": "4.3.
|
|
37
|
+
"@planu/core-darwin-arm64": "4.3.18",
|
|
38
|
+
"@planu/core-darwin-x64": "4.3.18",
|
|
39
|
+
"@planu/core-linux-arm64-gnu": "4.3.18",
|
|
40
|
+
"@planu/core-linux-arm64-musl": "4.3.18",
|
|
41
|
+
"@planu/core-linux-x64-gnu": "4.3.18",
|
|
42
|
+
"@planu/core-linux-x64-musl": "4.3.18",
|
|
43
|
+
"@planu/core-win32-arm64-msvc": "4.3.18",
|
|
44
|
+
"@planu/core-win32-x64-msvc": "4.3.18"
|
|
45
45
|
},
|
|
46
46
|
"engines": {
|
|
47
47
|
"node": ">=24.0.0"
|
package/planu-native.json
CHANGED
package/planu-plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "dev.planu.cli",
|
|
3
3
|
"displayName": "Planu — Spec Driven Development",
|
|
4
4
|
"description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
|
|
5
|
-
"version": "4.3.
|
|
5
|
+
"version": "4.3.18",
|
|
6
6
|
"icon": "assets/plugin/icon.svg",
|
|
7
7
|
"command": ["npx", "@planu/cli@latest"],
|
|
8
8
|
"packageName": "@planu/cli",
|