@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
package/CHANGELOG.md CHANGED
@@ -1,3 +1,19 @@
1
+ ## [4.11.2] - 2026-07-10
2
+
3
+ ### Bug Fixes
4
+ - fix(specs): ground lifecycle output and adopt TypeScript 7
5
+
6
+ ### Chores
7
+ - chore(deps): update patch dependencies
8
+
9
+
10
+ ## [4.11.1] - 2026-07-09
11
+
12
+ ### Bug Fixes
13
+ - fix: benchmark representative graph sources
14
+ - fix: verify release lockfile with pinned pnpm
15
+
16
+
1
17
  ## [4.11.0] - 2026-07-09
2
18
 
3
19
  ### Features
@@ -9,7 +9,6 @@ import { gitAutoStageHook } from './hooks/git-auto-stage.hook.js';
9
9
  import { velocityProfileHook } from './hooks/velocity-profile.hook.js';
10
10
  import { estimateCalibrationHook } from './hooks/estimate-calibration.hook.js';
11
11
  import { lessonDriftHook } from './hooks/lesson-drift.hook.js';
12
- import { livingSpecsHook } from './hooks/living-specs.hook.js';
13
12
  import { nextSpecResolverHook } from './hooks/next-spec-resolver.hook.js';
14
13
  import { orchestrationPlanHook } from './hooks/orchestration-plan.hook.js';
15
14
  import { completeLoopHook } from './hooks/complete-loop.hook.js';
