@planu/cli 5.3.62 → 5.3.64
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 +52 -0
- package/dist/.planu-build.json +1 -1
- package/dist/cli/commands/create.js +3 -16
- package/dist/cli/commands/dashboard.js +2 -16
- package/dist/cli/commands/init.js +2 -16
- package/dist/cli/commands/spec.js +5 -36
- package/dist/cli/commands/status.js +5 -28
- package/dist/cli/formatter.d.ts +7 -1
- package/dist/cli/formatter.js +49 -6
- package/dist/engine/cascade-hooks/hooks/implementing-kickoff.hook.js +38 -6
- package/dist/engine/dor-dod/index.d.ts +1 -1
- package/dist/engine/dor-dod/index.js +1 -1
- package/dist/engine/dor-dod/plan.d.ts +1 -0
- package/dist/engine/dor-dod/plan.js +2 -0
- package/dist/engine/evidence-gates/artifact-reader.js +1 -0
- package/dist/engine/evidence-gates/evidence-skeletons.js +1 -0
- package/dist/engine/execution-plan/plan-utils.d.ts +2 -6
- package/dist/engine/execution-plan/plan-utils.js +71 -37
- package/dist/engine/handoff-artifacts/implementation-review-reader.d.ts +4 -0
- package/dist/engine/handoff-artifacts/implementation-review-reader.js +72 -0
- package/dist/engine/rules-generator/index.js +4 -47
- package/dist/engine/skill-registry/installer.js +17 -28
- package/dist/engine/skills/skills-fetcher.js +0 -9
- package/dist/engine/skills-reconciler.js +1 -13
- package/dist/engine/validator/dor-dod.d.ts +2 -2
- package/dist/engine/validator/dor-dod.js +5 -4
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.d.ts +2 -4
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +24 -24
- package/dist/tools/challenge-spec/resilience-challenge-scenarios.js +1 -1
- package/dist/tools/challenge-spec/scenarios-failure.js +1 -2
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +3 -0
- package/dist/tools/challenge-spec/scenarios-utils.js +17 -1
- package/dist/tools/challenge-spec-helpers.d.ts +1 -1
- package/dist/tools/challenge-spec-helpers.js +5 -2
- package/dist/tools/challenge-spec.js +15 -3
- package/dist/tools/create-spec.js +18 -0
- package/dist/tools/generate-execution-plan.js +160 -54
- package/dist/tools/suggest-tooling/skills-catalog.js +0 -9
- package/dist/tools/update-status/dod-gates.js +46 -64
- package/dist/tools/update-status/side-effects.d.ts +4 -0
- package/dist/tools/update-status/side-effects.js +45 -4
- package/dist/tools/validate.js +8 -4
- package/dist/transports/transport-factory.js +13 -0
- package/dist/types/ai-tool-rules.d.ts +0 -1
- package/dist/types/cli.d.ts +3 -0
- package/dist/types/evidence-gates.d.ts +2 -0
- package/dist/types/execution.d.ts +5 -0
- package/dist/types/handoff-artifacts.d.ts +19 -0
- package/dist/types/index.d.ts +1 -0
- package/package.json +1 -1
- package/planu-plugin.json +1 -1
- package/scripts/lib/portable-paths.mjs +1 -0
- package/dist/engine/execution-plan/phases-b.d.ts +0 -4
- package/dist/engine/execution-plan/phases-b.js +0 -88
- package/dist/engine/execution-plan/phases.d.ts +0 -7
- package/dist/engine/execution-plan/phases.js +0 -216
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { reportClassifiedDegradation } from '../../errors/classified-degradation.js';
|
|
2
|
+
import { readTransitionLog } from '../../storage/transition-log.js';
|
|
3
|
+
import { readArtifact } from './io.js';
|
|
4
|
+
export function isLifecycleStubBody(specId, body) {
|
|
5
|
+
return (body === `Spec ${specId} reviewed by planu-spec-reviewer and approved for approval gates.` ||
|
|
6
|
+
body === `Spec ${specId} reviewed by planu-spec-reviewer with requested changes.`);
|
|
7
|
+
}
|
|
8
|
+
async function findImplementingActor(specId, projectId) {
|
|
9
|
+
const entries = await readTransitionLog(projectId, specId);
|
|
10
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
11
|
+
const entry = entries[i];
|
|
12
|
+
if (entry?.to === 'implementing' && entry.actor) {
|
|
13
|
+
return entry.actor;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
export async function readVerifiedImplementationReview(projectId, specId) {
|
|
19
|
+
try {
|
|
20
|
+
const result = await readArtifact({ projectId, specId, kind: 'implementation_review' });
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
const firstErr = result.errors[0];
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
reason: firstErr?.code === 'ARTIFACT_NOT_FOUND' ? 'missing' : 'schema_invalid',
|
|
26
|
+
errors: result.errors,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const review = result.payload;
|
|
30
|
+
if (review.specId !== specId) {
|
|
31
|
+
return { ok: false, reason: 'wrong_spec', review };
|
|
32
|
+
}
|
|
33
|
+
if (isLifecycleStubBody(specId, review.body)) {
|
|
34
|
+
return { ok: false, reason: 'stub_body', review };
|
|
35
|
+
}
|
|
36
|
+
if (review.verdict !== 'approved' || review.reviewer.verdict !== 'approved') {
|
|
37
|
+
return { ok: false, reason: 'not_approved', review };
|
|
38
|
+
}
|
|
39
|
+
if (review.reviewer.kind !== 'implementation-review-agent' ||
|
|
40
|
+
review.reviewer.agent !== 'planu-implementation-reviewer') {
|
|
41
|
+
return { ok: false, reason: 'wrong_agent_or_kind', review };
|
|
42
|
+
}
|
|
43
|
+
let implementerActor;
|
|
44
|
+
try {
|
|
45
|
+
implementerActor = await findImplementingActor(specId, projectId);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
/* reliability-optional: TRANSITION_LOG_GATE_READ — typed gate error blocks done */
|
|
49
|
+
reportClassifiedDegradation('TRANSITION_LOG_GATE_READ', err);
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
reason: 'actor_unreadable',
|
|
53
|
+
review,
|
|
54
|
+
readError: err instanceof Error ? err.message : String(err),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (implementerActor !== undefined && implementerActor === review.reviewer.agent) {
|
|
58
|
+
return { ok: false, reason: 'self_review', review, implementingActor: implementerActor };
|
|
59
|
+
}
|
|
60
|
+
return { ok: true, review, implementingActor: implementerActor ?? null };
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
/* reliability-optional: IMPLEMENTATION_REVIEW_GATE_READ — typed gate error blocks done */
|
|
64
|
+
reportClassifiedDegradation('IMPLEMENTATION_REVIEW_GATE_READ', err);
|
|
65
|
+
return {
|
|
66
|
+
ok: false,
|
|
67
|
+
reason: 'unreadable',
|
|
68
|
+
readError: err instanceof Error ? err.message : String(err),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=implementation-review-reader.js.map
|
|
@@ -27,11 +27,9 @@ export const ARCHITECTURE_RULES = {
|
|
|
27
27
|
],
|
|
28
28
|
conventions: [
|
|
29
29
|
'Never put business logic in route handlers',
|
|
30
|
-
'Route handlers should be under 30 lines',
|
|
31
30
|
'Services are framework-agnostic (no NextRequest/NextResponse)',
|
|
32
31
|
'Repositories return plain objects, never ORM entities',
|
|
33
32
|
],
|
|
34
|
-
maxLinesPerHandler: 30,
|
|
35
33
|
},
|
|
36
34
|
express: {
|
|
37
35
|
title: 'Express Architecture',
|
|
@@ -58,7 +56,6 @@ export const ARCHITECTURE_RULES = {
|
|
|
58
56
|
'Services are stateless',
|
|
59
57
|
'One file per resource',
|
|
60
58
|
],
|
|
61
|
-
maxLinesPerHandler: 40,
|
|
62
59
|
},
|
|
63
60
|
fastapi: {
|
|
64
61
|
title: 'FastAPI Architecture',
|
|
@@ -85,34 +82,6 @@ export const ARCHITECTURE_RULES = {
|
|
|
85
82
|
'Pydantic models for all I/O',
|
|
86
83
|
'Async by default',
|
|
87
84
|
],
|
|
88
|
-
maxLinesPerHandler: 30,
|
|
89
|
-
},
|
|
90
|
-
default: {
|
|
91
|
-
title: 'Clean Architecture',
|
|
92
|
-
description: 'Generic clean architecture rules',
|
|
93
|
-
layers: [
|
|
94
|
-
{
|
|
95
|
-
name: 'Handler',
|
|
96
|
-
path: 'src/handlers/',
|
|
97
|
-
responsibility: 'Input/output boundary',
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'Service',
|
|
101
|
-
path: 'src/services/',
|
|
102
|
-
responsibility: 'Business logic',
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
name: 'Repository',
|
|
106
|
-
path: 'src/repositories/',
|
|
107
|
-
responsibility: 'Data access',
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
conventions: [
|
|
111
|
-
'Separate concerns by layer',
|
|
112
|
-
'Dependencies point inward',
|
|
113
|
-
'No framework code in business logic',
|
|
114
|
-
],
|
|
115
|
-
maxLinesPerHandler: 50,
|
|
116
85
|
},
|
|
117
86
|
};
|
|
118
87
|
/** Detect stack from project files. Returns a key into ARCHITECTURE_RULES. */
|
|
@@ -187,9 +156,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
187
156
|
'## Conventions',
|
|
188
157
|
conventions,
|
|
189
158
|
'',
|
|
190
|
-
`## Handler Size Limit`,
|
|
191
|
-
`Max ${rule.maxLinesPerHandler} lines per handler/controller.`,
|
|
192
|
-
'',
|
|
193
159
|
].join('\n');
|
|
194
160
|
}
|
|
195
161
|
case 'copilot': {
|
|
@@ -205,9 +171,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
205
171
|
'## Coding Conventions',
|
|
206
172
|
conventions,
|
|
207
173
|
'',
|
|
208
|
-
`## Size Constraints`,
|
|
209
|
-
`- Max ${rule.maxLinesPerHandler} lines per handler`,
|
|
210
|
-
'',
|
|
211
174
|
].join('\n');
|
|
212
175
|
}
|
|
213
176
|
case 'aider': {
|
|
@@ -223,9 +186,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
223
186
|
'## Conventions',
|
|
224
187
|
conventions,
|
|
225
188
|
'',
|
|
226
|
-
`## Constraints`,
|
|
227
|
-
`- Handlers/controllers: max ${rule.maxLinesPerHandler} lines`,
|
|
228
|
-
'',
|
|
229
189
|
].join('\n');
|
|
230
190
|
}
|
|
231
191
|
case 'claude':
|
|
@@ -244,11 +204,6 @@ export function generateRulesContent(rule, tool) {
|
|
|
244
204
|
'',
|
|
245
205
|
conventions,
|
|
246
206
|
'',
|
|
247
|
-
'## Size Budget',
|
|
248
|
-
'',
|
|
249
|
-
`- Max **${rule.maxLinesPerHandler} lines** per handler/controller`,
|
|
250
|
-
'- Extract helpers when exceeding this limit',
|
|
251
|
-
'',
|
|
252
207
|
].join('\n');
|
|
253
208
|
}
|
|
254
209
|
}
|
|
@@ -273,8 +228,10 @@ function resolveRulesFilePath(projectPath, tool, stackKey) {
|
|
|
273
228
|
/** Main entry: detect stack + tools, generate all rules objects. */
|
|
274
229
|
export function generateProjectRules(projectPath) {
|
|
275
230
|
const stackKey = detectArchitectureStack(projectPath);
|
|
276
|
-
|
|
277
|
-
|
|
231
|
+
if (stackKey === 'default') {
|
|
232
|
+
return [];
|
|
233
|
+
}
|
|
234
|
+
const rule = ARCHITECTURE_RULES[stackKey];
|
|
278
235
|
if (!rule) {
|
|
279
236
|
return [];
|
|
280
237
|
}
|
|
@@ -79,35 +79,24 @@ function buildFrontmatter(entry) {
|
|
|
79
79
|
/** Prepare a built-in skill from the local catalog, generating a SKILL.md. */
|
|
80
80
|
function prepareFromBuiltIn(skillName) {
|
|
81
81
|
const entry = getBuiltInSkillEntry(skillName);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const frontmatter = buildFrontmatter(entry);
|
|
85
|
-
content = [
|
|
86
|
-
frontmatter,
|
|
87
|
-
`# ${entry.name}`,
|
|
88
|
-
``,
|
|
89
|
-
entry.description,
|
|
90
|
-
``,
|
|
91
|
-
`## When to use`,
|
|
92
|
-
``,
|
|
93
|
-
entry.trigger,
|
|
94
|
-
``,
|
|
95
|
-
`## Tags`,
|
|
96
|
-
``,
|
|
97
|
-
entry.tags.join(', '),
|
|
98
|
-
].join('\n');
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
content = [
|
|
102
|
-
`# ${skillName}`,
|
|
103
|
-
``,
|
|
104
|
-
`Built-in Planu skill.`,
|
|
105
|
-
``,
|
|
106
|
-
`## When to use`,
|
|
107
|
-
``,
|
|
108
|
-
`Consult the Planu documentation.`,
|
|
109
|
-
].join('\n');
|
|
82
|
+
if (!entry) {
|
|
83
|
+
throw new Error(`Built-in skill '${skillName}' has no catalog entry; refusing to install a placeholder`);
|
|
110
84
|
}
|
|
85
|
+
const frontmatter = buildFrontmatter(entry);
|
|
86
|
+
const content = [
|
|
87
|
+
frontmatter,
|
|
88
|
+
`# ${entry.name}`,
|
|
89
|
+
``,
|
|
90
|
+
entry.description,
|
|
91
|
+
``,
|
|
92
|
+
`## When to use`,
|
|
93
|
+
``,
|
|
94
|
+
entry.trigger,
|
|
95
|
+
``,
|
|
96
|
+
`## Tags`,
|
|
97
|
+
``,
|
|
98
|
+
entry.tags.join(', '),
|
|
99
|
+
].join('\n');
|
|
111
100
|
return { content, hasScripts: false };
|
|
112
101
|
}
|
|
113
102
|
/** Prepare a skill placeholder for skillssh/agentskill sources (no direct file API yet). */
|
|
@@ -89,15 +89,6 @@ async function fetchSkillsShCatalog(stack, language) {
|
|
|
89
89
|
function buildFallbackSkills(knowledge) {
|
|
90
90
|
const skills = [];
|
|
91
91
|
const stackLower = knowledge.stack.map((s) => s.toLowerCase());
|
|
92
|
-
const lang = knowledge.language.toLowerCase();
|
|
93
|
-
if (lang === technologyValue('technology-typescript-969545') || lang === 'javascript') {
|
|
94
|
-
skills.push({
|
|
95
|
-
name: 'typescript-patterns',
|
|
96
|
-
description: 'TypeScript best practices, strict mode patterns, and type safety conventions',
|
|
97
|
-
installCommand: 'npx skills add typescript-patterns',
|
|
98
|
-
justification: `Project uses ${knowledge.language}`,
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
92
|
if (stackLower.some((s) => s.includes(technologyValue('technology-react-275976')))) {
|
|
102
93
|
skills.push({
|
|
103
94
|
name: 'react-patterns',
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { technologyValue } from './technology-registry.js';
|
|
2
1
|
// engine/skills-reconciler.ts — SPEC-189: Detect stack changes and reconcile skills
|
|
3
2
|
// SPEC-395: autoFixSkills — install missing, remove stale, repair corrupt paths.
|
|
4
3
|
// SPEC-607: Detect old-format skills and new agents, trigger regeneration.
|
|
@@ -32,7 +31,7 @@ export async function reconcileSkills(projectPath, knowledge) {
|
|
|
32
31
|
const newSkillsDetected = recommendedSkills
|
|
33
32
|
.filter((s) => !installedSet.has(s.name))
|
|
34
33
|
.map((s) => s.name);
|
|
35
|
-
const staleSkills = installedNames.filter((name) => !recommendedNames.has(name)
|
|
34
|
+
const staleSkills = installedNames.filter((name) => !recommendedNames.has(name));
|
|
36
35
|
const totalRelevant = recommendedNames.size;
|
|
37
36
|
const alreadyInSync = installedNames.filter((name) => recommendedNames.has(name)).length;
|
|
38
37
|
const healthScore = computeHealthScore(totalRelevant, alreadyInSync, staleSkills.length);
|
|
@@ -167,17 +166,6 @@ function computeHealthScore(totalRelevant, alreadyInSync, staleCount) {
|
|
|
167
166
|
const penalty = staleCount * 5;
|
|
168
167
|
return Math.max(0, base - penalty);
|
|
169
168
|
}
|
|
170
|
-
function isProtectedBuiltInSkill(name, knowledge) {
|
|
171
|
-
if (name !== 'typescript-patterns') {
|
|
172
|
-
return false;
|
|
173
|
-
}
|
|
174
|
-
const language = knowledge.language.toLowerCase();
|
|
175
|
-
const stack = knowledge.stack.map((item) => item.toLowerCase());
|
|
176
|
-
return (language === technologyValue('technology-typescript-969545') ||
|
|
177
|
-
language === 'javascript' ||
|
|
178
|
-
stack.some((item) => item.includes(technologyValue('technology-typescript-969545')) ||
|
|
179
|
-
item.includes('javascript')));
|
|
180
|
-
}
|
|
181
169
|
/** Return a neutral reconciliation when prerequisites are missing. */
|
|
182
170
|
function buildEmptyReconciliation(_reason) {
|
|
183
171
|
return {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Spec, ValidateResult, DefinitionOfReady, DefinitionOfDone, CodeState } from '../../types/index.js';
|
|
1
|
+
import type { Spec, ValidateResult, DefinitionOfReady, DefinitionOfDone, CodeState, VerifiedImplementationReviewResult } from '../../types/index.js';
|
|
2
2
|
export declare function isBlockingQualitySeverity(severity: unknown): boolean;
|
|
3
3
|
/**
|
|
4
4
|
* Generate a Definition of Ready checklist for a spec.
|
|
@@ -7,5 +7,5 @@ export declare function generateDoR(spec: Spec): DefinitionOfReady;
|
|
|
7
7
|
/**
|
|
8
8
|
* Generate a Definition of Done checklist for a spec.
|
|
9
9
|
*/
|
|
10
|
-
export declare function generateDoD(spec: Spec, validationResult?: ValidateResult, codeState?: CodeState, projectPath?: string): Promise<DefinitionOfDone>;
|
|
10
|
+
export declare function generateDoD(spec: Spec, validationResult?: ValidateResult, codeState?: CodeState, projectPath?: string, verifiedReview?: VerifiedImplementationReviewResult): Promise<DefinitionOfDone>;
|
|
11
11
|
//# sourceMappingURL=dor-dod.d.ts.map
|
|
@@ -120,10 +120,11 @@ export function generateDoR(spec) {
|
|
|
120
120
|
generatedAt: new Date().toISOString(),
|
|
121
121
|
};
|
|
122
122
|
}
|
|
123
|
-
function buildDoDItems(spec, score, hasBlockingQualityIssues, hasTestFiles) {
|
|
123
|
+
function buildDoDItems(spec, score, hasBlockingQualityIssues, hasTestFiles, verifiedReview) {
|
|
124
124
|
// If spec is already marked "done", manual gates are implicitly satisfied —
|
|
125
125
|
// the user explicitly transitioned the spec, which implies review/tests/docs passed.
|
|
126
126
|
const isDone = spec.status === 'done';
|
|
127
|
+
const reviewApproved = isDone || verifiedReview?.ok === true;
|
|
127
128
|
return [
|
|
128
129
|
{
|
|
129
130
|
id: 'dod-1',
|
|
@@ -157,7 +158,7 @@ function buildDoDItems(spec, score, hasBlockingQualityIssues, hasTestFiles) {
|
|
|
157
158
|
category: 'review',
|
|
158
159
|
required: true,
|
|
159
160
|
autoCheck: false,
|
|
160
|
-
status:
|
|
161
|
+
status: reviewApproved ? 'passed' : 'pending',
|
|
161
162
|
},
|
|
162
163
|
{
|
|
163
164
|
id: 'dod-5',
|
|
@@ -321,12 +322,12 @@ function gateCategory(gate) {
|
|
|
321
322
|
/**
|
|
322
323
|
* Generate a Definition of Done checklist for a spec.
|
|
323
324
|
*/
|
|
324
|
-
export async function generateDoD(spec, validationResult, codeState, projectPath) {
|
|
325
|
+
export async function generateDoD(spec, validationResult, codeState, projectPath, verifiedReview) {
|
|
325
326
|
const score = validationResult?.score ?? 0;
|
|
326
327
|
const hasBlockingQualityIssues = validationResult?.qualityIssues.some((issue) => isBlockingQualitySeverity(issue.severity)) ??
|
|
327
328
|
false;
|
|
328
329
|
const hasTestFiles = await detectTestFiles(spec, codeState);
|
|
329
|
-
const items = buildDoDItems(spec, score, hasBlockingQualityIssues, hasTestFiles);
|
|
330
|
+
const items = buildDoDItems(spec, score, hasBlockingQualityIssues, hasTestFiles, verifiedReview);
|
|
330
331
|
// Override actuals check with progress.md parsing
|
|
331
332
|
const actualsItem = items.find((i) => i.id === 'dod-8');
|
|
332
333
|
if (actualsItem?.status === 'pending') {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { FailureScenario, Spec } from '../../types/index.js';
|
|
2
|
+
import { type ChallengeCapabilities } from './scenarios-utils.js';
|
|
2
3
|
export declare const EXTERNAL_CALL_KEYWORDS: string[];
|
|
3
4
|
export declare const VALIDATION_KEYWORDS: string[];
|
|
4
5
|
export declare const RATE_LIMIT_KEYWORDS: string[];
|
|
@@ -24,10 +25,7 @@ export declare function hasConcurrencySignal(spec: Spec, specContent: string): b
|
|
|
24
25
|
* Returns true when the spec describes a distributed saga.
|
|
25
26
|
*/
|
|
26
27
|
export declare function hasSagaSignal(spec: Spec, specContent: string): boolean;
|
|
27
|
-
|
|
28
|
-
* Generate validation boundary challenge scenarios.
|
|
29
|
-
*/
|
|
30
|
-
export declare function generateValidationBoundaryScenarios(spec: Spec): FailureScenario[];
|
|
28
|
+
export declare function generateValidationBoundaryScenarios(spec: Spec, capabilities: ChallengeCapabilities): FailureScenario[];
|
|
31
29
|
/**
|
|
32
30
|
* Generate cascade failure challenge scenarios.
|
|
33
31
|
*/
|
|
@@ -97,11 +97,8 @@ export function hasConcurrencySignal(spec, specContent) {
|
|
|
97
97
|
export function hasSagaSignal(spec, specContent) {
|
|
98
98
|
return contentMentions(`${spec.title}\n${spec.tags.join(' ')}\n${specContent}`, SAGA_KEYWORDS);
|
|
99
99
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
*/
|
|
103
|
-
export function generateValidationBoundaryScenarios(spec) {
|
|
104
|
-
return [
|
|
100
|
+
export function generateValidationBoundaryScenarios(spec, capabilities) {
|
|
101
|
+
const scenarios = [
|
|
105
102
|
{
|
|
106
103
|
scenario: `[${spec.id}] Validation Boundary — String Fields: ` +
|
|
107
104
|
'What happens when a required string field receives: (a) empty string "", (b) null, (c) field absent from body entirely, ' +
|
|
@@ -119,7 +116,9 @@ export function generateValidationBoundaryScenarios(spec) {
|
|
|
119
116
|
dataConsistency: 'Accepting empty strings as valid inputs leads to records with meaningless values polluting the database.',
|
|
120
117
|
userExperience: 'Clear per-field error messages with the specific constraint violated improve form UX significantly.',
|
|
121
118
|
},
|
|
122
|
-
|
|
119
|
+
];
|
|
120
|
+
if (capabilities.numericInput) {
|
|
121
|
+
scenarios.push({
|
|
123
122
|
scenario: `[${spec.id}] Validation Boundary — Numeric Fields: ` +
|
|
124
123
|
'What happens when a numeric field receives: (a) value exactly at min boundary (e.g., min=0 → test with -1, 0, 1), ' +
|
|
125
124
|
'(b) value exactly at max boundary, (c) NaN, (d) Infinity or -Infinity, ' +
|
|
@@ -132,24 +131,25 @@ export function generateValidationBoundaryScenarios(spec) {
|
|
|
132
131
|
'Coercion policy must be explicit: either always reject type mismatches or always coerce with documented behavior.',
|
|
133
132
|
dataConsistency: 'NaN or Infinity stored in numeric columns causes silent corruption or runtime exceptions.',
|
|
134
133
|
userExperience: 'Off-by-one errors in validation create confusing UX where users cannot submit valid data.',
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
scenarios.push({
|
|
137
|
+
scenario: `[${spec.id}] Validation Security — Injection Payloads: ` +
|
|
138
|
+
'Does the validation layer handle security-relevant strings without crashing or leaking internals? ' +
|
|
139
|
+
"Test with: SQL injection ('; DROP TABLE users; --), XSS (<script>alert(1)</script>), " +
|
|
140
|
+
'path traversal (../../etc/passwd), null bytes (\\0), template injection ({{7*7}}). ' +
|
|
141
|
+
'Validation is not the primary defense (that is parameterized queries / output encoding), ' +
|
|
142
|
+
'but it must not crash and must not expose stack traces.',
|
|
143
|
+
probability: 'medium',
|
|
144
|
+
impact: 'critical',
|
|
145
|
+
currentHandling: 'Injection payloads may cause unhandled exceptions in validation middleware, exposing internal error details.',
|
|
146
|
+
requiredHandling: 'All string inputs must be treated as untrusted. ' +
|
|
147
|
+
'Validation rejects malformed types; sanitization and parameterized queries handle injection prevention downstream. ' +
|
|
148
|
+
'Error responses must never include the rejected payload or stack trace — return sanitized field name + constraint only.',
|
|
149
|
+
dataConsistency: 'If injection payloads reach the database layer (even as errors), they can leak schema information via error messages.',
|
|
150
|
+
userExperience: 'Stack traces exposed in 400 responses are a security vulnerability — use opaque error IDs instead.',
|
|
151
|
+
});
|
|
152
|
+
return scenarios;
|
|
153
153
|
}
|
|
154
154
|
/**
|
|
155
155
|
* Generate cascade failure challenge scenarios.
|
|
@@ -15,7 +15,7 @@ export function generateResilienceChallengeScenarios(spec, specContent, knowledg
|
|
|
15
15
|
const resolvedCapabilities = capabilities ?? detectChallengeCapabilities(spec, specContent);
|
|
16
16
|
if (resolvedCapabilities.validation &&
|
|
17
17
|
(resolvedCapabilities.userInput || resolvedCapabilities.networkApi)) {
|
|
18
|
-
scenarios.push(...generateValidationBoundaryScenarios(spec));
|
|
18
|
+
scenarios.push(...generateValidationBoundaryScenarios(spec, resolvedCapabilities));
|
|
19
19
|
}
|
|
20
20
|
if (resolvedCapabilities.externalService || resolvedCapabilities.payment) {
|
|
21
21
|
scenarios.push(...generateCascadeFailureScenarios(spec));
|
|
@@ -3,8 +3,7 @@ import { contentMentions, detectChallengeCapabilities } from './scenarios-utils.
|
|
|
3
3
|
export function generateFailureScenarios(spec, content, _knowledge) {
|
|
4
4
|
const scenarios = [];
|
|
5
5
|
const capabilities = detectChallengeCapabilities(spec, content);
|
|
6
|
-
|
|
7
|
-
if (capabilities.networkApi) {
|
|
6
|
+
if (capabilities.outboundDependency) {
|
|
8
7
|
scenarios.push({
|
|
9
8
|
scenario: 'API endpoint becomes unreachable (network timeout)',
|
|
10
9
|
probability: 'medium',
|
|
@@ -24,6 +24,9 @@ export interface ChallengeCapabilities {
|
|
|
24
24
|
rateLimit: boolean;
|
|
25
25
|
saga: boolean;
|
|
26
26
|
platform: boolean;
|
|
27
|
+
outboundDependency: boolean;
|
|
28
|
+
numericInput: boolean;
|
|
29
|
+
rollout: boolean;
|
|
27
30
|
}
|
|
28
31
|
/** Boundary-aware, affirmative replacement for advisory substring checks. */
|
|
29
32
|
export declare function contentMentions(content: string, keywords: string[]): boolean;
|
|
@@ -33,7 +33,7 @@ const CAPABILITY_SIGNALS = {
|
|
|
33
33
|
/\bbatch\s+upload\b/i,
|
|
34
34
|
],
|
|
35
35
|
scale: [
|
|
36
|
-
/\b(?:traffic|load|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
|
|
36
|
+
/\b(?:traffic|load\b(?![\s‐-―-]+bearing)|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
|
|
37
37
|
/\b(?:auto[ -]?scal|load\s+test|capacity\s+plan)/i,
|
|
38
38
|
/\b(?:data\s+growth|growing\s+dataset|query\s+performance\s+at\s+scale)\b/i,
|
|
39
39
|
],
|
|
@@ -87,6 +87,22 @@ const CAPABILITY_SIGNALS = {
|
|
|
87
87
|
platform: [
|
|
88
88
|
/\b(?:smart\s+contract|solidity|discord\s+bot|telegram\s+bot|iot\s+device|firmware|infrastructure\s+as\s+code|terraform|machine\s+learning\s+model)\b/i,
|
|
89
89
|
],
|
|
90
|
+
outboundDependency: [
|
|
91
|
+
/\boutbound\s+(?:http\s+)?(?:client|call|request|dependency)\b/i,
|
|
92
|
+
/\b(?:fetch(?:es|ing)?|calls?|invokes?)\b(?:\W+\w+){0,4}?\W+(?:api|endpoint|service|dependency)\b/i,
|
|
93
|
+
/\bremote\s+dependency\s+call\b/i,
|
|
94
|
+
],
|
|
95
|
+
numericInput: [
|
|
96
|
+
/\bnumeric\s+(?:field|input|value|schema|constraint)\b/i,
|
|
97
|
+
/\b(?:integer|float|decimal)\s+field\b/i,
|
|
98
|
+
/\b(?:min|max)(?:imum)?\s+(?:value|constraint|boundary)\b/i,
|
|
99
|
+
/\bz\.number\(/i,
|
|
100
|
+
],
|
|
101
|
+
rollout: [
|
|
102
|
+
/\b(?:canary|phased\s+rollout|gradual\s+rollout|blue-green\s+deploy(?:ment)?|rolling\s+deployment|feature\s+flag)\b/i,
|
|
103
|
+
/\broll(?:s|ed|ing)?\s+out\b(?:\W+\w+){0,4}?\W+(?:in\s+phases|gradually)\b/i,
|
|
104
|
+
/\bdeployment\s+strategy\b/i,
|
|
105
|
+
],
|
|
90
106
|
};
|
|
91
107
|
const AUTHENTICATION_BARE_TOKEN_RE = /\bauth(?:entication|enticate)?\b/i;
|
|
92
108
|
// A bare "auth" token next to registry/package-manager vocabulary describes a
|
|
@@ -9,7 +9,7 @@ export interface ConcurrencyAnalysisResult extends ConcurrencyAnalysis {
|
|
|
9
9
|
suppressedCount: number;
|
|
10
10
|
}
|
|
11
11
|
export declare function generateConcurrencyAnalysis(_spec: Spec, content: string, _knowledge: ProjectKnowledge): ConcurrencyAnalysisResult;
|
|
12
|
-
export declare function buildScalabilityAssessment(spec: Spec, knowledge: ProjectKnowledge, scenarios: FailureScenario[]): string;
|
|
12
|
+
export declare function buildScalabilityAssessment(spec: Spec, knowledge: ProjectKnowledge, scenarios: FailureScenario[], hasRolloutEvidence?: boolean): string;
|
|
13
13
|
export declare function calculateOverallRisk(scenarios: FailureScenario[], concurrency: ConcurrencyAnalysis): RiskLevel;
|
|
14
14
|
export declare function readSpecContent(spec: Spec): Promise<string>;
|
|
15
15
|
//# sourceMappingURL=challenge-spec-helpers.d.ts.map
|
|
@@ -93,7 +93,7 @@ export function generateConcurrencyAnalysis(_spec, content, _knowledge) {
|
|
|
93
93
|
return { hotPaths, raceConditions, sharedState, recommendations, suppressedCount };
|
|
94
94
|
}
|
|
95
95
|
// --- Scalability and risk ---
|
|
96
|
-
export function buildScalabilityAssessment(spec, knowledge, scenarios) {
|
|
96
|
+
export function buildScalabilityAssessment(spec, knowledge, scenarios, hasRolloutEvidence = false) {
|
|
97
97
|
const criticalCount = scenarios.filter((s) => s.impact === 'critical').length;
|
|
98
98
|
const arch = knowledge.architecture.primary;
|
|
99
99
|
const parts = [];
|
|
@@ -108,7 +108,10 @@ export function buildScalabilityAssessment(spec, knowledge, scenarios) {
|
|
|
108
108
|
parts.push('No critical scalability concerns for the current scope.');
|
|
109
109
|
}
|
|
110
110
|
if (spec.scope === 'architectural' || spec.scope === 'cross-module') {
|
|
111
|
-
parts.push('Cross-module scope increases blast radius.
|
|
111
|
+
parts.push('Cross-module scope increases blast radius.');
|
|
112
|
+
if (hasRolloutEvidence) {
|
|
113
|
+
parts.push('Consider phased rollout with feature flags.');
|
|
114
|
+
}
|
|
112
115
|
}
|
|
113
116
|
return parts.join(' ');
|
|
114
117
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Analyzes a spec from adversarial perspectives: failure scenarios,
|
|
3
3
|
// concurrency issues, scale limits, security holes, and data consistency.
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
5
6
|
import { specStore, knowledgeStore } from '../storage/index.js';
|
|
6
7
|
// SPEC-1011 Bug F: fallback resolver using disk fingerprints
|
|
7
8
|
import { resolveProjectFromPath } from '../storage/project-resolver.js';
|
|
@@ -29,6 +30,14 @@ const ALL_FOCUS_AREAS = [
|
|
|
29
30
|
'security',
|
|
30
31
|
'data-consistency',
|
|
31
32
|
];
|
|
33
|
+
function truncateCriterionDiscriminator(criterion) {
|
|
34
|
+
const codePoints = Array.from(criterion);
|
|
35
|
+
if (codePoints.length <= 80) {
|
|
36
|
+
return criterion;
|
|
37
|
+
}
|
|
38
|
+
const digest = createHash('sha256').update(criterion).digest('hex').slice(0, 8);
|
|
39
|
+
return `${codePoints.slice(0, 79).join('')}… [${digest}]`;
|
|
40
|
+
}
|
|
32
41
|
function extractOutOfScopeSection(content) {
|
|
33
42
|
const headingPattern = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
|
|
34
43
|
const fencePattern = /^[ \t]{0,3}(`{3,}|~{3,})/;
|
|
@@ -224,7 +233,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
224
233
|
const contradictionResult = checkScopeContradictions(resolvedOutOfScope, criteriaTexts);
|
|
225
234
|
for (const c of contradictionResult.contradictions) {
|
|
226
235
|
failureScenarios.push({
|
|
227
|
-
scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}"`,
|
|
236
|
+
scenario: `Criterion contradicts outOfScope declaration: "${c.contradicts}" (criterion: "${truncateCriterionDiscriminator(c.criterion)}")`,
|
|
228
237
|
probability: 'medium',
|
|
229
238
|
impact: 'low',
|
|
230
239
|
currentHandling: `Criterion: "${c.criterion}"`,
|
|
@@ -259,7 +268,7 @@ export async function handleChallengeSpec(args, server) {
|
|
|
259
268
|
const actionableFailureScenarios = failureScenarios.filter((scenario) => isScenarioSupportedByCapabilities(scenario, capabilities));
|
|
260
269
|
const suppressedScenarioCount = failureScenarios.length - actionableFailureScenarios.length;
|
|
261
270
|
// 7. Build scalability assessment from grounded scenarios only.
|
|
262
|
-
const scalabilityAssessment = buildScalabilityAssessment(spec, knowledge, actionableFailureScenarios);
|
|
271
|
+
const scalabilityAssessment = buildScalabilityAssessment(spec, knowledge, actionableFailureScenarios, capabilities.rollout);
|
|
263
272
|
// 8. Calculate overall risk from grounded scenarios only.
|
|
264
273
|
const overallRisk = calculateOverallRisk(actionableFailureScenarios, concurrencyAnalysis);
|
|
265
274
|
// 9. Compute relevance scores and select top-3 diagnostics (SPEC-338) — retained as-is
|
|
@@ -456,7 +465,10 @@ function isScenarioSupportedByCapabilities(scenario, capabilities) {
|
|
|
456
465
|
],
|
|
457
466
|
[/\b(?:sql|nosql|database|connection pool)\b/, capabilities.database],
|
|
458
467
|
[/\b(?:event contract|event schema|message broker|kafka|rabbitmq|dlq)\b/, capabilities.events],
|
|
459
|
-
[
|
|
468
|
+
[
|
|
469
|
+
/\b(?:api endpoint|http route|http status|network timeout)\b/,
|
|
470
|
+
capabilities.outboundDependency,
|
|
471
|
+
],
|
|
460
472
|
[/\b(?:traffic spike|load test|auto-scaling|throughput)\b/, capabilities.scale],
|
|
461
473
|
];
|
|
462
474
|
return rules.every(([pattern, supported]) => !pattern.test(haystack) || supported);
|
|
@@ -1335,8 +1335,26 @@ async function prepareCreateSpecCandidate(initialParams, server) {
|
|
|
1335
1335
|
},
|
|
1336
1336
|
};
|
|
1337
1337
|
}
|
|
1338
|
+
function isNonEmptyString(value) {
|
|
1339
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
1340
|
+
}
|
|
1341
|
+
function buildMissingTitleResult() {
|
|
1342
|
+
return {
|
|
1343
|
+
content: [
|
|
1344
|
+
{
|
|
1345
|
+
type: 'text',
|
|
1346
|
+
text: 'create_spec requires a non-empty title. Provide the title field and call create_spec again.',
|
|
1347
|
+
},
|
|
1348
|
+
],
|
|
1349
|
+
isError: true,
|
|
1350
|
+
structuredContent: { error: 'MISSING_TITLE', code: 'INVALID_INPUT', field: 'title' },
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1338
1353
|
// eslint-disable-next-line max-lines-per-function
|
|
1339
1354
|
export async function handleCreateSpec(inputParams, server) {
|
|
1355
|
+
if (!isNonEmptyString(inputParams.title)) {
|
|
1356
|
+
return buildMissingTitleResult();
|
|
1357
|
+
}
|
|
1340
1358
|
const enumValidation = validateCreateSpecEnums(inputParams);
|
|
1341
1359
|
if (enumValidation) {
|
|
1342
1360
|
return enumValidation;
|