@planu/cli 4.11.0 → 4.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (116) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/engine/cascade-hooks/registry.js +0 -2
  3. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  4. package/dist/engine/compliance-injector.d.ts +3 -3
  5. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  6. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  7. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  8. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  9. package/dist/engine/elicitation/answer-extractor.js +197 -68
  10. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  11. package/dist/engine/elicitation/index.d.ts +0 -1
  12. package/dist/engine/elicitation/index.js +0 -1
  13. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  14. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  15. package/dist/engine/evidence-index/index-builder.js +7 -2
  16. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  17. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  18. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  19. package/dist/engine/human-summary.js +3 -3
  20. package/dist/engine/implementation-contract/common.d.ts +1 -1
  21. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  22. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  23. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  24. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  25. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  26. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  27. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  28. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  29. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  30. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  31. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  32. package/dist/engine/text-signal-boundaries.js +45 -12
  33. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  34. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  35. package/dist/engine/workers/schema.d.ts +3 -3
  36. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  37. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  38. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  39. package/dist/server/openapi-spec.d.ts +132 -132
  40. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  41. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  42. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  43. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  44. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  45. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  46. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  47. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  48. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  49. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  50. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  51. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  52. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  53. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  54. package/dist/tools/challenge-spec.js +39 -118
  55. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  56. package/dist/tools/create-spec.js +4 -24
  57. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  58. package/dist/tools/generate-batch-script.d.ts +2 -2
  59. package/dist/tools/init-project/rules-generator.js +0 -1
  60. package/dist/tools/oauth-handler.d.ts +1 -1
  61. package/dist/tools/register-sdd-tools.d.ts +1 -1
  62. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  63. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  64. package/dist/tools/schemas/agents.d.ts +7 -7
  65. package/dist/tools/schemas/analysis.d.ts +11 -11
  66. package/dist/tools/schemas/delete.d.ts +5 -5
  67. package/dist/tools/schemas/github.d.ts +4 -4
  68. package/dist/tools/schemas/ide-config.d.ts +12 -12
  69. package/dist/tools/schemas/infra.d.ts +9 -9
  70. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  71. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  72. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  73. package/dist/tools/schemas/project-graph.d.ts +6 -6
  74. package/dist/tools/schemas/rbac.d.ts +2 -2
  75. package/dist/tools/schemas/registry.d.ts +3 -3
  76. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  77. package/dist/tools/schemas/session.d.ts +2 -2
  78. package/dist/tools/schemas/spec.d.ts +15 -15
  79. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  80. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  81. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  82. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  83. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  84. package/dist/tools/triage-request.d.ts +3 -3
  85. package/dist/tools/update-status/file-sync.d.ts +1 -1
  86. package/dist/tools/update-status/file-sync.js +10 -45
  87. package/dist/tools/update-status/index.js +28 -27
  88. package/dist/tools/update-status/response-builder.d.ts +1 -1
  89. package/dist/tools/update-status/response-builder.js +11 -19
  90. package/dist/tools/update-status/transition-guard.js +1 -1
  91. package/dist/tools/update-status-actions.d.ts +0 -1
  92. package/dist/tools/update-status-actions.js +10 -26
  93. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  94. package/dist/tools/update-status-convention-gate.js +9 -33
  95. package/dist/types/docs.d.ts +0 -4
  96. package/dist/types/env.d.ts +0 -4
  97. package/dist/types/estimation.d.ts +0 -2
  98. package/dist/types/multi-teammate-review.d.ts +4 -4
  99. package/dist/types/plugin-install.d.ts +7 -7
  100. package/package.json +21 -18
  101. package/planu-native.json +1 -1
  102. package/planu-plugin.json +1 -1
  103. package/src/i18n/messages/en.json +0 -72
  104. package/src/i18n/messages/es.json +0 -72
  105. package/src/i18n/messages/pt.json +0 -72
  106. package/dist/config/elicitation-dimensions.json +0 -51
  107. package/dist/engine/actuals-estimator.d.ts +0 -14
  108. package/dist/engine/actuals-estimator.js +0 -64
  109. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  110. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  111. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  112. package/dist/engine/elicitation/option-builder.js +0 -85
  113. package/dist/engine/progress-writer.d.ts +0 -10
  114. package/dist/engine/progress-writer.js +0 -63
  115. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  116. package/dist/tools/update-status/mode-hints.js +0 -24
@@ -1,5 +1,5 @@
1
1
  // tools/challenge-spec/scenarios-security.ts — Authentication, injection, and CSRF scenarios
2
- import { contentMentions, hasAnyAffirmedMatch } from './scenarios-utils.js';
2
+ import { contentMentions, detectChallengeCapabilities, hasAnyAffirmedMatch, } from './scenarios-utils.js';
3
3
  const AUTH_SIGNALS = [
4
4
  /\b(auth|authentication|login|session|token|jwt|oauth|permission|role|rbac)\b/i,
5
5
  ];