@@ -38,7 +37,6 @@ export const HOOKS = Object.freeze([
38
37
  velocityProfileHook,
39
38
  estimateCalibrationHook,
40
39
  lessonDriftHook,
41
- livingSpecsHook,
42
40
  nextSpecResolverHook,
43
41
  orchestrationPlanHook,
44
42
  completeLoopHook,
@@ -6,15 +6,15 @@ export declare const ChallengeScenarioPatternSchema: z.ZodObject<{
6
6
  triggerKeywords: z.ZodArray<z.ZodString>;
7
7
  scenario: z.ZodString;
8
8
  probability: z.ZodEnum<{
9
+ high: "high";
9
10
  low: "low";
10
11
  medium: "medium";
11
- high: "high";
12
12
  }>;
13
13
  impact: z.ZodEnum<{
14
+ critical: "critical";
15
+ high: "high";
14
16
  low: "low";
15
17
  medium: "medium";
16
- high: "high";
17
- critical: "critical";
18
18
  }>;
19
19
  handlingKeywords: z.ZodArray<z.ZodString>;
20
20
  handlingFound: z.ZodString;
@@ -5,11 +5,11 @@ export declare const ComplianceFrameworkDefSchema: z.ZodObject<{
5
5
  name: z.ZodString;
6
6
  fullName: z.ZodString;
7
7
  type: z.ZodEnum<{
8
- security: "security";
9
- testing: "testing";
10
- quality: "quality";
11
8
  accessibility: "accessibility";
12
9
  privacy: "privacy";
10
+ quality: "quality";
11
+ security: "security";
12
+ testing: "testing";
13
13
  }>;
14
14
  regions: z.ZodArray<z.ZodString>;
15
15
  triggerKeywords: z.ZodArray<z.ZodString>;
@@ -1,6 +1,6 @@
1
1
  import type { DashboardMetrics, DashboardSpecCard, ProjectHealthScore, SpecStatusUpdateResult, StatusTransitionMap } from '../../types/index.js';
2
2
  /** Ordered list of valid spec statuses */
3
- export declare const VALID_STATUSES: readonly ["draft", "review", "approved", "implementing", "done", "discarded"];
3
+ export declare const VALID_STATUSES: readonly ['draft', 'review', 'approved', 'implementing', 'done', 'discarded'];
4
4
  /** Valid forward + one-step-back transitions */
5
5
  export declare const STATUS_TRANSITIONS: StatusTransitionMap;
6
6
  /** Check if a status string is a valid spec status */
@@ -1,5 +1,5 @@
1
1
  import type { SpecRisk, RiskCategory } from '../../../types/risk.js';
2
- declare const IMPACT_LEVELS: readonly ["low", "medium", "high", "very_high", "critical"];
2
+ declare const IMPACT_LEVELS: readonly ['low', 'medium', 'high', 'very_high', 'critical'];
3
3
  declare const PROB_LEVELS: readonly [20, 40, 60, 80, 100];
4
4
  declare const CATEGORY_COLORS: Record<RiskCategory, string>;
5
5
  /**
@@ -1,6 +1,6 @@
1
1
  import type { ProposalData } from '../../../types/docs.js';
2
2
  /** Canonical section IDs for includeSections / excludeSections filtering. */
3
- export declare const SECTION_IDS: readonly ["hero", "executive-summary", "scope", "spec-breakdown", "timeline", "budget", "investment-summary", "architecture", "risk", "decisions", "impact-analysis", "gap-analysis", "team-resources", "pricing-methodology", "design-system", "quality-metrics", "living-spec", "assumptions", "alternatives", "post-launch", "module-mapping"];
3
+ export declare const SECTION_IDS: readonly ['hero', 'executive-summary', 'scope', 'spec-breakdown', 'timeline', 'budget', 'investment-summary', 'architecture', 'risk', 'decisions', 'impact-analysis', 'gap-analysis', 'team-resources', 'pricing-methodology', 'design-system', 'quality-metrics', 'living-spec', 'assumptions', 'alternatives', 'post-launch', 'module-mapping'];
4
4
  /**
5
5
  * Generate a self-contained HTML proposal document.
6
6
  * All sections are conditional — empty string returned when data is insufficient.
@@ -1,7 +1,4 @@
1
1
  import type { DescriptionSignals } from '../../types/index.js';
2
- /**
3
- * Scan a feature description for pre-existing answers.
4
- * Any signal set to true means the corresponding question can be suppressed.
5
- */
2
+ /** Scan a feature description for explicit, affirmed request evidence. */
6
3
  export declare function extractSignals(description: string): DescriptionSignals;
7
4
  //# sourceMappingURL=answer-extractor.d.ts.map
@@ -1,96 +1,225 @@
1
1
  // src/engine/elicitation/answer-extractor.ts
2
- // SPEC-624: Extracts pre-existing answers from description to suppress redundant questions.
3
- const FRONTEND_TERMS = /\b(component|ui|page|css|style|layout|form|button|modal|dialog)\b/;
4
- const BACKEND_TERMS = /\b(api|endpoint|route|handler|migration|query|rpc|action|middleware)\b/;
5
- const INFRA_TERMS = /\b(deploy|ci|cd|docker|pipeline|config|env)\b/;
6
- const BILLING_TERMS = /\b(payment|billing|invoice|subscription|checkout|pricing|pay)\b/;
7
- const DATABASE_TERMS = /\b(database|schema|migration|table|query|sql|rls|rpc|persist|persistence|data access)\b/;
8
- const ACTION_RE = /\b(add|build|create|fix|update|remove|delete|integrate|sync|import|export|approve|reject|upload|download|migrate|configure|validate)\b\s+(?:(?:a|an|the|new)\s+)?([a-z0-9][a-z0-9 -]{1,60})/i;
9
- const ACTOR_TERMS = /\b(admin|administrator|manager|owner|customer|user|member|team|approver)\b/gi;
10
- const DATA_OBJECT_TERMS = /\b(account|profile|request|invoice|subscription|payment|order|report|file|avatar|table|policy|record|settings)\b/gi;
11
- const PERMISSION_TERMS = /\b(approval|approve|reject|admin|permission|role|rls|policy|access|auth|login|oauth|owner|manager)\b/;
12
- const DESTRUCTIVE_TERMS = /\b(delete|remove|archive|discard|drop|truncate|revoke|reset)\b/;
13
- const RISK_TERMS = /\b(approval|approve|payment|billing|auth|login|permission|role|delete|remove|migration|sync|webhook|external|api|rls|privacy|security)\b/gi;
14
- const PAYMENT_PROVIDERS = [
15
- { name: 'Stripe', pattern: /\bstripe\b/ },
16
- { name: 'PayPal', pattern: /\bpaypal\b/ },
17
- { name: 'Paddle', pattern: /\bpaddle\b/ },
18
- { name: 'Braintree', pattern: /\bbraintree\b/ },
19
- { name: 'Square', pattern: /\bsquare\b/ },
20
- { name: 'Adyen', pattern: /\badyen\b/ },
2
+ // SPEC-624 / SPEC-1121: Extracts affirmed request evidence for contextual questions.
3
+ const UI_TARGET_PATTERN = /\b(component|ui|page|css|style|layout|form|button|modal|dialog)\b/i;
4
+ const TARGET_TERMS = [
5
+ ['frontend', UI_TARGET_PATTERN],
6
+ ['backend', /\b(endpoint|route|handler|middleware|controller|server action)\b/i],
7
+ [
8
+ 'tooling',
9
+ /\b(compiler|typecheck|toolchain|typescript|build script|package alias|dependency|release guard)\b/i,
10
+ ],
11
+ ['infrastructure', /\b(deploy|ci|cd|docker|pipeline|configuration|environment)\b/i],
21
12
  ];
22
- /**
23
- * Scan a feature description for pre-existing answers.
24
- * Any signal set to true means the corresponding question can be suppressed.
25
- */
13
+ const BILLING_TERMS = [
14
+ ['payment', /\bpayments?\b/i],
15
+ ['billing', /\bbilling\b/i],
16
+ ['invoice', /\binvoices?\b/i],
17
+ ['subscription', /\bsubscriptions?\b/i],
18
+ ['checkout', /\bcheckout\b/i],
19
+ ['pricing', /\bpricing\b/i],
20
+ ];
21
+ const DATABASE_TERMS = [
22
+ ['database', /\bdatabases?\b/i],
23
+ ['schema', /\bschemas?\b/i],
24
+ ['table', /\btables?\b/i],
25
+ ['sql', /\bsql\b/i],
26
+ ['rls', /\brls\b/i],
27
+ ['persistence', /\b(?:persist|persists|persisted|persistence|data access)\b/i],
28
+ ['data-migration', /\b(?:database|schema|table)\s+migrations?\b|\bmigrate\s+(?:a\s+)?table\b/i],
29
+ ];
30
+ const ACTOR_TERMS = [
31
+ ['admin', /\b(?:admin|administrator)\b/i],
32
+ ['manager', /\bmanager\b/i],
33
+ ['owner', /\bowner\b/i],
34
+ ['customer', /\bcustomer\b/i],
35
+ ['user', /\buser\b/i],
36
+ ['member', /\bmember\b/i],
37
+ ['team', /\bteam\b/i],
38
+ ['approver', /\bapprover\b/i],
39
+ ];
40
+ const DATA_OBJECT_TERMS = [
41
+ ['account', /\baccounts?\b/i],
42
+ ['profile', /\bprofiles?\b/i],
43
+ ['request', /\brequests?\b/i],
44
+ ['invoice', /\binvoices?\b/i],
45
+ ['subscription', /\bsubscriptions?\b/i],
46
+ ['payment', /\bpayments?\b/i],
47
+ ['order', /\borders?\b/i],
48
+ ['report', /\breports?\b/i],
49
+ ['file', /\bfiles?\b/i],
50
+ ['avatar', /\bavatars?\b/i],
51
+ ['table', /\btables?\b/i],
52
+ ['policy', /\bpolic(?:y|ies)\b/i],
53
+ ['record', /\brecords?\b/i],
54
+ ['settings', /\bsettings\b/i],
55
+ ];
56
+ const PERMISSION_TERMS = [
57
+ ['approval', /\b(?:approval|approve|approves|reject|rejects)\b/i],
58
+ ['authorization', /\b(?:authorization|permission|role|rbac|access control)\b/i],
59
+ ['authentication', /\b(?:auth|authentication|login|oauth)\b/i],
60
+ ['ownership', /\b(?:owner|allowed to|who can)\b/i],
61
+ ];
62
+ const DESTRUCTIVE_TERMS = [
63
+ ['delete', /\b(?:delete|remove|archive|discard|drop|truncate|revoke|reset)\b/i],
64
+ ];
65
+ const RISK_TERMS = [
66
+ ...BILLING_TERMS,
67
+ ...PERMISSION_TERMS,
68
+ ['delete', /\b(?:delete|remove|archive|discard|drop|truncate|revoke|reset)\b/i],
69
+ ['sync', /\bsync(?:hronize)?\b/i],
70
+ ['webhook', /\bwebhooks?\b/i],
71
+ ['external', /\b(?:external|third-party|remote)\b/i],
72
+ ['privacy', /\bprivacy\b/i],
73
+ ['security', /\bsecurity\b/i],
74
+ ];
75
+ const ACTION_RE = /\b(add|adopt|build|create|fix|update|upgrade|replace|remove|delete|integrate|sync|import|export|approve|reject|upload|download|migrate|configure|validate|verify|retain|preserve)\b\s+(?:(?:a|an|the|new)\s+)?([a-z0-9][a-z0-9 -]{1,60})/i;
76
+ const NEGATION_RE = /\b(?:no|not|never|without|exclude|excludes|excluding|neither|nor|do not|does not|don't|doesn't|must not|should not|isn't|aren't|non-goal)\b|\bout[- ]of[- ]scope\b/i;
77
+ const EXAMPLE_RE = /\b(?:example|examples|for example|e\.g\.|such as)\b/i;
78
+ const TEST_PROSE_RE = /\b(?:test|tests|tested|testing|fixture|fixtures|assert|assertion|coverage|regression)\b/i;
79
+ const INTERNAL_API_CONTEXT_RE = /\b(?:compiler|programmatic|package|library|ast|type(?:script)?|declaration)\s+api\b|\bapi\s+(?:compatibility|surface)\b/i;
80
+ const GENERIC_CANDIDATES = new Set([
81
+ 'api',
82
+ 'billing',
83
+ 'checkout',
84
+ 'integration',
85
+ 'payment',
86
+ 'provider',
87
+ 'sdk',
88
+ 'system',
89
+ ]);
90
+ /** Scan a feature description for explicit, affirmed request evidence. */
26
91
  export function extractSignals(description) {
27
- const lower = stripIncidentalReferences(description.toLowerCase());
28
- const hasTarget = FRONTEND_TERMS.test(lower) || BACKEND_TERMS.test(lower) || INFRA_TERMS.test(lower);
29
- const hasDatabase = DATABASE_TERMS.test(lower);
30
- const hasUi = FRONTEND_TERMS.test(lower);
31
- let namedProvider = null;
32
- for (const { name, pattern } of PAYMENT_PROVIDERS) {
33
- if (pattern.test(lower)) {
34
- namedProvider = name;
35
- break;
36
- }
37
- }
38
- // hasBilling: billing/payment keywords OR a specific provider was named
39
- const hasBilling = BILLING_TERMS.test(lower) || namedProvider !== null;
40
- const wordCount = description.trim().split(/\s+/).length;
41
- const hasScope = wordCount >= 10;
92
+ const cleaned = stripIncidentalReferences(description);
93
+ const segments = splitSegments(cleaned);
94
+ const targetTerms = collectAffirmedTerms(segments, TARGET_TERMS);
95
+ const billingTerms = collectAffirmedTerms(segments, BILLING_TERMS);
96
+ const databaseTerms = collectAffirmedTerms(segments, DATABASE_TERMS);
97
+ const permissionTerms = collectAffirmedTerms(segments, PERMISSION_TERMS);
98
+ const integrations = extractExternalIntegrations(segments);
99
+ const namedProvider = extractNamedBillingCandidate(segments);
100
+ const lower = cleaned.toLowerCase();
42
101
  const actionMatch = ACTION_RE.exec(lower);
43
102
  const action = actionMatch?.[1] ?? null;
44
103
  const domainObject = cleanDomainObject(actionMatch?.[2] ?? null);
45
- const actors = uniqueMatches(lower, ACTOR_TERMS);
46
- const dataObjects = uniqueMatches(lower, DATA_OBJECT_TERMS);
47
- const riskTerms = uniqueMatches(lower, RISK_TERMS);
48
- const integrations = [
49
- ...PAYMENT_PROVIDERS.filter(({ pattern }) => pattern.test(lower)).map(({ name }) => name),
50
- ...extractExternalIntegrations(lower),
51
- ];
52
- const hasPermissionRisk = PERMISSION_TERMS.test(lower);
53
- const hasDestructiveAction = DESTRUCTIVE_TERMS.test(lower);
104
+ const wordCount = description.trim().split(/\s+/).filter(Boolean).length;
54
105
  return {
55
- hasTarget,
56
- hasBilling,
57
- hasDatabase,
58
- hasUi,
106
+ hasTarget: targetTerms.length > 0,
107
+ hasBilling: billingTerms.length > 0 || namedProvider !== null,
108
+ hasDatabase: databaseTerms.length > 0,
109
+ hasUi: segments.some((segment) => hasAffirmedMatch(segment, UI_TARGET_PATTERN)),
59
110
  namedProvider,
60
- hasScope,
111
+ hasScope: wordCount >= 10,
61
112
  action,
62
113
  domainObject,
63
- actors,
114
+ actors: collectAffirmedTerms(segments, ACTOR_TERMS),
64
115
  integrations,
65
- dataObjects,
66
- hasPermissionRisk,
67
- hasDestructiveAction,
68
- riskTerms,
116
+ dataObjects: collectAffirmedTerms(segments, DATA_OBJECT_TERMS),
117
+ hasPermissionRisk: permissionTerms.length > 0,
118
+ hasDestructiveAction: collectAffirmedTerms(segments, DESTRUCTIVE_TERMS).length > 0,
119
+ riskTerms: collectAffirmedTerms(segments, RISK_TERMS),
69
120
  };
70
121
  }
71
122
  function stripIncidentalReferences(description) {
72
123
  return description
73
- .replace(/`[^`]*(?:\/|\.\w{1,8})[^`]*`/g, ' ')
74
- .replace(/\b\S+\/\S+\b/g, ' ')
75
- .replace(/\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|py|go|rs|java|rb|php)\b/g, ' ');
124
+ .replace(/```[\s\S]*?```/g, ' ')
125
+ .replace(/`[^`]+`/g, ' ')
126
+ .replace(/\[[^\]]*\]\([^)]*\)/g, ' ')
127
+ .replace(/https?:\/\/\S+/gi, ' ')
128
+ .replace(/\B@[\w.-]+\/[\w.-]+(?:@[\w.-]+)?/g, ' ')
129
+ .replace(/\b[\w.-]+\/[\w./-]+\b/g, ' ')
130
+ .replace(/\b[\w-]+\.(?:ts|tsx|js|jsx|mjs|cjs|json|md|css|scss|py|go|rs|java|rb|php)\b/gi, ' ')
131
+ .replace(/\b[a-z0-9]+(?:-[a-z0-9]+)+(?:@[^\s,;]+)?\b/gi, ' ');
132
+ }
133
+ function splitSegments(description) {
134
+ return description
135
+ .split(/(?:\r?\n|[.;!?]+)\s*/)
136
+ .map((segment) => segment.replace(/^\s*(?:[-*]|\d+[.)])\s*/, '').trim())
137
+ .filter(Boolean);
138
+ }
139
+ function collectAffirmedTerms(segments, definitions) {
140
+ const found = [];
141
+ for (const [name, pattern] of definitions) {
142
+ if (segments.some((segment) => hasAffirmedMatch(segment, pattern))) {
143
+ found.push(name);
144
+ }
145
+ }
146
+ return found;
147
+ }
148
+ function hasAffirmedMatch(segment, pattern) {
149
+ const flags = pattern.flags.replace('g', '');
150
+ const match = new RegExp(pattern.source, flags).exec(segment);
151
+ if (!match) {
152
+ return false;
153
+ }
154
+ const prefix = segment.slice(0, match.index);
155
+ if (NEGATION_RE.test(prefix) || EXAMPLE_RE.test(prefix) || TEST_PROSE_RE.test(prefix)) {
156
+ return false;
157
+ }
158
+ const matchedText = match[0].toLowerCase();
159
+ if (matchedText === 'api' && INTERNAL_API_CONTEXT_RE.test(segment)) {
160
+ return false;
161
+ }
162
+ return true;
76
163
  }
77
164
  function cleanDomainObject(value) {
78
165
  if (value === null) {
79
166
  return null;
80
167
  }
81
168
  const cleaned = value
82
- .replace(/\b(that|with|to|from|when|where|and|or|but|using|in)\b.*$/i, '')
169
+ .replace(/\b(that|with|to|from|when|where|and|or|but|using|in|while)\b.*$/i, '')
83
170
  .replace(/\s+/g, ' ')
84
171
  .trim();
85
172
  return cleaned.length > 0 ? cleaned : null;
86
173
  }
87
- function uniqueMatches(text, pattern) {
88
- const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`;
89
- const globalPattern = new RegExp(pattern.source, flags);
90
- return Array.from(new Set(Array.from(text.matchAll(globalPattern)).map((match) => match[0])));
174
+ function extractNamedBillingCandidate(segments) {
175
+ for (const segment of segments) {
176
+ if (!BILLING_TERMS.some(([, pattern]) => hasAffirmedMatch(segment, pattern))) {
177
+ continue;
178
+ }
179
+ const candidate = extractCandidate(segment, [
180
+ /\b(?:integrate|use|using|via|with)\s+(?:with\s+)?([a-z][a-z0-9-]{1,40})\b/i,
181
+ /\bprovider\s*(?:is|:)\s*([a-z][a-z0-9-]{1,40})\b/i,
182
+ /\b([a-z][a-z0-9-]{1,40})\s+(?:payment|billing|checkout)\b/i,
183
+ /\b([a-z][a-z0-9-]{1,40})\s+(?:payment|billing|checkout)\s+(?:api|sdk|integration|provider)\b/i,
184
+ ]);
185
+ if (candidate !== null) {
186
+ return normalizeCandidate(candidate);
187
+ }
188
+ }
189
+ return null;
190
+ }
191
+ function extractExternalIntegrations(segments) {
192
+ const integrations = [];
193
+ for (const segment of segments) {
194
+ if (INTERNAL_API_CONTEXT_RE.test(segment)) {
195
+ continue;
196
+ }
197
+ const candidate = extractCandidate(segment, [
198
+ /\b(?:integrate|integration with|sync|sync with)\s+(?:with\s+)?([a-z][a-z0-9-]{1,40})\b/i,
199
+ /\b([a-z][a-z0-9-]{1,40})\s+(?:api|webhook|sdk|integration)\b/i,
200
+ ]);
201
+ if (candidate !== null && isAffirmedCandidate(segment, candidate)) {
202
+ integrations.push(normalizeCandidate(candidate));
203
+ }
204
+ }
205
+ return Array.from(new Set(integrations));
206
+ }
207
+ function extractCandidate(segment, patterns) {
208
+ for (const pattern of patterns) {
209
+ const match = pattern.exec(segment);
210
+ const candidate = match?.[1];
211
+ if (candidate && !GENERIC_CANDIDATES.has(candidate.toLowerCase())) {
212
+ return candidate;
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ function isAffirmedCandidate(segment, candidate) {
218
+ const index = segment.toLowerCase().indexOf(candidate.toLowerCase());
219
+ const prefix = index < 0 ? segment : segment.slice(0, index);
220
+ return !NEGATION_RE.test(prefix) && !EXAMPLE_RE.test(prefix) && !TEST_PROSE_RE.test(prefix);
91
221
  }
92
- function extractExternalIntegrations(text) {
93
- const matches = text.match(/\b([a-z][a-z0-9-]+)\s+(?:api|webhook|sdk|integration)\b/gi) ?? [];
94
- return Array.from(new Set(matches.map((match) => match.replace(/\s+(api|webhook|sdk|integration)$/i, ''))));
222
+ function normalizeCandidate(candidate) {
223
+ return candidate.charAt(0).toUpperCase() + candidate.slice(1);
95
224
  }
96
225
  //# sourceMappingURL=answer-extractor.js.map
@@ -3,12 +3,11 @@ export function detectDecisionGaps(signals, _projectContext) {
3
3
  const gaps = [
4
4
  buildPermissionGap(signals),
5
5
  buildProviderGap(signals),
6
- buildBillingModelGap(signals),
7
6
  buildDataGap(signals),
8
7
  buildFailureGap(signals),
9
8
  ].filter((gap) => gap !== null);
10
- if (gaps.length === 0 && !signals.hasScope) {
11
- gaps.push(buildBehaviorGap(signals));
9
+ if (gaps.length === 0 && isShortAmbiguous(signals)) {
10
+ gaps.push(buildScopeGap(signals));
12
11
  }
13
12
  return gaps.slice(0, MAX_GAPS);
14
13
  }
@@ -18,16 +17,19 @@ function buildPermissionGap(signals) {
18
17
  }
19
18
  const subject = formatSubject(signals);
20
19
  return makeGap({
21
- id: 'permission-owner',
20
+ id: 'permission-boundary',
22
21
  kind: 'permission',
23
- header: 'Permission',
24
- question: `For ${subject}, who is allowed to perform this action? This changes permissions, validation, and audit behavior.`,
25
- evidence: evidenceFrom(signals, ['permission-sensitive wording']),
22
+ header: 'Access',
23
+ question: `For ${subject}, which access boundary is intended? This changes authorization checks and audit criteria.`,
24
+ evidence: evidenceFrom(signals, ['capability:permission']),
26
25
  impact: 'Changes authorization checks and acceptance criteria.',
27
26
  options: options([
28
- ['Role-based owner (Recommended)', 'Use the existing role or owner model for this action.'],
29
- ['Admin only', 'Restrict the behavior to administrators.'],
30
- ['Any signed-in user', 'Allow all authenticated users and keep checks minimal.'],
27
+ ['Existing access boundary', 'Apply the access boundary already defined by the project.'],
28
+ [
29
+ 'New access boundary',
30
+ 'Define the permitted actor without assuming a role or identity model.',
31
+ ],
32
+ ['Access control out of scope', 'Keep authorization changes outside this spec.'],
31
33
  ]),
32
34
  });
33
35
  }
@@ -37,38 +39,21 @@ function buildProviderGap(signals) {
37
39
  }
38
40
  const subject = formatSubject(signals);
39
41
  return makeGap({
40
- id: 'payment-provider',
42
+ id: 'payment-provider-boundary',
41
43
  kind: 'provider',
42
44
  header: 'Provider',
43
- question: `For ${subject}, which payment provider should own the payment flow? This changes API contracts, webhook handling, and test fixtures.`,
44
- evidence: evidenceFrom(signals, ['billing/payment wording']),
45
- impact: 'Changes integration code, webhook behavior, and verification fixtures.',
45
+ question: `For ${subject}, is payment-provider selection part of this spec? This changes integration ownership and acceptance criteria.`,
46
+ evidence: evidenceFrom(signals, ['domain:billing']),
47
+ impact: 'Changes integration ownership and acceptance criteria.',
46
48
  options: options([
47
- ['Stripe (Recommended)', 'Use the most common provider for subscription and checkout flows.'],
48
- ['PayPal', 'Use PayPal checkout and payment APIs.'],
49
49
  [
50
- 'No provider yet',
51
- 'Keep provider integration out of scope and define internal contracts only.',
50
+ 'Provider selection in scope',
51
+ 'Define selection criteria without choosing an unevidenced vendor.',
52
+ ],
53
+ [
54
+ 'Provider selection out of scope',
55
+ 'Limit the spec to behavior that does not choose a provider.',
52
56
  ],
53
- ]),
54
- });
55
- }
56
- function buildBillingModelGap(signals) {
57
- if (!signals.hasBilling) {
58
- return null;
59
- }
60
- const subject = formatSubject(signals);
61
- return makeGap({
62
- id: 'billing-model',
63
- kind: 'billing-model',
64
- header: 'Billing',
65
- question: `For ${subject}, is the billing behavior recurring, one-time, or invoice-based? This changes states, events, and acceptance criteria.`,
66
- evidence: evidenceFrom(signals, ['billing/payment wording']),
67
- impact: 'Changes lifecycle states and acceptance criteria.',
68
- options: options([
69
- ['Recurring subscription (Recommended)', 'Model ongoing subscriptions and renewal states.'],
70
- ['One-time checkout', 'Model a single payment completion flow.'],
71
- ['Invoice-based', 'Model invoice creation, payment, and overdue states.'],
72
57
  ]),
73
58
  });
74
59
  }
@@ -78,81 +63,76 @@ function buildDataGap(signals) {
78
63
  }
79
64
  const subject = formatSubject(signals);
80
65
  return makeGap({
81
- id: 'data-shape',
66
+ id: 'data-boundary',
82
67
  kind: 'data',
83
68
  header: 'Data',
84
- question: `For ${subject}, what data must be stored or updated? This changes schema, validation, and migration work.`,
85
- evidence: evidenceFrom(signals, ['database/data wording']),
86
- impact: 'Changes schema, persistence, and validation requirements.',
69
+ question: `For ${subject}, which data shape belongs in scope? This changes persistence, validation, and migration criteria.`,
70
+ evidence: evidenceFrom(signals, ['domain:data']),
71
+ impact: 'Changes persistence, validation, and migration criteria.',
87
72
  options: options([
88
- [
89
- 'Existing records only (Recommended)',
90
- 'Use existing tables or records without a new schema.',
91
- ],
92
- ['New table or field', 'Add persistence changes and migration coverage.'],
93
- ['Read-only data', 'Do not persist new state for this behavior.'],
73
+ ['Existing data shape', 'Use only data structures already present in the project.'],
74
+ ['New data shape', 'Define the required data change without assuming a database product.'],
75
+ ['Data change out of scope', 'Keep persistence changes outside this spec.'],
94
76
  ]),
95
77
  });
96
78
  }
97
79
  function buildFailureGap(signals) {
98
- if (signals.integrations.length === 0 &&
99
- !signals.riskTerms.includes('sync') &&
100
- !signals.riskTerms.includes('api')) {
80
+ const integration = signals.integrations[0];
81
+ if (integration === undefined) {
101
82
  return null;
102
83
  }
103
84
  const subject = formatSubject(signals);
104
85
  return makeGap({
105
- id: 'failure-handling',
86
+ id: 'failure-boundary',
106
87
  kind: 'failure',
107
88
  header: 'Failure',
108
- question: `For ${subject}, what should happen if ${formatIntegration(signals)} fails? This changes retry, error, and user-visible behavior.`,
109
- evidence: evidenceFrom(signals, signals.integrations.length > 0 ? signals.integrations : ['integration/API wording']),
110
- impact: 'Changes error handling, retries, and tests.',
89
+ question: `For ${subject}, should ${integration} failure behavior be in scope? This changes error-handling and verification criteria.`,
90
+ evidence: evidenceFrom(signals, [`integration:${integration}`]),
91
+ impact: 'Changes error-handling and verification criteria.',
111
92
  options: options([
112
- ['Show recoverable error (Recommended)', 'Return a clear error and preserve current state.'],
113
- ['Retry automatically', 'Retry transient failures before surfacing an error.'],
114
- ['Queue for later', 'Persist work for asynchronous retry.'],
93
+ [
94
+ 'Failure behavior in scope',
95
+ 'Define the observable failure outcome without assuming a recovery strategy.',
96
+ ],
97
+ ['Failure behavior out of scope', 'Limit this spec to the successful integration boundary.'],
115
98
  ]),
116
99
  });
117
100
  }
118
- function buildBehaviorGap(signals) {
101
+ function buildScopeGap(signals) {
119
102
  const subject = formatSubject(signals);
120
103
  return makeGap({
121
- id: 'behavior-outcome',
122
- kind: 'behavior',
123
- header: 'Behavior',
124
- question: `For ${subject}, what observable outcome should prove the work is done? This becomes the primary acceptance criterion.`,
125
- evidence: evidenceFrom(signals, ['short request']),
126
- impact: 'Changes the main acceptance criterion and verification command.',
104
+ id: 'scope-outcome',
105
+ kind: 'scope',
106
+ header: 'Scope',
107
+ question: `For ${subject}, which observable outcome should define the scope? This changes the primary acceptance criterion.`,
108
+ evidence: evidenceFrom(signals, ['request:short-ambiguous']),
109
+ impact: 'Changes the primary acceptance criterion.',
127
110
  options: options([
128
- ['User-visible workflow (Recommended)', 'Define the end-to-end behavior a user can observe.'],
129
- ['Backend behavior', 'Define API, persistence, job, or integration behavior.'],
130
- ['Cleanup or fix', 'Define the before/after bug or cleanup result.'],
111
+ ['Behavior change', 'Describe the concrete before-and-after behavior.'],
112
+ ['Bounded investigation', 'Limit the work to a finding and its verification evidence.'],
113
+ ['Maintenance outcome', 'Define the concrete cleanup or compatibility result.'],
131
114
  ]),
132
115
  });
133
116
  }
117
+ function isShortAmbiguous(signals) {
118
+ return (!signals.hasScope &&
119
+ !signals.hasTarget &&
120
+ !signals.hasBilling &&
121
+ !signals.hasDatabase &&
122
+ !signals.hasPermissionRisk &&
123
+ signals.integrations.length === 0);
124
+ }
134
125
  function makeGap(gap) {
135
126
  return { ...gap, multiSelect: gap.multiSelect ?? false, blocking: true };
136
127
  }
137
128
  function formatSubject(signals) {
138
129
  const action = signals.action ?? 'this request';
139
- const object = signals.domainObject;
140
- return object === null ? action : `${action} ${object}`;
141
- }
142
- function formatIntegration(signals) {
143
- if (signals.integrations.length > 0) {
144
- return signals.integrations[0] ?? 'the integration';
145
- }
146
- if (signals.riskTerms.includes('sync')) {
147
- return 'the sync';
148
- }
149
- return 'the API';
130
+ return signals.domainObject === null ? action : `${action} ${signals.domainObject}`;
150
131
  }
151
132
  function evidenceFrom(signals, extra) {
152
133
  return [
153
134
  ...(signals.action ? [`action:${signals.action}`] : []),
154
135
  ...(signals.domainObject ? [`object:${signals.domainObject}`] : []),
155
- ...signals.riskTerms.map((term) => `risk:${term}`),
156
136
  ...extra,
157
137
  ].slice(0, 6);
158
138
  }
@@ -5,7 +5,6 @@ export * from './question-generator.js';
5
5
  export * from './capability.js';
6
6
  export * from './elicit-helper.js';
7
7
  export * from './answer-extractor.js';
8
- export * from './option-builder.js';
9
8
  export * from './user-elicitor.js';
10
9
  export * from './decision-cache.js';
11
10
  //# sourceMappingURL=index.d.ts.map
@@ -6,7 +6,6 @@ export * from './question-generator.js';
6
6
  export * from './capability.js';
7
7
  export * from './elicit-helper.js';
8
8
  export * from './answer-extractor.js';
9
- export * from './option-builder.js';
10
9
  export * from './user-elicitor.js';
11
10
  export * from './decision-cache.js';
12
11
  //# sourceMappingURL=index.js.map