@planu/cli 4.3.17 → 4.3.19

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 CHANGED
@@ -1,3 +1,9 @@
1
+ ## [4.3.19] - 2026-05-27
2
+
3
+ ### Bug Fixes
4
+ - fix(SPEC-1072): clean spec advisory surfaces
5
+
6
+
1
7
  ## [4.3.13] - 2026-05-25
2
8
 
3
9
  ### Bug Fixes
@@ -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
@@ -1,5 +1,6 @@
1
1
  // pii-detector/core.ts — PII detection in spec text (SPEC-030)
2
2
  import { createRequire } from 'node:module';
3
+ import { stripNonContractText } from '../text-signal-boundaries.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  export const patterns = require('../../config/pii-patterns.json');
5
6
  // ---------------------------------------------------------------------------
@@ -33,6 +34,16 @@ function isBlacklistedContext(modelContext) {
33
34
  const lower = modelContext.toLowerCase();
34
35
  return patterns.modelContextBlacklist.some((b) => lower.includes(b.toLowerCase()));
35
36
  }
37
+ function isWhitelistedContext(modelContext) {
38
+ const lower = modelContext.toLowerCase();
39
+ return patterns.modelContextWhitelist.some((w) => lower.includes(w.toLowerCase()));
40
+ }
41
+ function hasFieldCue(contextWindow) {
42
+ return /\b(field|fields|column|columns|property|properties|attribute|schema|model|entity|table|class|interface)\b/i.test(contextWindow);
43
+ }
44
+ function hasCollectionCue(contextWindow) {
45
+ return /\b(collect|store|process|persist|save|record|provide|must have|includes?|contains?)\b/i.test(contextWindow);
46
+ }
36
47
  /**
37
48
  * Determines if a detected PII field is a GDPR Art. 9 special category.
38
49
  */
@@ -77,18 +88,26 @@ function inferModelContext(contextWindow) {
77
88
  */
78
89
  function extractCandidates(text) {
79
90
  const candidates = [];
80
- const lower = text.toLowerCase();
91
+ const cleaned = stripNonContractText(text);
92
+ const lower = cleaned.toLowerCase();
81
93
  /* v8 ignore start */
82
- const words = text.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) ?? [];
94
+ const words = cleaned.match(/\b[a-zA-Z_][a-zA-Z0-9_]*\b/g) ?? [];
83
95
  /* v8 ignore stop */
84
96
  for (const word of words) {
85
97
  if (word.length < 3) {
86
98
  continue;
87
99
  }
100
+ if (matchVocabulary(normalizeField(word)) === null) {
101
+ continue;
102
+ }
88
103
  const wordIndex = lower.indexOf(word.toLowerCase());
89
- const contextWindow = text.substring(Math.max(0, wordIndex - 100), wordIndex + 100);
104
+ const contextWindow = cleaned.substring(Math.max(0, wordIndex - 100), wordIndex + 100);
90
105
  const context = inferModelContext(contextWindow);
91
- candidates.push({ field: word, context });
106
+ if (isWhitelistedContext(context) ||
107
+ (hasFieldCue(contextWindow) && !isBlacklistedContext(contextWindow)) ||
108
+ (hasCollectionCue(contextWindow) && isWhitelistedContext(contextWindow))) {
109
+ candidates.push({ field: word, context });
110
+ }
92
111
  }
93
112
  return candidates;
94
113
  }
