@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.
@@ -0,0 +1,4 @@
1
+ export * from './spec-assurance.js';
2
+ export * from './new-code-gate.js';
3
+ export type * from '../../types/ai-assurance.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,3 @@
1
+ export * from './spec-assurance.js';
2
+ export * from './new-code-gate.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,3 @@
1
+ import type { NewCodeFile, NewCodeGateResult } from '../../types/ai-assurance.js';
2
+ export declare function evaluateNewCodeGate(files: NewCodeFile[]): NewCodeGateResult;
3
+ //# sourceMappingURL=new-code-gate.d.ts.map
@@ -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,3 @@
1
+ import type { SpecAssuranceInput, SpecAssuranceResult } from '../../types/ai-assurance.js';
2
+ export declare function evaluateSpecAssurance(input: SpecAssuranceInput): SpecAssuranceResult;
3
+ //# sourceMappingURL=spec-assurance.d.ts.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',
@@ -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
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=ai-assurance.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@planu/cli",
3
- "version": "4.3.17",
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.17",
38
- "@planu/core-darwin-x64": "4.3.17",
39
- "@planu/core-linux-arm64-gnu": "4.3.17",
40
- "@planu/core-linux-arm64-musl": "4.3.17",
41
- "@planu/core-linux-x64-gnu": "4.3.17",
42
- "@planu/core-linux-x64-musl": "4.3.17",
43
- "@planu/core-win32-arm64-msvc": "4.3.17",
44
- "@planu/core-win32-x64-msvc": "4.3.17"
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.3.17",
4
+ "version": "4.3.18",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
package/planu-plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "dev.planu.cli",
3
3
  "displayName": "Planu — Spec Driven Development",
4
4
  "description": "Manage software specs, estimations, and autonomous SDD workflows. Language-agnostic MCP server for Claude Code.",
5
- "version": "4.3.17",
5
+ "version": "4.3.18",
6
6
  "icon": "assets/plugin/icon.svg",
7
7
  "command": ["npx", "@planu/cli@latest"],
8
8
  "packageName": "@planu/cli",