@@ -9,10 +9,11 @@ const INPUT_SIGNALS = [
9
9
  const STATE_CHANGING_WEB_SIGNALS = [
10
10
  /\b(form submit|post request|put request|delete request|mutation|state-changing|cookie auth)\b/i,
11
11
  ];
12
- export function generateSecurityScenarios(_spec, content, _knowledge) {
12
+ export function generateSecurityScenarios(spec, content, _knowledge) {
13
13
  const scenarios = [];
14
14
  const lower = content.toLowerCase();
15
15
  const text = content;
16
+ const capabilities = detectChallengeCapabilities(spec, content);
16
17
  // Authentication bypass
17
18
  if (hasAnyAffirmedMatch(text, AUTH_SIGNALS)) {
18
19
  scenarios.push({
@@ -22,23 +23,29 @@ export function generateSecurityScenarios(_spec, content, _knowledge) {
22
23
  currentHandling: contentMentions(lower, ['auth', 'jwt', 'session', 'middleware'])
23
24
  ? 'Authentication mentioned'
24
25
  : 'No explicit auth handling',
25
- requiredHandling: 'Validate auth tokens on every request. Implement RBAC. Never trust client-side auth state.',
26
- dataConsistency: 'Ensure RLS policies are enabled. No data leak through unauthorized queries.',
27
- userExperience: 'Redirect to login on 401. Clear error messages without leaking implementation details.',
26
+ requiredHandling: 'Validate identity at the declared trust boundary and enforce the authorization model named by the contract.',
27
+ dataConsistency: 'Unauthorized operations must not read or mutate protected state.',
28
+ userExperience: 'Use the unauthorized-access behavior defined by the client contract.',
28
29
  });
29
30
  }
30
31
  // Injection attacks
31
32
  if (hasAnyAffirmedMatch(text, INPUT_SIGNALS)) {
32
33
  scenarios.push({
33
- scenario: 'SQL injection / XSS / command injection via user input',
34
+ scenario: capabilities.database
35
+ ? 'SQL injection / XSS / command injection via user input'
36
+ : 'XSS / command injection via user input',
34
37
  probability: 'high',
35
38
  impact: 'critical',
36
39
  currentHandling: contentMentions(lower, ['sanitiz', 'escap', 'parameterized', 'prepared'])
37
40
  ? 'Some sanitization mentioned'
38
41
  : 'Not explicitly addressed',
39
- requiredHandling: 'Use parameterized queries (never string concatenation). Sanitize HTML output. Use CSP headers.',
40
- dataConsistency: 'Injection can corrupt or exfiltrate data. Use least-privilege DB connections.',
41
- userExperience: 'Transparent to user. Invalid input is silently sanitized.',
42
+ requiredHandling: capabilities.database
43
+ ? 'Use parameterized queries and sanitize output at the declared input boundary.'
44
+ : 'Sanitize output and constrain command execution at the declared input boundary.',
45
+ dataConsistency: capabilities.database
46
+ ? 'Use least-privilege database access for data reached through this input.'
47
+ : 'Invalid input must not reach state-changing operations.',
48
+ userExperience: 'Use the invalid-input response defined by the interaction contract.',
42
49
  });
43
50
  }
44
51
  // CSRF / rate limiting only applies when the spec explicitly involves auth or
@@ -52,9 +59,9 @@ export function generateSecurityScenarios(_spec, content, _knowledge) {
52
59
  currentHandling: contentMentions(lower, ['csrf', 'rate limit', 'throttle', 'captcha'])
53
60
  ? 'Some protection mentioned'
54
61
  : 'Not addressed',
55
- requiredHandling: 'Implement CSRF tokens for state-changing operations. Rate-limit sensitive endpoints. Add CAPTCHA for auth endpoints.',
62
+ requiredHandling: 'Define request-forgery and abuse controls from the authentication and mutation contracts.',
56
63
  dataConsistency: 'Prevent mass data modifications from automated attacks.',
57
- userExperience: 'Show rate limit message. Require CAPTCHA after N failed attempts.',
64
+ userExperience: 'Use the throttling and verification behavior defined by the auth contract.',
58
65
  });
59
66
  }
60
67
  return scenarios;
@@ -1,7 +1,33 @@
1
- /**
2
- * Returns true if any of the keywords appear in lowerContent (case-insensitive match).
3
- * lowerContent is expected to already be lowercased by the caller.
4
- */
5
- export declare function contentMentions(lowerContent: string, keywords: string[]): boolean;
1
+ import type { Spec } from '../../types/index.js';
2
+ export interface ChallengeCapabilities {
3
+ networkApi: boolean;
4
+ database: boolean;
5
+ authentication: boolean;
6
+ authorization: boolean;
7
+ userInput: boolean;
8
+ externalService: boolean;
9
+ payment: boolean;
10
+ fileTransfer: boolean;
11
+ scale: boolean;
12
+ dataWrite: boolean;
13
+ cache: boolean;
14
+ duplicateOperation: boolean;
15
+ concurrency: boolean;
16
+ privacy: boolean;
17
+ events: boolean;
18
+ implementationContract: boolean;
19
+ desktopUpdate: boolean;
20
+ agent: boolean;
21
+ ddd: boolean;
22
+ xp: boolean;
23
+ validation: boolean;
24
+ rateLimit: boolean;
25
+ saga: boolean;
26
+ platform: boolean;
27
+ }
28
+ /** Boundary-aware, affirmative replacement for advisory substring checks. */
29
+ export declare function contentMentions(content: string, keywords: string[]): boolean;
30
+ export declare function detectChallengeCapabilities(spec: Spec, specContent: string): ChallengeCapabilities;
31
+ export declare function hasAnyChallengeCapability(capabilities: ChallengeCapabilities, names: (keyof ChallengeCapabilities)[]): boolean;
6
32
  export { hasAffirmedMatch, hasAnyAffirmedMatch, stripNonContractText, } from '../../engine/text-signal-boundaries.js';
7
33
  //# sourceMappingURL=scenarios-utils.d.ts.map
@@ -1,10 +1,106 @@
1
1
  // tools/challenge-spec/scenarios-utils.ts — Shared utilities for scenario generators
2
- /**
3
- * Returns true if any of the keywords appear in lowerContent (case-insensitive match).
4
- * lowerContent is expected to already be lowercased by the caller.
5
- */
6
- export function contentMentions(lowerContent, keywords) {
7
- return keywords.some((kw) => lowerContent.includes(kw.toLowerCase()));
2
+ import { hasAnyAffirmedMatch } from '../../engine/text-signal-boundaries.js';
3
+ const CAPABILITY_SIGNALS = {
4
+ networkApi: [
5
+ /(?<!compiler\s)\bapi\b(?!\s+(?:compatibility|surface|package|type))/i,
6
+ /\b(?:public|external|remote|rest|http)\s+api\b/i,
7
+ /\bapi\s+(?:endpoint|route|request|response|server|client)\b/i,
8
+ /\b(?:http|rest)\s+(?:endpoint|route|request|response)\b/i,
9
+ /\bfetch(?:es|ing)?\s+(?:from\s+)?(?:an?\s+)?(?:endpoint|service|url)\b/i,
10
+ ],
11
+ database: [
12
+ /\b(?:database|postgres(?:ql)?|mysql|sqlite|mongodb|dynamodb|supabase)\b/i,
13
+ /\b(?:sql|nosql)\s+(?:query|statement|table|database)\b/i,
14
+ /\b(?:database|persistence)\s+(?:query|schema|table|migration|transaction)\b/i,
15
+ /\b(?:orm|prisma|drizzle|typeorm|sequelize)\b/i,
16
+ ],
17
+ authentication: [
18
+ /\b(?:auth(?:entication)?|login|sign[ -]?in|password|jwt|oauth2?|session\s+auth)\b/i,
19
+ ],
20
+ authorization: [
21
+ /\b(?:authori[sz]ation|permission|rbac|role-based|access\s+control|resource\s+ownership|tenant)\b/i,
22
+ ],
23
+ userInput: [
24
+ /\b(?:user|client)\s+input\b/i,
25
+ /\b(?:input\s+form|form\s+(?:input|submission)|request\s+body|query\s+parameter|uploaded\s+payload)\b/i,
26
+ /\binputs?\s+(?:are\s+)?validat/i,
27
+ ],
28
+ externalService: [
29
+ /\b(?:third-party|external|remote|downstream)\s+(?:service|provider|dependency|system)\b/i,
30
+ /\bwebhook\s+(?:provider|delivery|endpoint|request)\b/i,
31
+ ],
32
+ payment: [/\b(?:payment|billing|checkout|invoice|stripe|paypal)\b/i],
33
+ fileTransfer: [
34
+ /\b(?:(?:large|binary|uploaded)\s+)?(?:file|image|media|attachment)\s+(?:upload|download|transfer|stream)\b/i,
35
+ /\b(?:upload|download|stream)(?:ed|ing|s)?\s+(?:a\s+)?(?:(?:large|binary)\s+)?(?:file|image|media|attachment)\b/i,
36
+ /\bbatch\s+(?:import|export|processing|operation)\b/i,
37
+ /\bbatch\s+upload\b/i,
38
+ ],
39
+ scale: [
40
+ /\b(?:traffic|load|throughput|requests?\s+per\s+second|concurrent\s+users?)\b/i,
41
+ /\b(?:auto[ -]?scal|load\s+test|capacity\s+plan)/i,
42
+ /\b(?:data\s+growth|growing\s+dataset|query\s+performance\s+at\s+scale)\b/i,
43
+ ],
44
+ dataWrite: [
45
+ /\b(?:database\s+transaction|persist(?:s|ed|ing)?\s+(?:record|entity|data)|write(?:s|ing)?\s+(?:record|entity|database))\b/i,
46
+ /\b(?:state-changing|data\s+mutation|atomic\s+database\s+write)\b/i,
47
+ /\b(?:transaction|atomic\s+write|update\s+(?:record|state)|modify\s+(?:record|state))\b/i,
48
+ ],
49
+ cache: [/\b(?:cache|caching|cached\s+(?:data|response)|redis)\b/i],
50
+ duplicateOperation: [
51
+ /\b(?:double[ -]?click|duplicate\s+(?:request|submission|operation|record)|idempoten)/i,
52
+ ],
53
+ concurrency: [
54
+ /\b(?:concurren|race\s+condition|shared\s+state|parallel\s+write|atomic\s+increment|optimistic\s+lock)/i,
55
+ /\b(?:counter|increment|balance|stock|quantity|vote|status\s+(?:update|transition))\b/i,
56
+ /\b(?:file\s+upload|resource\s+manager|search\s+feed|list\s+endpoint|global\s+cache|session\s+state|cart\s+state|draft\s+state)\b/i,
57
+ ],
58
+ privacy: [
59
+ /\b(?:pii|personal\s+data|personal\s+information|privacy|gdpr|hipaa|data\s+subject)\b/i,
60
+ /\b(?:email\s+address|phone\s+number|social\s+security\s+number)\b/i,
61
+ ],
62
+ events: [
63
+ /\b(?:process|handle|publish|consume)\w*(?:\s+[a-z0-9_-]+){0,3}\s+event\b/i,
64
+ /\b(?:event\s+(?:schema|contract|producer|consumer|handler|stream)|message\s+(?:broker|queue|consumer|producer))\b/i,
65
+ /\b(?:kafka|rabbitmq|pubsub|nats|eventbridge|sqs|sns|dead[ -]?letter\s+queue|dlq)\b/i,
66
+ /\b(?:publish|produce|consume)(?:es|d|r|rs|ing)?\s+(?:an?\s+)?(?:event|message)\b/i,
67
+ ],
68
+ implementationContract: [
69
+ /\bimplementation\s+contract\b/i,
70
+ /\b(?:file-level\s+work\s+plan|acceptance-to-verification\s+map|behavior\s+contract)\b/i,
71
+ ],
72
+ desktopUpdate: [
73
+ /\b(?:desktop|electron|tauri)\s+(?:app\s+)?(?:auto[ -]?)?update\b/i,
74
+ /\b(?:auto[ -]?updater|electron-updater)\b/i,
75
+ ],
76
+ agent: [/\b(?:ai\s+agent|agent\s+contract|llm\s+agent|tool\s+call|autonomous\s+agent)\b/i],
77
+ ddd: [/\b(?:domain-driven\s+design|ddd\s+aggregate|aggregate\s+root|bounded\s+context)\b/i],
78
+ xp: [/\b(?:pair\s+programming|extreme\s+programming|xp\s+pair)\b/i],
79
+ validation: [/\b(?:input|payload|request|schema)\s+validat/i, /\b(?:zod|joi|json\s+schema)\b/i],
80
+ rateLimit: [/\b(?:rate[ -]?limit|throttl|request\s+quota)\b/i],
81
+ saga: [/\b(?:saga|compensating\s+transaction|distributed\s+transaction)\b/i],
82
+ platform: [
83
+ /\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,
84
+ ],
85
+ };
86
+ function keywordPattern(keyword) {
87
+ const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '\\s+');
88
+ const suffix = /^[a-z0-9]+$/i.test(keyword) && keyword.length > 3 ? '[a-z0-9_-]*' : '';
89
+ return new RegExp(`\\b${escaped}${suffix}\\b`, 'i');
90
+ }
91
+ /** Boundary-aware, affirmative replacement for advisory substring checks. */
92
+ export function contentMentions(content, keywords) {
93
+ return hasAnyAffirmedMatch(content, keywords.map(keywordPattern));
94
+ }
95
+ export function detectChallengeCapabilities(spec, specContent) {
96
+ const text = `${spec.title}\n${spec.tags.join(' ')}\n${specContent}`;
97
+ return Object.fromEntries(Object.entries(CAPABILITY_SIGNALS).map(([capability, patterns]) => [
98
+ capability,
99
+ hasAnyAffirmedMatch(text, patterns),
100
+ ]));
101
+ }
102
+ export function hasAnyChallengeCapability(capabilities, names) {
103
+ return names.some((name) => capabilities[name]);
8
104
  }
9
105
  export { hasAffirmedMatch, hasAnyAffirmedMatch, stripNonContractText, } from '../../engine/text-signal-boundaries.js';
10
106
  //# sourceMappingURL=scenarios-utils.js.map
@@ -3,7 +3,7 @@ import type { FailureScenario, Spec, ProjectKnowledge } from '../../types/index.
3
3
  * Generate security challenge scenarios specific to authorization, STRIDE threats,
4
4
  * and resource ownership. Used by handleChallengeSpec in the 'security' focus area.
5
5
  */
6
- export declare function generateSecurityChallengeScenarios(spec: Spec, _specContent: string, _knowledge: ProjectKnowledge): FailureScenario[];
6
+ export declare function generateSecurityChallengeScenarios(spec: Spec, specContent: string, _knowledge: ProjectKnowledge): FailureScenario[];
7
7
  /**
8
8
  * Generate security test case scaffolds for auth/API features.
9
9
  * SPEC-029 criterion 12: auto-generate security test scaffolds.
@@ -1,12 +1,13 @@
1
1
  // tools/challenge-spec/security-challenge-scenarios.ts — Security challenges for challenge_spec
2
2
  // SPEC-029 criteria 11, 24: authorization challenges and STRIDE-based challenges
3
3
  import { detectSecurityProfiles } from '../../engine/security-analyzer.js';
4
+ import { hasAnyAffirmedMatch } from './scenarios-utils.js';
4
5
  /**
5
6
  * Generate security challenge scenarios specific to authorization, STRIDE threats,
6
7
  * and resource ownership. Used by handleChallengeSpec in the 'security' focus area.
7
8
  */
8
- export function generateSecurityChallengeScenarios(spec, _specContent, _knowledge) {
9
- const profiles = detectSecurityProfiles(spec.title, '', spec.tags);
9
+ export function generateSecurityChallengeScenarios(spec, specContent, _knowledge) {
10
+ const profiles = detectSecurityProfiles(spec.title, specContent, spec.tags);
10
11
  const scenarios = [];
11
12
  // Authorization challenge (criterion 11): resource ownership
12
13
  if (profiles.includes('public-api') || profiles.includes('db-access')) {
@@ -16,15 +17,14 @@ export function generateSecurityChallengeScenarios(spec, _specContent, _knowledg
16
17
  impact: 'critical',
17
18
  currentHandling: 'Authorization check may be missing at the data access layer — only route-level guards present.',
18
19
  requiredHandling: 'Implement object-level authorization on every data access operation. ' +
19
- 'Never trust client-provided resource IDs without verifying ownership. ' +
20
- 'Use indirect references (UUIDs) instead of sequential IDs.',
21
- dataConsistency: 'Unauthorized access should return 403, never 404 that leaks resource existence.',
22
- userExperience: 'Return consistent 403 Forbidden for unauthorized access — do not reveal if the resource exists.',
20
+ 'Never trust client-provided resource identifiers without verifying ownership using the declared authorization model.',
21
+ dataConsistency: 'Unauthorized access must not reveal resource existence.',
22
+ userExperience: 'Use the authorization failure response defined by the API contract.',
23
23
  });
24
24
  }
25
25
  // JWT-specific challenges
26
- const lower = `${spec.title} ${spec.tags.join(' ')}`.toLowerCase();
27
- const hasJwt = lower.includes('jwt') || lower.includes('token') || lower.includes('auth');
26
+ const signalText = `${spec.title}\n${spec.tags.join(' ')}\n${specContent}`;
27
+ const hasJwt = hasAnyAffirmedMatch(signalText, [/\b(?:jwt|json\s+web\s+token)\b/i]);
28
28
  if (hasJwt) {
29
29
  scenarios.push({
30
30
  scenario: `[${spec.id}] JWT Algorithm Confusion Attack: attacker changes JWT header algorithm to "none" or switches RS256 to HS256. Server accepts forged token if it trusts the header.`,
@@ -34,17 +34,16 @@ export function generateSecurityChallengeScenarios(spec, _specContent, _knowledg
34
34
  requiredHandling: 'Explicitly configure the allowed algorithm(s) in JWT verification. ' +
35
35
  'Never use the algorithm from the token header. Reject tokens with "none" algorithm.',
36
36
  dataConsistency: 'Invalid tokens must never reach business logic — reject at middleware boundary.',
37
- userExperience: 'Return 401 Unauthorized for invalid tokens do not expose algorithm validation details.',
37
+ userExperience: 'Use the invalid-token response defined by the authentication contract.',
38
38
  });
39
39
  scenarios.push({
40
- scenario: `[${spec.id}] Expired Token Access: user presents an expired JWT. Does the API return 401 immediately, or does expired token reach business logic?`,
40
+ scenario: `[${spec.id}] Expired Token Access: user presents an expired JWT. Does the declared authentication failure occur before business logic?`,
41
41
  probability: 'medium',
42
42
  impact: 'high',
43
43
  currentHandling: 'Expiry check may not be the first middleware step — race conditions possible.',
44
- requiredHandling: 'Verify token expiry (exp claim) as the first step in auth middleware. ' +
45
- 'Return 401 with error code "token_expired" so clients can trigger refresh flow.',
44
+ requiredHandling: 'Verify token expiry before business logic and use the expiration result defined by the authentication contract.',
46
45
  dataConsistency: 'Expired tokens must never modify state — check before any write operations.',
47
- userExperience: 'Return structured error { "error": "token_expired" } so clients can refresh automatically.',
46
+ userExperience: 'Use the expiration and refresh behavior defined by the client contract.',
48
47
  });
49
48
  }
50
49
  // SQL injection challenges for DB features
@@ -62,28 +61,24 @@ export function generateSecurityChallengeScenarios(spec, _specContent, _knowledg
62
61
  });
63
62
  }
64
63
  // Role-based access challenges
65
- const hasRoles = lower.includes('role') ||
66
- lower.includes('admin') ||
67
- lower.includes('permission') ||
68
- lower.includes('rbac');
64
+ const hasRoles = hasAnyAffirmedMatch(signalText, [
65
+ /\b(?:role|admin|permission|rbac|access\s+control)\b/i,
66
+ ]);
69
67
  if (hasRoles) {
70
68
  scenarios.push({
71
69
  scenario: `[${spec.id}] Privilege Escalation via Role Manipulation: user with role "viewer" calls admin endpoint, or modifies JWT payload claiming role "admin". Role enforcement missing at middleware.`,
72
70
  probability: 'medium',
73
71
  impact: 'critical',
74
72
  currentHandling: 'Role enforcement may be per-handler instead of middleware — inconsistent coverage.',
75
- requiredHandling: 'Enforce RBAC in middleware, not per-handler. ' +
76
- 'Never derive roles from user-supplied data; always load from a trusted store. ' +
77
- 'Log unauthorized access attempts for monitoring.',
73
+ requiredHandling: 'Enforce the declared role model consistently before protected behavior, load roles from the trusted identity boundary, and record the audit evidence required by the contract.',
78
74
  dataConsistency: 'Admin operations that modify state must verify role before any data mutation.',
79
- userExperience: 'Return 403 Forbidden with a user-facing message log the attempt with user ID for audit.',
75
+ userExperience: 'Use the forbidden-action response defined by the authorization contract.',
80
76
  });
81
77
  }
82
78
  // STRIDE elevation of privilege challenge for admin/payment
83
- const isHighRisk = lower.includes('payment') ||
84
- lower.includes('admin') ||
85
- lower.includes('billing') ||
86
- lower.includes('sensitive');
79
+ const isHighRisk = hasAnyAffirmedMatch(signalText, [
80
+ /\b(?:payment|billing|admin\s+operation|sensitive\s+operation)\b/i,
81
+ ]);
87
82
  if (isHighRisk) {
88
83
  scenarios.push({
89
84
  scenario: `[${spec.id}] STRIDE Elevation of Privilege: regular user performs a sequence of legitimate API calls that results in unintended elevated access (e.g., create → transfer → access with original permissions).`,
@@ -8,23 +8,16 @@ import { updateSpec } from '../storage/spec-store.js';
8
8
  import { resolveProjectId, missingProjectIdError } from './resolve-project-id.js';
9
9
  import { elicitOrFallback, buildEnumSchema } from '../engine/elicitation/elicit-helper.js';
10
10
  import { t, ti } from '../i18n/index.js';
11
- import { generateFailureScenarios, generateSecurityScenarios, generateScaleScenarios, generateDataConsistencyScenarios, generateGameChallenges, generateDesktopAutoUpdateChallenges, } from './challenge-spec-scenarios.js';
12
11
  import { generateConcurrencyAnalysis, buildScalabilityAssessment, calculateOverallRisk, readSpecContent, } from './challenge-spec-helpers.js';
13
12
  import { prioritizeScenarios, buildPrioritizedSummary } from '../engine/challenge-prioritizer.js';
14
13
  import { checkContradictions as checkScopeContradictions } from '../engine/scope-boundaries/index.js';
15
14
  import { buildChallengeSpecSummary } from '../engine/human-summary.js';
16
- import { generateAgentChallengeScenarios, isAgentSpec, } from './challenge-spec/agent-challenge-scenarios.js';
17
- import { generateImplementationContractChallengeScenarios } from './challenge-spec/implementation-contract-challenge-scenarios.js';
18
- import { getPlatformChallenges } from './challenge-spec/platform-challenge-scenarios.js';
19
- import { generateSecurityChallengeScenarios } from './challenge-spec/security-challenge-scenarios.js';
20
- import { generatePrivacyChallengeScenarios } from './challenge-spec/privacy-challenge-scenarios.js';
21
- import { generateEventChallengeScenarios } from './challenge-spec/event-challenge-scenarios.js';
22
- import { generateDddChallenges, generateXpPairClarityChallenge, } from './challenge-spec/architecture-challenge-scenarios.js';
23
- import { generateResilienceChallengeScenarios } from './challenge-spec/resilience-challenge-scenarios.js';
24
15
  import { detectContradictions as detectDecisionContradictions, searchPriorDecisions, } from '../engine/prior-decisions/index.js';
25
16
  import { getDecisions } from '../storage/decision-store.js';
26
17
  import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-budget/index.js';
27
18
  import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
19
+ import { detectChallengeCapabilities, } from './challenge-spec/scenarios-utils.js';
20
+ import { collectCapabilityScenarios } from './challenge-spec/scenario-collector.js';
28
21
  const ALL_FOCUS_AREAS = [
29
22
  'failures',
30
23
  'concurrency',
@@ -32,7 +25,6 @@ const ALL_FOCUS_AREAS = [
32
25
  'security',
33
26
  'data-consistency',
34
27
  ];
35
- const ACTIONABLE_RELEVANCE_MIN = 15;
36
28
  /**
37
29
  * SPEC-615 AC3: detect contradictions between spec criteria and prior decisions.
38
30
  * Mutates failureScenarios in place. Best-effort — never throws.
@@ -125,6 +117,7 @@ export async function handleChallengeSpec(args, server) {
125
117
  const constitution = await knowledgeStore.getConstitution(projectId);
126
118
  // 3. Read spec content
127
119
  const specContent = await readSpecContent(spec);
120
+ const capabilities = detectChallengeCapabilities(spec, specContent);
128
121
  // 4. Determine focus areas
129
122
  const focusAreas = focus && focus.length > 0 ? focus : ALL_FOCUS_AREAS;
130
123
  // 5. Generate failure scenarios
@@ -153,58 +146,7 @@ export async function handleChallengeSpec(args, server) {
153
146
  catch {
154
147
  // Minimality challenge is best-effort and must not hide resilience findings.
155
148
  }
156
- if (focusAreas.includes('failures')) {
157
- failureScenarios.push(...generateFailureScenarios(spec, specContent, knowledge));
158
- }
159
- if (focusAreas.includes('security')) {
160
- failureScenarios.push(...generateSecurityScenarios(spec, specContent, knowledge));
161
- }
162
- if (focusAreas.includes('scale')) {
163
- failureScenarios.push(...generateScaleScenarios(spec, specContent, knowledge));
164
- }
165
- if (focusAreas.includes('data-consistency')) {
166
- failureScenarios.push(...generateDataConsistencyScenarios(spec, specContent, knowledge));
167
- }
168
- // 5e. Game-specific challenges (performance, balance, cheating, physics)
169
- if (knowledge.projectCategory === 'game' ||
170
- (knowledge.framework !== null &&
171
- ['unity', 'godot', 'unreal', 'bevy', 'phaser', 'pygame'].includes(knowledge.framework))) {
172
- failureScenarios.push(...generateGameChallenges(spec, specContent, knowledge));
173
- }
174
- // 5f. SPEC-014b: Desktop auto-update edge cases
175
- failureScenarios.push(...generateDesktopAutoUpdateChallenges(spec, specContent, knowledge));
176
- // 5g. SPEC-016a: AI agent-specific failure modes
177
- if (isAgentSpec(spec, specContent)) {
178
- failureScenarios.push(...generateAgentChallengeScenarios(spec, specContent, knowledge));
179
- }
180
- if (focusAreas.includes('failures')) {
181
- failureScenarios.push(...generateImplementationContractChallengeScenarios(spec, specContent));
182
- }
183
- // 5h. SPEC-015a: Smart contract, bot, and IoT challenges
184
- failureScenarios.push(...getPlatformChallenges(spec, specContent, knowledge));
185
- // 5i. SPEC-029: Security authorization and STRIDE challenges
186
- if (focusAreas.includes('security')) {
187
- failureScenarios.push(...generateSecurityChallengeScenarios(spec, specContent, knowledge));
188
- }
189
- // 5j. SPEC-030: Privacy challenges (mandatory when spec has PII)
190
- failureScenarios.push(...generatePrivacyChallengeScenarios(spec, specContent, knowledge));
191
- // 5k. SPEC-031: Event-driven challenges (contract, idempotence, DLQ)
192
- failureScenarios.push(...generateEventChallengeScenarios(spec, specContent, knowledge));
193
- // 5l. SPEC-033: DDD aggregate integrity challenges
194
- const hasDdd = knowledge.architecture.primary === 'ddd' || knowledge.architecture.secondary.includes('ddd');
195
- if (hasDdd) {
196
- failureScenarios.push(...generateDddChallenges(spec, specContent));
197
- }
198
- // 5n. SPEC-034: Resilience, validation, concurrency, and saga challenges
199
- failureScenarios.push(...generateResilienceChallengeScenarios(spec, specContent, knowledge));
200
- // 5m. SPEC-033: XP pair programming clarity challenge
201
- // `agileConfig` may be present on enriched knowledge objects (AgileProcessDetection mixin)
202
- // but is not declared on the base ProjectKnowledge interface — intentional duck-type access.
203
- // ProjectKnowledge has no index signature, so `as unknown` intermediate is required.
204
- const agileConfig = knowledge.agileConfig;
205
- if (agileConfig?.process === 'xp') {
206
- failureScenarios.push(...generateXpPairClarityChallenge(spec, specContent));
207
- }
149
+ failureScenarios.push(...collectCapabilityScenarios({ spec, specContent, knowledge, focusAreas, capabilities }));
208
150
  // SPEC-612: Check for contradictions between outOfScope and acceptance criteria
209
151
  if (spec.outOfScope !== undefined && spec.outOfScope.length > 0) {
210
152
  const criteriaTexts = specContent
@@ -228,31 +170,25 @@ export async function handleChallengeSpec(args, server) {
228
170
  // SPEC-615 AC3: Check criteria against prior decisions for technology contradictions
229
171
  await runPriorDecisionContradictions(projectId, spec.title, spec.tags, specContent, failureScenarios);
230
172
  // 6. Generate concurrency analysis
231
- const concurrencyAnalysis = focusAreas.includes('concurrency')
173
+ const concurrencyAnalysis = focusAreas.includes('concurrency') && capabilities.concurrency
232
174
  ? generateConcurrencyAnalysis(spec, specContent, knowledge)
233
175
  : { hotPaths: [], raceConditions: [], sharedState: [], recommendations: [] };
234
- // 7. Build scalability assessment
235
- const scalabilityAssessment = buildScalabilityAssessment(spec, knowledge, failureScenarios);
236
- // 8. Calculate overall risk
237
- const overallRisk = calculateOverallRisk(failureScenarios, concurrencyAnalysis);
176
+ // Unsupported domain templates are removed before prioritization and risk calculation.
177
+ const actionableFailureScenarios = failureScenarios.filter((scenario) => isScenarioSupportedByCapabilities(scenario, capabilities));
178
+ const suppressedScenarioCount = failureScenarios.length - actionableFailureScenarios.length;
179
+ // 7. Build scalability assessment from grounded scenarios only.
180
+ const scalabilityAssessment = buildScalabilityAssessment(spec, knowledge, actionableFailureScenarios);
181
+ // 8. Calculate overall risk from grounded scenarios only.
182
+ const overallRisk = calculateOverallRisk(actionableFailureScenarios, concurrencyAnalysis);
238
183
  // 9. Compute relevance scores and select top-3 (SPEC-338)
239
- const prioritized = prioritizeScenarios(failureScenarios, spec);
184
+ const prioritized = prioritizeScenarios(actionableFailureScenarios, spec);
240
185
  const prioritizedSummary = buildPrioritizedSummary(prioritized);
241
- // Attach relevanceScore to each scenario in the full list
242
- const failureScenariosScored = failureScenarios.map((s) => {
186
+ const failureScenariosScored = actionableFailureScenarios.map((s) => {
243
187
  const match = prioritized.find((p) => p.scenario === s.scenario);
244
188
  return match !== undefined ? { ...s, relevanceScore: match.relevanceScore } : s;
245
189
  });
246
- const prioritizedScenarios = new Set(prioritized.map((scenario) => scenario.scenario));
247
- const actionableFailureScenarios = failureScenariosScored.filter((scenario) => prioritizedScenarios.has(scenario.scenario) ||
248
- scenario.impact === 'critical' ||
249
- scenario.impact === 'high' ||
250
- isDomainRelevantScenario(scenario, specContent, knowledge) ||
251
- (scenario.relevanceScore ?? computeScenarioRelevanceFallback(scenario)) >=
252
- ACTIONABLE_RELEVANCE_MIN);
253
- const suppressedScenarioCount = failureScenariosScored.length - actionableFailureScenarios.length;
254
190
  const analysis = {
255
- failureScenarios: actionableFailureScenarios,
191
+ failureScenarios: failureScenariosScored,
256
192
  concurrencyAnalysis,
257
193
  scalabilityAssessment,
258
194
  overallRisk,
@@ -271,12 +207,12 @@ export async function handleChallengeSpec(args, server) {
271
207
  },
272
208
  summary: {
273
209
  totalScenarios: failureScenarios.length,
274
- actionableScenarios: actionableFailureScenarios.length,
210
+ actionableScenarios: failureScenariosScored.length,
275
211
  suppressedLowRelevanceScenarios: suppressedScenarioCount,
276
212
  shownByDefault: 3,
277
213
  mustAddressBeforeCoding: prioritizedSummary,
278
- criticalImpact: failureScenarios.filter((s) => s.impact === 'critical').length,
279
- highImpact: failureScenarios.filter((s) => s.impact === 'high').length,
214
+ criticalImpact: actionableFailureScenarios.filter((s) => s.impact === 'critical').length,
215
+ highImpact: actionableFailureScenarios.filter((s) => s.impact === 'high').length,
280
216
  raceConditions: concurrencyAnalysis.raceConditions.length,
281
217
  overallRisk,
282
218
  focusAreas: focusAreas,
@@ -302,8 +238,8 @@ export async function handleChallengeSpec(args, server) {
302
238
  await updateSpec(projectId, specId, {
303
239
  challengeReport: {
304
240
  runAt: new Date().toISOString(),
305
- totalScenarios: failureScenarios.length,
306
- addressedCount: failureScenarios.length, // All generated scenarios are considered "addressed" by running challenge_spec
241
+ totalScenarios: actionableFailureScenarios.length,
242
+ addressedCount: actionableFailureScenarios.length,
307
243
  focusAreas: focusAreas,
308
244
  overallRisk: overallRisk,
309
245
  passed: true,
@@ -314,7 +250,7 @@ export async function handleChallengeSpec(args, server) {
314
250
  // Best-effort: don't fail the challenge if persistence fails
315
251
  }
316
252
  // SPEC-595: Elicit how user wants to apply suggestions when critical issues exist
317
- const criticalCount = failureScenarios.filter((s) => s.impact === 'critical').length;
253
+ const criticalCount = actionableFailureScenarios.filter((s) => s.impact === 'critical').length;
318
254
  if (server !== undefined && criticalCount > 0) {
319
255
  const { field, property } = buildEnumSchema('apply', ['all', 'critical', 'none'], ['Apply all suggestions', 'Apply critical only (Recommended)', 'Review manually'], 'Apply suggestions', 'critical');
320
256
  const schema = { type: 'object', properties: { [field]: property } };
@@ -326,7 +262,7 @@ export async function handleChallengeSpec(args, server) {
326
262
  {
327
263
  type: 'text',
328
264
  text: ti('tools.challenge_spec.success', {
329
- scenarioCount: String(failureScenarios.length),
265
+ scenarioCount: String(actionableFailureScenarios.length),
330
266
  }),
331
267
  },
332
268
  { type: 'text', text: humanSummary },
@@ -349,7 +285,7 @@ export async function handleChallengeSpec(args, server) {
349
285
  {
350
286
  type: 'text',
351
287
  text: ti('tools.challenge_spec.success', {
352
- scenarioCount: String(failureScenarios.length),
288
+ scenarioCount: String(actionableFailureScenarios.length),
353
289
  }),
354
290
  },
355
291
  { type: 'text', text: humanSummary },
@@ -362,12 +298,12 @@ export async function handleChallengeSpec(args, server) {
362
298
  {
363
299
  type: 'text',
364
300
  text: ti('tools.challenge_spec.success', {
365
- scenarioCount: String(failureScenarios.length),
301
+ scenarioCount: String(actionableFailureScenarios.length),
366
302
  }),
367
303
  },
368
304
  {
369
305
  type: 'text',
370
- text: `Actionable scenarios: ${String(actionableFailureScenarios.length)}` +
306
+ text: `Actionable scenarios: ${String(failureScenariosScored.length)}` +
371
307
  ` | Suppressed low-relevance: ${String(suppressedScenarioCount)}` +
372
308
  ` | Overall risk: ${overallRisk}`,
373
309
  },
@@ -376,37 +312,22 @@ export async function handleChallengeSpec(args, server) {
376
312
  structuredContent: analysisPayload,
377
313
  };
378
314
  }
379
- function computeScenarioRelevanceFallback(scenario) {
380
- if (scenario.impact === 'critical') {
381
- return 100;
382
- }
383
- if (scenario.impact === 'high') {
384
- return 70;
385
- }
386
- if (scenario.probability === 'high') {
387
- return 35;
388
- }
389
- return 0;
390
- }
391
- function isDomainRelevantScenario(scenario, specContent, knowledge) {
315
+ function isScenarioSupportedByCapabilities(scenario, capabilities) {
392
316
  const haystack = `${scenario.scenario} ${scenario.currentHandling} ${scenario.requiredHandling}`
393
317
  .toLowerCase()
394
318
  .trim();
395
- const specText = specContent.toLowerCase();
396
- const framework = knowledge.framework?.toLowerCase() ?? '';
397
- const isGameProject = knowledge.projectCategory === 'game' ||
398
- ['unity', 'godot', 'unreal', 'bevy', 'phaser', 'pygame'].includes(framework);
399
- if (isGameProject && /\b(game|physics|cheat|balance|save|hud|player)\b/.test(haystack)) {
400
- return true;
401
- }
402
- if (/\b(database|query|index|pagination|transaction|cache)\b/.test(haystack) &&
403
- /\b(database|query|index|pagination|transaction|cache|sql|postgres|sqlite)\b/.test(specText)) {
404
- return true;
405
- }
406
- if (/\b(api|auth|jwt|session|csrf|injection|tenant|bola)\b/.test(haystack) &&
407
- /\b(api|auth|jwt|session|csrf|injection|tenant|bola)\b/.test(specText)) {
408
- return true;
409
- }
410
- return false;
319
+ const rules = [
320
+ [/\b(?:payment|billing|checkout|stripe|paypal)\b/, capabilities.payment],
321
+ [/\b(?:privacy|pii|gdpr|personal data)\b/, capabilities.privacy],
322
+ [
323
+ /\b(?:bola|idor|jwt|authentication|authorization|csrf|privilege)\b/,
324
+ capabilities.authentication || capabilities.authorization,
325
+ ],
326
+ [/\b(?:sql|nosql|database|connection pool)\b/, capabilities.database],
327
+ [/\b(?:event contract|event schema|message broker|kafka|rabbitmq|dlq)\b/, capabilities.events],
328
+ [/\b(?:api endpoint|http route|http status|network timeout)\b/, capabilities.networkApi],
329
+ [/\b(?:traffic spike|load test|auto-scaling|throughput)\b/, capabilities.scale],
330
+ ];
331
+ return rules.every(([pattern, supported]) => !pattern.test(haystack) || supported);
411
332
  }
412
333
  //# sourceMappingURL=challenge-spec.js.map
@@ -3,8 +3,8 @@ import type { ToolResult } from '../types/index.js';
3
3
  export declare const ConfigureComplianceGateSchema: z.ZodObject<{
4
4
  projectPath: z.ZodString;
5
5
  mode: z.ZodEnum<{
6
- soft: "soft";
7
6
  hard: "hard";
7
+ soft: "soft";
8
8
  }>;
9
9
  threshold: z.ZodDefault<z.ZodNumber>;
10
10
  }, z.core.$strip>;