@@ -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',
@@ -0,0 +1,6 @@
1
+ /** Remove fenced code and quoted examples before keyword-driven inference. */
2
+ export declare function stripNonContractText(text: string): string;
3
+ /** True only when a regex match appears in contract prose and is not negated/excluded. */
4
+ export declare function hasAffirmedMatch(text: string, pattern: RegExp): boolean;
5
+ export declare function hasAnyAffirmedMatch(text: string, patterns: RegExp[]): boolean;
6
+ //# sourceMappingURL=text-signal-boundaries.d.ts.map
@@ -0,0 +1,62 @@
1
+ // engine/text-signal-boundaries.ts — Boundary-aware keyword matching for advisory signals.
2
+ const NEGATION_WINDOW_WORDS = 5;
3
+ const NEGATION_TERMS = new Set([
4
+ 'no',
5
+ 'not',
6
+ 'without',
7
+ 'avoid',
8
+ 'exclude',
9
+ 'excluding',
10
+ 'except',
11
+ 'skip',
12
+ 'remove',
13
+ 'disable',
14
+ 'disallow',
15
+ 'never',
16
+ ]);
17
+ const OUT_OF_SCOPE_RE = /\b(?:out\s+of\s+scope|not\s+in\s+scope|no\s+scope|do\s+not|don't)\b/i;
18
+ /** Remove fenced code and quoted examples before keyword-driven inference. */
19
+ export function stripNonContractText(text) {
20
+ return text
21
+ .replace(/```[\s\S]*?```/g, ' ')
22
+ .replace(/`[^`\n]+`/g, ' ')
23
+ .replace(/"[^"\n]+"/g, ' ')
24
+ .replace(/'[^'\n]+'/g, ' ');
25
+ }
26
+ function wordTokensBefore(text, index) {
27
+ return text
28
+ .slice(Math.max(0, index - 160), index)
29
+ .toLowerCase()
30
+ .split(/[^a-z0-9]+/i)
31
+ .filter(Boolean);
32
+ }
33
+ function isNegatedAt(text, index) {
34
+ const before = wordTokensBefore(text, index);
35
+ const recent = before.slice(-NEGATION_WINDOW_WORDS);
36
+ return recent.some((word) => NEGATION_TERMS.has(word));
37
+ }
38
+ function isOutOfScopeLine(line, indexInLine) {
39
+ return OUT_OF_SCOPE_RE.test(line.slice(0, Math.max(indexInLine, 0) + 80));
40
+ }
41
+ /** True only when a regex match appears in contract prose and is not negated/excluded. */
42
+ export function hasAffirmedMatch(text, pattern) {
43
+ const cleaned = stripNonContractText(text);
44
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
45
+ const re = new RegExp(pattern.source, flags);
46
+ let match = re.exec(cleaned);
47
+ while (match) {
48
+ const index = match.index;
49
+ const lineStart = cleaned.lastIndexOf('\n', index) + 1;
50
+ const lineEnd = cleaned.indexOf('\n', index);
51
+ const line = cleaned.slice(lineStart, lineEnd === -1 ? cleaned.length : lineEnd);
52
+ if (!isNegatedAt(cleaned, index) && !isOutOfScopeLine(line, index - lineStart)) {
53
+ return true;
54
+ }
55
+ match = re.exec(cleaned);
56
+ }
57
+ return false;
58
+ }
59
+ export function hasAnyAffirmedMatch(text, patterns) {
60
+ return patterns.some((pattern) => hasAffirmedMatch(text, pattern));
61
+ }
62
+ //# sourceMappingURL=text-signal-boundaries.js.map
@@ -1,14 +1,18 @@
1
1
  // tools/challenge-spec/scenarios-data.ts — Data consistency and partial write scenarios
2
- import { contentMentions } from './scenarios-utils.js';
2
+ import { contentMentions, hasAnyAffirmedMatch } from './scenarios-utils.js';
3
+ const WRITE_SIGNALS = [
4
+ /\b(transaction|persist|database write|save record|create record|update record|delete record)\b/i,
5
+ /\b(order|checkout|submit form|mutation|multi-step write)\b/i,
6
+ ];
7
+ const CACHE_SIGNALS = [/\b(cache|real-time|realtime|concurrent|websocket|stale data)\b/i];
8
+ const DUPLICATE_OPERATION_SIGNALS = [
9
+ /\b(submit|form|checkout|order|payment|create record|mutation|button click)\b/i,
10
+ ];
3
11
  export function generateDataConsistencyScenarios(_spec, content, _knowledge) {
4
12
  const scenarios = [];
5
13
  const lower = content.toLowerCase();
6
14
  // Partial writes
7
- if (lower.includes('transaction') ||
8
- lower.includes('update') ||
9
- lower.includes('create') ||
10
- lower.includes('modify') ||
11
- lower.includes('save')) {
15
+ if (hasAnyAffirmedMatch(content, WRITE_SIGNALS)) {
12
16
  scenarios.push({
13
17
  scenario: 'Partial write: server crashes mid-transaction',
14
18
  probability: 'low',
@@ -22,7 +26,7 @@ export function generateDataConsistencyScenarios(_spec, content, _knowledge) {
22
26
  });
23
27
  }
24
28
  // Stale data
25
- if (lower.includes('cache') || lower.includes('real-time') || lower.includes('concurrent')) {
29
+ if (hasAnyAffirmedMatch(content, CACHE_SIGNALS)) {
26
30
  scenarios.push({
27
31
  scenario: 'Stale data served from cache after underlying data changes',
28
32
  probability: 'high',
@@ -35,18 +39,20 @@ export function generateDataConsistencyScenarios(_spec, content, _knowledge) {
35
39
  userExperience: 'Show "last updated" timestamp. Offer manual refresh option.',
36
40
  });
37
41
  }
38
- // Duplicate operations
39
- scenarios.push({
40
- scenario: 'User double-clicks submit, causing duplicate records',
41
- probability: 'high',
42
- impact: 'medium',
43
- currentHandling: contentMentions(lower, ['idempoten', 'debounce', 'disable', 'lock'])
44
- ? 'Some prevention mentioned'
45
- : 'Not addressed',
46
- requiredHandling: 'Implement idempotency keys for mutations. Disable submit button on click. Use unique constraints in DB.',
47
- dataConsistency: 'No duplicate records from repeated submissions.',
48
- userExperience: 'Button disabled during submission. Clear success/error feedback.',
49
- });
42
+ // Duplicate operations only apply to explicit user-triggered mutations.
43
+ if (hasAnyAffirmedMatch(content, DUPLICATE_OPERATION_SIGNALS)) {
44
+ scenarios.push({
45
+ scenario: 'User double-clicks submit, causing duplicate records',
46
+ probability: 'high',
47
+ impact: 'medium',
48
+ currentHandling: contentMentions(lower, ['idempoten', 'debounce', 'disable', 'lock'])
49
+ ? 'Some prevention mentioned'
50
+ : 'Not addressed',
51
+ requiredHandling: 'Implement idempotency keys for mutations. Disable submit button on click. Use unique constraints in DB.',
52
+ dataConsistency: 'No duplicate records from repeated submissions.',
53
+ userExperience: 'Button disabled during submission. Clear success/error feedback.',
54
+ });
55
+ }
50
56
  return scenarios;
51
57
  }
52
58
  //# sourceMappingURL=scenarios-data.js.map
@@ -1,14 +1,20 @@
1
1
  // tools/challenge-spec/scenarios-security.ts — Authentication, injection, and CSRF scenarios
2
- import { contentMentions } from './scenarios-utils.js';
2
+ import { contentMentions, hasAnyAffirmedMatch } from './scenarios-utils.js';
3
+ const AUTH_SIGNALS = [
4
+ /\b(auth|authentication|login|session|token|jwt|oauth|permission|role|rbac)\b/i,
5
+ ];
6
+ const INPUT_SIGNALS = [
7
+ /\b(user input|form|query param|search input|request body|payload|html input)\b/i,
8
+ ];
9
+ const STATE_CHANGING_WEB_SIGNALS = [
10
+ /\b(form submit|post request|put request|delete request|mutation|state-changing|cookie auth)\b/i,
11
+ ];
3
12
  export function generateSecurityScenarios(_spec, content, _knowledge) {
4
13
  const scenarios = [];
5
14
  const lower = content.toLowerCase();
15
+ const text = content;
6
16
  // Authentication bypass
7
- if (lower.includes('auth') ||
8
- lower.includes('login') ||
9
- lower.includes('user') ||
10
- lower.includes('permission') ||
11
- lower.includes('role')) {
17
+ if (hasAnyAffirmedMatch(text, AUTH_SIGNALS)) {
12
18
  scenarios.push({
13
19
  scenario: 'Unauthorized access attempt / authentication bypass',
14
20
  probability: 'high',
@@ -22,11 +28,7 @@ export function generateSecurityScenarios(_spec, content, _knowledge) {
22
28
  });
23
29
  }
24
30
  // Injection attacks
25
- if (lower.includes('input') ||
26
- lower.includes('form') ||
27
- lower.includes('query') ||
28
- lower.includes('search') ||
29
- lower.includes('user')) {
31
+ if (hasAnyAffirmedMatch(text, INPUT_SIGNALS)) {
30
32
  scenarios.push({
31
33
  scenario: 'SQL injection / XSS / command injection via user input',
32
34
  probability: 'high',
@@ -39,18 +41,22 @@ export function generateSecurityScenarios(_spec, content, _knowledge) {
39
41
  userExperience: 'Transparent to user. Invalid input is silently sanitized.',
40
42
  });
41
43
  }
42
- // CSRF / rate limiting
43
- scenarios.push({
44
- scenario: 'CSRF attack or brute-force attempt',
45
- probability: 'medium',
46
- impact: 'high',
47
- currentHandling: contentMentions(lower, ['csrf', 'rate limit', 'throttle', 'captcha'])
48
- ? 'Some protection mentioned'
49
- : 'Not addressed',
50
- requiredHandling: 'Implement CSRF tokens for state-changing operations. Rate-limit sensitive endpoints. Add CAPTCHA for auth endpoints.',
51
- dataConsistency: 'Prevent mass data modifications from automated attacks.',
52
- userExperience: 'Show rate limit message. Require CAPTCHA after N failed attempts.',
53
- });
44
+ // CSRF / rate limiting only applies when the spec explicitly involves auth or
45
+ // state-changing web input. Tooling/spec-cleanup tasks should not get this as noise.
46
+ if (hasAnyAffirmedMatch(text, AUTH_SIGNALS) ||
47
+ hasAnyAffirmedMatch(text, STATE_CHANGING_WEB_SIGNALS)) {
48
+ scenarios.push({
49
+ scenario: 'CSRF attack or brute-force attempt',
50
+ probability: 'medium',
51
+ impact: 'high',
52
+ currentHandling: contentMentions(lower, ['csrf', 'rate limit', 'throttle', 'captcha'])
53
+ ? 'Some protection mentioned'
54
+ : 'Not addressed',
55
+ requiredHandling: 'Implement CSRF tokens for state-changing operations. Rate-limit sensitive endpoints. Add CAPTCHA for auth endpoints.',
56
+ dataConsistency: 'Prevent mass data modifications from automated attacks.',
57
+ userExperience: 'Show rate limit message. Require CAPTCHA after N failed attempts.',
58
+ });
59
+ }
54
60
  return scenarios;
55
61
  }
56
62
  //# sourceMappingURL=scenarios-security.js.map
@@ -3,4 +3,5 @@
3
3
  * lowerContent is expected to already be lowercased by the caller.
4
4
  */
5
5
  export declare function contentMentions(lowerContent: string, keywords: string[]): boolean;
6
+ export { hasAffirmedMatch, hasAnyAffirmedMatch, stripNonContractText, } from '../../engine/text-signal-boundaries.js';
6
7
  //# sourceMappingURL=scenarios-utils.d.ts.map
@@ -6,4 +6,5 @@
6
6
  export function contentMentions(lowerContent, keywords) {
7
7
  return keywords.some((kw) => lowerContent.includes(kw.toLowerCase()));
8
8
  }
9
+ export { hasAffirmedMatch, hasAnyAffirmedMatch, stripNonContractText, } from '../../engine/text-signal-boundaries.js';
9
10
  //# sourceMappingURL=scenarios-utils.js.map
@@ -182,6 +182,15 @@ const EN_STOPWORDS = new Set([
182
182
  'it',
183
183
  'be',
184
184
  'as',
185
+ 'spec',
186
+ 'create',
187
+ 'workflow',
188
+ 'files',
189
+ 'file',
190
+ 'example',
191
+ 'avoid',
192
+ 'planu',
193
+ 'feature',
185
194
  ]);
186
195
  /** Infer meaningful tags from title and description when no tags were provided. */
187
196
  export function inferTagsFromContent(title, description, type) {
@@ -213,13 +222,13 @@ export function inferTagsFromContent(title, description, type) {
213
222
  }
214
223
  return tags;
215
224
  }
216
- /** Build the final tags array for a spec, enforcing minimum 3 tags. */
217
- function buildTags(tags, feature, title, description, type) {
218
- const result = feature ? [...new Set([...tags, slugify(feature)])] : [...tags];
219
- if (result.length === 0) {
220
- return inferTagsFromContent(title, description, type);
221
- }
222
- return result;
225
+ /** Build the final tags array from explicit user/tool input only. */
226
+ function buildTags(tags, feature, _title, _description, _type) {
227
+ const explicitTags = tags
228
+ .map((tag) => slugify(tag))
229
+ .filter((tag) => tag.length > 0 && !EN_STOPWORDS.has(tag));
230
+ const result = feature ? [...new Set([...explicitTags, slugify(feature)])] : explicitTags;
231
+ return [...new Set(result)];
223
232
  }
224
233
  /* v8 ignore start -- filesystem scanning is best-effort, tested via integration */
225
234
  /** Scan filesystem for SPEC-XXX directories to find max ID globally. */
@@ -14,7 +14,7 @@ import { setupGitBranch, checkContradictions, fireSpecCreatedHook, generatePostC
14
14
  import { notifyStoreChange } from '../engine/doc-generator/portal/regen-hook.js';
15
15
  import { compactObj } from '../engine/compact-obj.js';
16
16
  import { buildCreateSpecSummary } from '../engine/human-summary.js';
17
- import { runAutoPostCreatePipeline, formatPipelineLines } from './create-spec/auto-pipeline.js';
17
+ import { runAutoPostCreatePipeline } from './create-spec/auto-pipeline.js';
18
18
  import { generateLeanSpecContent } from '../engine/spec-format/lean-spec-generator.js';
19
19
  import { generateLeanTechnicalContent } from '../engine/spec-format/lean-technical-generator.js';
20
20
  import { buildUnifiedSpecContent } from '../engine/spec-format/unified-spec-builder.js';
@@ -133,6 +133,16 @@ function runSimplicityCheck(text, hours) {
133
133
  return null;
134
134
  }
135
135
  }
136
+ function makeAdvisorySignal(args) {
137
+ return { ...args, persistable: false };
138
+ }
139
+ function buildAdvisoryCompat() {
140
+ return {
141
+ legacyTopLevelAdvisoryFields: true,
142
+ deprecatedAfter: 'vNext+2',
143
+ canonicalField: 'advisorySignals',
144
+ };
145
+ }
136
146
  /** SPEC-612: resolve outOfScope items (provided by user, or auto-suggested from description). */
137
147
  function resolveOutOfScope(description, provided) {
138
148
  if (provided !== undefined && provided.length > 0) {
@@ -181,18 +191,6 @@ async function runComplexityAdvice(projectId, currentSpecId, tags, targetCriteri
181
191
  return null;
182
192
  }
183
193
  }
184
- function formatSimplicityLines(r) {
185
- const icon = r.recommendation === 'simplify' ? '🔴' : r.recommendation === 'review' ? '🟡' : '🟢';
186
- const signalNames = r.signals.map((s) => s.type).join(', ');
187
- const lines = [
188
- '',
189
- `${icon} **Simplicity**: score ${String(r.score)}/100 — ${signalNames !== '' ? signalNames : 'no signals'}`,
190
- ];
191
- if (r.recommendation !== 'approve' && r.simpleAlternative !== null) {
192
- lines.push(` Suggestion: ${r.simpleAlternative}`);
193
- }
194
- return lines;
195
- }
196
194
  /** SPEC-783: Synthesize agent team findings into unified spec.md ## Technical section. */
197
195
  async function handleAgentTeamSynthesis(specId, projectPath, findings) {
198
196
  try {
@@ -723,9 +721,21 @@ export async function handleCreateSpec(inputParams, server) {
723
721
  /* v8 ignore next -- requires real git repo */
724
722
  ...(gitSetupResult ? { gitAutoSetup: gitSetupResult.data } : {}),
725
723
  };
724
+ const advisorySignals = [];
726
725
  const splitResult = buildSplitResult(splitSuggestion, knowledge?.experienceLevel);
727
726
  if (splitResult) {
728
727
  result.splitSuggestion = splitResult;
728
+ advisorySignals.push(makeAdvisorySignal({
729
+ key: 'split-suggestion',
730
+ kind: 'complexity',
731
+ message: 'Spec may benefit from splitting.',
732
+ source: 'heuristic',
733
+ evidence: ['spec-splitter heuristic'],
734
+ confidence: 0.5,
735
+ surface: 'structuredContent',
736
+ value: splitResult,
737
+ deprecatedAlias: 'splitSuggestion',
738
+ }));
729
739
  }
730
740
  // Dispatch hook event (fire-and-forget)
731
741
  fireSpecCreatedHook(projectId, spec, params.projectPath ?? '');
@@ -780,31 +790,97 @@ export async function handleCreateSpec(inputParams, server) {
780
790
  const qualityValue = unwrapBudget(qualityResult, null, budgetWarnings);
781
791
  if (qualityValue !== null) {
782
792
  result.qualityScore = qualityValue;
793
+ advisorySignals.push(makeAdvisorySignal({
794
+ key: 'quality-score',
795
+ kind: 'quality',
796
+ message: `Quality score ${String(qualityValue.total)}/100 (${qualityValue.grade}).`,
797
+ source: 'validator',
798
+ evidence: ['spec-quality-scorer'],
799
+ confidence: 0.7,
800
+ surface: 'structuredContent',
801
+ value: qualityValue,
802
+ deprecatedAlias: 'qualityScore',
803
+ }));
783
804
  }
784
805
  // SPEC-485: Simplicity autopilot — detect over-engineering signals (best-effort, sync)
785
806
  const simplicityResult = runSimplicityCheck([params.description, ...filteredCriteria].join('\n'), estimation.devHours);
786
807
  if (simplicityResult) {
787
808
  result.simplicityCheck = simplicityResult;
809
+ advisorySignals.push(makeAdvisorySignal({
810
+ key: 'simplicity-check',
811
+ kind: 'simplicity',
812
+ message: `Simplicity recommendation: ${simplicityResult.recommendation}.`,
813
+ source: 'heuristic',
814
+ evidence: simplicityResult.signals.map((signal) => signal.type),
815
+ confidence: 0.55,
816
+ surface: 'structuredContent',
817
+ value: simplicityResult,
818
+ deprecatedAlias: 'simplicityCheck',
819
+ }));
788
820
  }
789
821
  // SPEC-514: duplicate results
790
822
  const possibleDuplicates = unwrapBudget(duplicatesResult, [], budgetWarnings);
791
823
  if (possibleDuplicates.length > 0) {
792
824
  result.possibleDuplicates = possibleDuplicates;
825
+ advisorySignals.push(makeAdvisorySignal({
826
+ key: 'possible-duplicates',
827
+ kind: 'duplicate',
828
+ message: `${String(possibleDuplicates.length)} possible duplicate spec(s) detected.`,
829
+ source: 'heuristic',
830
+ evidence: possibleDuplicates.map((dup) => dup.specId),
831
+ confidence: 0.5,
832
+ surface: 'structuredContent',
833
+ value: possibleDuplicates,
834
+ deprecatedAlias: 'possibleDuplicates',
835
+ }));
793
836
  }
794
837
  // SPEC-222 Trigger 2: Auto-challenge hint for high-risk specs
795
838
  if (spec.risk === 'high' || spec.difficulty >= 4) {
796
839
  result.riskWarning = HIGH_RISK_WARNING;
840
+ advisorySignals.push(makeAdvisorySignal({
841
+ key: 'risk-warning',
842
+ kind: 'challenge',
843
+ message: 'High-risk heuristic warning generated.',
844
+ source: 'heuristic',
845
+ evidence: [`risk:${spec.risk}`, `difficulty:${String(spec.difficulty)}`],
846
+ confidence: 0.6,
847
+ surface: 'structuredContent',
848
+ value: HIGH_RISK_WARNING,
849
+ deprecatedAlias: 'riskWarning',
850
+ }));
797
851
  }
798
852
  // SPEC-614 AC4: complexity advice
799
853
  const cAdvice = unwrapBudget(complexityResult, null, budgetWarnings);
800
854
  if (cAdvice) {
801
855
  result.complexityAdvice = cAdvice;
856
+ advisorySignals.push(makeAdvisorySignal({
857
+ key: 'complexity-advice',
858
+ kind: 'complexity',
859
+ message: cAdvice.reasoning,
860
+ source: 'history',
861
+ evidence: [`similarSpecs:${String(cAdvice.similarSpecsCount)}`],
862
+ confidence: 0.6,
863
+ surface: 'structuredContent',
864
+ value: cAdvice,
865
+ deprecatedAlias: 'complexityAdvice',
866
+ }));
802
867
  }
803
868
  // SPEC-615: prior decisions
804
869
  const priorLinks = unwrapBudget(priorLinksResult, [], budgetWarnings);
805
870
  if (priorLinks.length > 0) {
806
871
  result.priorDecisions = priorLinks.map((l) => l.decisionId);
807
872
  spec.priorDecisions = result.priorDecisions;
873
+ advisorySignals.push(makeAdvisorySignal({
874
+ key: 'prior-decisions',
875
+ kind: 'prior-decision',
876
+ message: `${String(priorLinks.length)} prior decision link(s) found.`,
877
+ source: 'history',
878
+ evidence: priorLinks.map((link) => link.decisionId),
879
+ confidence: 0.65,
880
+ surface: 'structuredContent',
881
+ value: result.priorDecisions,
882
+ deprecatedAlias: 'priorDecisions',
883
+ }));
808
884
  }
809
885
  // Post-creation suggestions
810
886
  const nextSteps = unwrapBudget(nextStepsResult, [], budgetWarnings);
@@ -871,30 +947,13 @@ export async function handleCreateSpec(inputParams, server) {
871
947
  if (contradictionHint) {
872
948
  lines.push('', `⚠️ ${contradictionHint}`);
873
949
  }
874
- if (possibleDuplicates.length > 0) {
875
- lines.push('', '⚠️ **Possible Duplicates** (semantic similarity > 30%):');
876
- for (const dup of possibleDuplicates) {
877
- const pct = Math.round(dup.score * 100);
878
- lines.push(`- **${dup.specId}** — ${dup.title} (${String(pct)}% similar)`);
879
- }
880
- }
881
- if (result.riskWarning) {
882
- lines.push('', `⚠️ ${result.riskWarning}`);
883
- }
884
- if (splitResult) {
885
- lines.push('', `💡 ${splitResult.formatted ?? 'Consider splitting this spec.'}`);
886
- }
887
- // SPEC-492: Quality score output
888
- if (result.qualityScore) {
889
- const qs = result.qualityScore;
890
- lines.push('', `📊 **Quality score**: ${String(qs.total)}/100 (${qs.grade})`);
891
- }
892
- // SPEC-485: Simplicity autopilot output
893
- if (simplicityResult) {
894
- lines.push(...formatSimplicityLines(simplicityResult));
950
+ if (possibleDuplicates.length > 0 ||
951
+ result.riskWarning ||
952
+ splitResult ||
953
+ result.qualityScore ||
954
+ simplicityResult) {
955
+ lines.push('', 'Advisory signals were computed and are available in structuredContent.advisorySignals.');
895
956
  }
896
- // Append auto-pipeline output (SPEC-445)
897
- lines.push(...formatPipelineLines(pipelineResult));
898
957
  const allNextSteps = (result.nextSteps ?? []).map((step) => (typeof step === 'string' ? step : formatPostCreationSuggestion(step)));
899
958
  const markdownText = allNextSteps.length > 0
900
959
  ? addNextSteps(formatSuccess(ti('tools.create_spec.success', { id: spec.id, title: spec.title }), lines.join('\n')), allNextSteps)
@@ -904,15 +963,54 @@ export async function handleCreateSpec(inputParams, server) {
904
963
  // SPEC-612: Record outOfScope auto-suggestion in autopilot summary
905
964
  if (outOfScopeSuggestionMsg !== null) {
906
965
  collector.pushOk('scope-boundaries', outOfScopeSuggestionMsg);
966
+ advisorySignals.push(makeAdvisorySignal({
967
+ key: 'out-of-scope-suggestions',
968
+ kind: 'out-of-scope',
969
+ message: outOfScopeSuggestionMsg,
970
+ source: 'heuristic',
971
+ evidence: ['scope-boundaries suggester'],
972
+ confidence: 0.45,
973
+ surface: 'structuredContent',
974
+ value: spec.outOfScope ?? [],
975
+ }));
907
976
  }
908
977
  if (autopilot.detectedPatterns.length > 0) {
909
978
  collector.pushOk('pattern-detection', `Detected patterns: ${autopilot.detectedPatterns.join(', ')}`);
979
+ advisorySignals.push(makeAdvisorySignal({
980
+ key: 'detected-patterns',
981
+ kind: 'domain',
982
+ message: `Detected patterns: ${autopilot.detectedPatterns.join(', ')}`,
983
+ source: 'heuristic',
984
+ evidence: autopilot.detectedPatterns,
985
+ confidence: 0.5,
986
+ surface: 'structuredContent',
987
+ value: autopilot.detectedPatterns,
988
+ }));
910
989
  }
911
990
  const totalSuggestedFiles = autopilot.suggestedFiles.create.length +
912
991
  autopilot.suggestedFiles.modify.length +
913
992
  autopilot.suggestedFiles.test.length;
914
993
  if (totalSuggestedFiles > 0) {
915
994
  collector.pushOk('file-analysis', `Suggested ${String(totalSuggestedFiles)} files (${String(autopilot.suggestedFiles.create.length)} create, ${String(autopilot.suggestedFiles.modify.length)} modify, ${String(autopilot.suggestedFiles.test.length)} test)`);
995
+ advisorySignals.push(makeAdvisorySignal({
996
+ key: 'suggested-files',
997
+ kind: 'file',
998
+ message: `${String(totalSuggestedFiles)} possible related file(s) detected.`,
999
+ source: 'heuristic',
1000
+ evidence: [
1001
+ ...autopilot.suggestedFiles.create.map((f) => f.path),
1002
+ ...autopilot.suggestedFiles.modify.map((f) => f.path),
1003
+ ...autopilot.suggestedFiles.test.map((f) => f.path),
1004
+ ],
1005
+ confidence: 0.45,
1006
+ surface: 'structuredContent',
1007
+ value: {
1008
+ create: autopilot.suggestedFiles.create.map((f) => f.path),
1009
+ modify: autopilot.suggestedFiles.modify.map((f) => f.path),
1010
+ test: autopilot.suggestedFiles.test.map((f) => f.path),
1011
+ },
1012
+ deprecatedAlias: 'autopilotSummary.suggestedFiles',
1013
+ }));
916
1014
  }
917
1015
  if (filteredCriteria.length > 0) {
918
1016
  collector.pushOk('criteria-enrichment', `Added ${String(filteredCriteria.length)} acceptance criteria from project context`);
@@ -926,15 +1024,37 @@ export async function handleCreateSpec(inputParams, server) {
926
1024
  ? ` — difficulty ${String(spec.difficulty)}: use Opus to add exact file paths + function names`
927
1025
  : '';
928
1026
  collector.pushOk('readiness', `Readiness score: ${String(pipelineResult.readinessScore)}/100${opusHint}`);
1027
+ advisorySignals.push(makeAdvisorySignal({
1028
+ key: 'readiness-score',
1029
+ kind: 'readiness',
1030
+ message: `Readiness score: ${String(pipelineResult.readinessScore)}/100.`,
1031
+ source: 'validator',
1032
+ evidence: ['auto post-creation pipeline'],
1033
+ confidence: 0.6,
1034
+ surface: 'structuredContent',
1035
+ value: pipelineResult.readinessScore,
1036
+ }));
929
1037
  }
930
1038
  else if (spec.difficulty >= 3) {
931
1039
  // SPEC-629: always surface the Opus hint for high-difficulty specs
932
1040
  collector.pushOk('recommend_model', `Difficulty ${String(spec.difficulty)} spec — use Opus to add exact file paths, function names and anticipated test breaks.`);
1041
+ advisorySignals.push(makeAdvisorySignal({
1042
+ key: 'model-recommendation',
1043
+ kind: 'model',
1044
+ message: 'High-difficulty spec may benefit from a stronger model for review.',
1045
+ source: 'heuristic',
1046
+ evidence: [`difficulty:${String(spec.difficulty)}`],
1047
+ confidence: 0.4,
1048
+ surface: 'structuredContent',
1049
+ value: { recommendedTier: 'max' },
1050
+ }));
933
1051
  }
934
1052
  if (calibrationEnrichment.calibrationNote) {
935
1053
  collector.pushOk('calibration', `📐 ${calibrationEnrichment.calibrationNote}`);
936
1054
  }
937
1055
  const humanSummary = buildCreateSpecSummary(spec.title, estimation.devHours);
1056
+ result.advisorySignals = advisorySignals;
1057
+ result.compat = buildAdvisoryCompat();
938
1058
  const compactResult = compactObj(result);
939
1059
  // SPEC-722: Issue a planner token for this spec creation.
940
1060
  // Best-effort — token failure never blocks spec creation.
@@ -965,6 +1085,8 @@ export async function handleCreateSpec(inputParams, server) {
965
1085
  : undefined;
966
1086
  const baseResult = toolResult(markdownText, {
967
1087
  ...compactResult,
1088
+ advisorySignals,
1089
+ compat: buildAdvisoryCompat(),
968
1090
  humanSummary,
969
1091
  ...(collector.hasEntries() ? { autopilotSummary: collector.getMessages() } : {}),
970
1092
  ...(suggestedFilesPayload !== undefined
@@ -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.19",
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.19",
38
+ "@planu/core-darwin-x64": "4.3.19",
39
+ "@planu/core-linux-arm64-gnu": "4.3.19",
40
+ "@planu/core-linux-arm64-musl": "4.3.19",
41
+ "@planu/core-linux-x64-gnu": "4.3.19",
42
+ "@planu/core-linux-x64-musl": "4.3.19",
43
+ "@planu/core-win32-arm64-msvc": "4.3.19",
44
+ "@planu/core-win32-x64-msvc": "4.3.19"
45
45
  },
46
46
  "engines": {
47
47
  "node": ">=24.0.0"
package/planu-native.json CHANGED
@@ -1,20 +1,26 @@
1
1
  {
2
2
  "name": "dev.planu.native",
3
3
  "displayName": "Planu Native Lightweight Surface",
4
- "version": "4.3.17",
4
+ "version": "4.3.19",
5
5
  "packageName": "@planu/cli",
6
6
  "modes": {
7
7
  "lightweight": {
8
8
  "requiresMcp": false,
9
9
  "requiresDaemon": false,
10
- "hosts": ["codex", "claude-code"],
10
+ "hosts": [
11
+ "codex",
12
+ "claude-code"
13
+ ],
11
14
  "commands": [
12
15
  {
13
16
  "id": "planu.status",
14
17
  "title": "Project status",
15
18
  "description": "Show the compact Planu project snapshot without loading the MCP tool graph.",
16
19
  "invocation": "planu status",
17
- "hosts": ["codex", "claude-code"],
20
+ "hosts": [
21
+ "codex",
22
+ "claude-code"
23
+ ],
18
24
  "requiresMcp": false,
19
25
  "requiresDaemon": false,
20
26
  "mapsTo": "handlePlanStatus"
@@ -24,7 +30,10 @@
24
30
  "title": "Create spec",
25
31
  "description": "Create a new spec through the CLI-backed SDD contract.",
26
32
  "invocation": "planu spec create \"<title>\"",
27
- "hosts": ["codex", "claude-code"],
33
+ "hosts": [
34
+ "codex",
35
+ "claude-code"
36
+ ],
28
37
  "requiresMcp": false,
29
38
  "requiresDaemon": false,
30
39
  "mapsTo": "handleCreateSpec"
@@ -34,7 +43,10 @@
34
43
  "title": "List specs",
35
44
  "description": "List specs in the current project with optional status/type filters.",
36
45
  "invocation": "planu spec list",
37
- "hosts": ["codex", "claude-code"],
46
+ "hosts": [
47
+ "codex",
48
+ "claude-code"
49
+ ],
38
50
  "requiresMcp": false,
39
51
  "requiresDaemon": false,
40
52
  "mapsTo": "handleListSpecs"
@@ -44,7 +56,10 @@
44
56
  "title": "Validate spec",
45
57
  "description": "Validate a spec against the current codebase from the native CLI surface.",
46
58
  "invocation": "planu spec validate SPEC-001",
47
- "hosts": ["codex", "claude-code"],
59
+ "hosts": [
60
+ "codex",
61
+ "claude-code"
62
+ ],
48
63
  "requiresMcp": false,
49
64
  "requiresDaemon": false,
50
65
  "mapsTo": "handleValidate"
@@ -54,7 +69,10 @@
54
69
  "title": "Audit technical debt",
55
70
  "description": "Run the read-only project audit path for lightweight debt checks.",
56
71
  "invocation": "planu audit debt",
57
- "hosts": ["codex", "claude-code"],
72
+ "hosts": [
73
+ "codex",
74
+ "claude-code"
75
+ ],
58
76
  "requiresMcp": false,
59
77
  "requiresDaemon": false,
60
78
  "mapsTo": "handleAudit"
@@ -64,7 +82,10 @@
64
82
  "title": "Check release readiness",
65
83
  "description": "Check local branch cleanliness and main/develop/release sync readiness.",
66
84
  "invocation": "planu release check",
67
- "hosts": ["codex", "claude-code"],
85
+ "hosts": [
86
+ "codex",
87
+ "claude-code"
88
+ ],
68
89
  "requiresMcp": false,
69
90
  "requiresDaemon": false,
70
91
  "mapsTo": "releaseCommand"
package/planu-plugin.json CHANGED
@@ -2,9 +2,12 @@
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.19",
6
6
  "icon": "assets/plugin/icon.svg",
7
- "command": ["npx", "@planu/cli@latest"],
7
+ "command": [
8
+ "npx",
9
+ "@planu/cli@latest"
10
+ ],
8
11
  "packageName": "@planu/cli",
9
12
  "capabilities": {
10
13
  "tools": [
@@ -23,17 +26,42 @@
23
26
  "create_skill",
24
27
  "skill_search"
25
28
  ],
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"]
29
+ "resources": [
30
+ "planu://specs/list",
31
+ "planu://specs/{id}",
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
+ ]
29
45
  },
30
46
  "compatibility": {
31
47
  "minimumHostVersion": "1.0.0",
32
- "requiredFeatures": ["mcp-tools", "file-editing"]
48
+ "requiredFeatures": [
49
+ "mcp-tools",
50
+ "file-editing"
51
+ ]
33
52
  },
34
53
  "repository": "https://github.com/planu-dev/planu",
35
54
  "author": "Planu",
36
55
  "license": "MIT",
37
56
  "homepage": "https://planu.dev",
38
- "keywords": ["sdd", "spec-driven-development", "mcp", "specs", "planning", "ai", "bdd", "tdd"]
57
+ "keywords": [
58
+ "sdd",
59
+ "spec-driven-development",
60
+ "mcp",
61
+ "specs",
62
+ "planning",
63
+ "ai",
64
+ "bdd",
65
+ "tdd"
66
+ ]
39
67
  }