@planu/cli 4.11.1 → 4.11.3

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 (155) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/dist/config/elicitation-questions.json +5 -17
  3. package/dist/engine/cascade-hooks/registry.js +0 -2
  4. package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
  5. package/dist/engine/compliance-injector.d.ts +3 -3
  6. package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
  7. package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
  8. package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
  9. package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
  10. package/dist/engine/elicitation/answer-extractor.js +197 -68
  11. package/dist/engine/elicitation/decision-gap-detector.js +59 -79
  12. package/dist/engine/elicitation/index.d.ts +0 -1
  13. package/dist/engine/elicitation/index.js +0 -1
  14. package/dist/engine/elicitation/question-generator.js +10 -3
  15. package/dist/engine/elicitation/question-grounding-gate.js +83 -34
  16. package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
  17. package/dist/engine/evidence-index/index-builder.js +7 -2
  18. package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
  19. package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
  20. package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
  21. package/dist/engine/human-summary.js +3 -3
  22. package/dist/engine/implementation-contract/common.d.ts +1 -1
  23. package/dist/engine/implementation-contract/renderer.js +17 -39
  24. package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
  25. package/dist/engine/lifecycle-hints.d.ts +1 -1
  26. package/dist/engine/lifecycle-hints.js +13 -29
  27. package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
  28. package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
  29. package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
  30. package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
  31. package/dist/engine/safety/atomic-write-file.d.ts +1 -1
  32. package/dist/engine/safety/cross-process-lock.d.ts +1 -1
  33. package/dist/engine/safety/path-sanitizer.d.ts +1 -1
  34. package/dist/engine/security-analyzer/profile-detector.js +48 -16
  35. package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
  36. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  37. package/dist/engine/spec-format/lean-spec-generator.js +9 -10
  38. package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
  39. package/dist/engine/spec-generator/fallback-generator.js +13 -20
  40. package/dist/engine/spec-grounding/contract.d.ts +5 -0
  41. package/dist/engine/spec-grounding/contract.js +10 -6
  42. package/dist/engine/spec-migrator/lean-migration.js +9 -16
  43. package/dist/engine/text-signal-boundaries.js +45 -12
  44. package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
  45. package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
  46. package/dist/engine/web-fetcher/stack-advisor.js +78 -17
  47. package/dist/engine/well-known/manifest-schema.d.ts +33 -33
  48. package/dist/engine/workers/schema.d.ts +3 -3
  49. package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
  50. package/dist/hosts/codex/rest-shim.d.ts +1 -1
  51. package/dist/native/lightweight-command-catalog.d.ts +30 -30
  52. package/dist/server/openapi-spec.d.ts +132 -132
  53. package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
  54. package/dist/tools/challenge-spec/challenge-report.js +55 -0
  55. package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
  56. package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
  57. package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
  58. package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
  59. package/dist/tools/challenge-spec/scenario-collector.js +86 -0
  60. package/dist/tools/challenge-spec/scenarios-data.js +10 -9
  61. package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
  62. package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
  63. package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
  64. package/dist/tools/challenge-spec/scenarios-security.js +18 -11
  65. package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
  66. package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
  67. package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
  68. package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
  69. package/dist/tools/challenge-spec.js +46 -124
  70. package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
  71. package/dist/tools/clarify-requirements/questions-context.js +11 -11
  72. package/dist/tools/clarify-requirements/questions.js +3 -23
  73. package/dist/tools/clarify-requirements.js +38 -0
  74. package/dist/tools/compliance-gate-handler.d.ts +1 -1
  75. package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
  76. package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
  77. package/dist/tools/create-spec/post-creation.js +23 -4
  78. package/dist/tools/create-spec.js +51 -168
  79. package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
  80. package/dist/tools/elicit-requirements-handler.js +20 -12
  81. package/dist/tools/generate-batch-script.d.ts +2 -2
  82. package/dist/tools/init-project/rules-generator.js +0 -1
  83. package/dist/tools/oauth-handler.d.ts +1 -1
  84. package/dist/tools/register-sdd-tools.d.ts +1 -1
  85. package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
  86. package/dist/tools/render-spec-for-provider.d.ts +2 -2
  87. package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
  88. package/dist/tools/schemas/agents.d.ts +7 -7
  89. package/dist/tools/schemas/analysis.d.ts +11 -11
  90. package/dist/tools/schemas/delete.d.ts +5 -5
  91. package/dist/tools/schemas/github.d.ts +4 -4
  92. package/dist/tools/schemas/ide-config.d.ts +12 -12
  93. package/dist/tools/schemas/infra.d.ts +9 -9
  94. package/dist/tools/schemas/lifecycle.d.ts +50 -50
  95. package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
  96. package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
  97. package/dist/tools/schemas/project-graph.d.ts +6 -6
  98. package/dist/tools/schemas/rbac.d.ts +2 -2
  99. package/dist/tools/schemas/registry.d.ts +3 -3
  100. package/dist/tools/schemas/runtime-security.d.ts +2 -2
  101. package/dist/tools/schemas/session.d.ts +2 -2
  102. package/dist/tools/schemas/spec.d.ts +15 -15
  103. package/dist/tools/schemas/token-intelligence.d.ts +6 -6
  104. package/dist/tools/schemas/token-optimization.d.ts +2 -2
  105. package/dist/tools/schemas/workers-schema.d.ts +4 -4
  106. package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
  107. package/dist/tools/suggest-stack.js +49 -13
  108. package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
  109. package/dist/tools/triage-request.d.ts +3 -3
  110. package/dist/tools/update-status/dod-gates.d.ts +8 -0
  111. package/dist/tools/update-status/dod-gates.js +68 -43
  112. package/dist/tools/update-status/file-sync.d.ts +1 -1
  113. package/dist/tools/update-status/file-sync.js +10 -45
  114. package/dist/tools/update-status/index.js +133 -97
  115. package/dist/tools/update-status/qa-gate.js +26 -0
  116. package/dist/tools/update-status/response-builder.d.ts +1 -1
  117. package/dist/tools/update-status/response-builder.js +11 -19
  118. package/dist/tools/update-status/transition-guard.d.ts +2 -2
  119. package/dist/tools/update-status/transition-guard.js +11 -8
  120. package/dist/tools/update-status-actions.d.ts +0 -1
  121. package/dist/tools/update-status-actions.js +10 -26
  122. package/dist/tools/update-status-convention-gate.d.ts +1 -6
  123. package/dist/tools/update-status-convention-gate.js +9 -33
  124. package/dist/tools/validate-lint.d.ts +1 -1
  125. package/dist/tools/validate-lint.js +39 -1
  126. package/dist/types/docs.d.ts +0 -4
  127. package/dist/types/env.d.ts +0 -4
  128. package/dist/types/estimation.d.ts +0 -2
  129. package/dist/types/multi-teammate-review.d.ts +4 -4
  130. package/dist/types/plugin-install.d.ts +7 -7
  131. package/dist/types/spec/core.d.ts +20 -0
  132. package/dist/types/spec/inputs.d.ts +1 -1
  133. package/dist/types/stack/index.d.ts +1 -1
  134. package/dist/types/stack/recommend.d.ts +10 -0
  135. package/package.json +21 -18
  136. package/planu-native.json +1 -1
  137. package/planu-plugin.json +1 -1
  138. package/src/i18n/messages/en.json +0 -72
  139. package/src/i18n/messages/es.json +0 -72
  140. package/src/i18n/messages/pt.json +0 -72
  141. package/dist/config/elicitation-dimensions.json +0 -51
  142. package/dist/engine/actuals-estimator.d.ts +0 -14
  143. package/dist/engine/actuals-estimator.js +0 -64
  144. package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
  145. package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
  146. package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
  147. package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
  148. package/dist/engine/elicitation/option-builder.d.ts +0 -9
  149. package/dist/engine/elicitation/option-builder.js +0 -85
  150. package/dist/engine/progress-writer.d.ts +0 -10
  151. package/dist/engine/progress-writer.js +0 -63
  152. package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
  153. package/dist/tools/clarify-requirements/interview-mode.js +0 -342
  154. package/dist/tools/update-status/mode-hints.d.ts +0 -4
  155. package/dist/tools/update-status/mode-hints.js +0 -24
@@ -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
@@ -46,13 +46,20 @@ function shouldInclude(config, lowerDesc, context) {
46
46
  if (context?.hasDatabase === true) {
47
47
  return true;
48
48
  }
49
- return (config.conditionKeywords ?? []).some((kw) => lowerDesc.includes(kw));
49
+ return (config.conditionKeywords ?? []).some((keyword) => hasAffirmedKeyword(lowerDesc, keyword));
50
50
  }
51
51
  // condition === 'api' is the only remaining case
52
52
  if (context?.isApi === true) {
53
53
  return true;
54
54
  }
55
- return (config.conditionKeywords ?? []).some((kw) => lowerDesc.includes(kw));
55
+ return (config.conditionKeywords ?? []).some((keyword) => hasAffirmedKeyword(lowerDesc, keyword));
56
+ }
57
+ const NEGATION_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope)\b/i;
58
+ function hasAffirmedKeyword(description, keyword) {
59
+ return description.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
60
+ const index = segment.indexOf(keyword.toLowerCase());
61
+ return index >= 0 && !NEGATION_RE.test(segment.slice(0, index));
62
+ });
56
63
  }
57
64
  /** Generate standard clarification questions for a feature spec */
58
65
  export function generateSpecQuestions(title, description, context) {
@@ -101,7 +108,7 @@ export function formatQuestionsAsPrompt(session) {
101
108
  lines.push('');
102
109
  });
103
110
  lines.push('---');
104
- lines.push('*Reply with your answers (e.g., "1. Admin users 2. User sees dashboard with charts...")*');
111
+ lines.push('*Reply with numbered answers that reflect only confirmed requirements.*');
105
112
  return lines.join('\n');
106
113
  }
107
114
  /** Parse answers from free-form text (numbered list format) */
@@ -6,49 +6,98 @@ const GENERIC_QUESTION_PATTERNS = [
6
6
  /^what data is needed\??$/i,
7
7
  /^what are the requirements\??$/i,
8
8
  ];
9
+ const REQUIRED_EVIDENCE = {
10
+ permission: ['capability:permission'],
11
+ provider: ['domain:billing'],
12
+ 'billing-model': ['domain:billing'],
13
+ data: ['domain:data'],
14
+ failure: ['integration:'],
15
+ behavior: ['request:short-ambiguous'],
16
+ scope: ['request:short-ambiguous'],
17
+ };
18
+ const OPTION_CONCEPTS = [
19
+ [/\b(payment|billing|checkout|subscription|invoice|provider|vendor)\b/i, ['domain:billing']],
20
+ [/\b(database|schema|table|sql|persistence)\b/i, ['domain:data']],
21
+ [/\b(admin|role|auth|authorization|permission|access control)\b/i, ['capability:permission']],
22
+ [/\b(retry|backoff|queue|queued)\b/i, ['capability:retry']],
23
+ [/\b(docker|kubernetes|cloud|serverless)\b/i, ['capability:infrastructure']],
24
+ ];
25
+ const NEUTRAL_TITLE_WORDS = new Set([
26
+ 'access',
27
+ 'apply',
28
+ 'behavior',
29
+ 'bounded',
30
+ 'current',
31
+ 'data',
32
+ 'define',
33
+ 'describe',
34
+ 'existing',
35
+ 'failure',
36
+ 'in',
37
+ 'include',
38
+ 'keep',
39
+ 'limit',
40
+ 'maintenance',
41
+ 'new',
42
+ 'no',
43
+ 'out',
44
+ 'project',
45
+ 'provider',
46
+ 'request',
47
+ 'scope',
48
+ 'selection',
49
+ 'the',
50
+ 'use',
51
+ ]);
9
52
  export function validateQuestionGrounding(question, context) {
10
53
  const text = question.question.trim();
11
54
  if (GENERIC_QUESTION_PATTERNS.some((pattern) => pattern.test(text))) {
12
55
  return { passed: false, reason: 'Question is a generic reusable prompt.' };
13
56
  }
57
+ const requiredMarkers = REQUIRED_EVIDENCE[context.gap.kind] ?? [];
58
+ if (!requiredMarkers.some((marker) => hasEvidenceMarker(context.gap.evidence, marker))) {
59
+ return { passed: false, reason: 'Gap lacks affirmed evidence for this decision kind.' };
60
+ }
61
+ const groundingPhrases = context.gap.evidence
62
+ .filter((item) => /^(?:action|object|integration|project):/i.test(item))
63
+ .map((item) => item.slice(item.indexOf(':') + 1).toLowerCase())
64
+ .filter((item) => item.length > 1);
14
65
  const lowerQuestion = text.toLowerCase();
15
- const lowerEvidence = [context.requestText, ...context.gap.evidence].join(' ').toLowerCase();
16
- const evidenceTokens = tokenSet(lowerEvidence);
17
- const hasRequestEvidence = Array.from(evidenceTokens).some((token) => token.length >= 3 && lowerQuestion.includes(token));
66
+ const hasRequestEvidence = groundingPhrases.some((phrase) => lowerQuestion.includes(phrase));
18
67
  if (!hasRequestEvidence) {
19
- return {
20
- passed: false,
21
- reason: 'Question does not include evidence from the request or detected gap.',
22
- };
68
+ return { passed: false, reason: 'Question does not include concrete request evidence.' };
23
69
  }
24
- if (!/\bchanges?\b|\bthis changes\b|\baffects?\b|\bbecause\b|\bbecomes?\b/i.test(text)) {
25
- return {
26
- passed: false,
27
- reason: 'Question does not explain why the answer matters for the spec.',
28
- };
70
+ if (!/\bchanges?\b|\baffects?\b|\bbecause\b|\bbecomes?\b/i.test(text)) {
71
+ return { passed: false, reason: 'Question does not explain why the answer matters.' };
29
72
  }
30
- return { passed: true };
73
+ const optionFailure = validateOptions(question, context);
74
+ return optionFailure ?? { passed: true };
31
75
  }
32
- function tokenSet(text) {
33
- return new Set(text
34
- .replace(/[^a-z0-9 ]/gi, ' ')
35
- .split(/\s+/)
36
- .filter((token) => !STOP_WORDS.has(token) && token.length > 2));
76
+ function validateOptions(question, context) {
77
+ const evidenceText = `${context.requestText} ${context.gap.evidence.join(' ')}`.toLowerCase();
78
+ for (const option of question.options) {
79
+ const optionText = `${option.label} ${option.description}`;
80
+ for (const [pattern, markers] of OPTION_CONCEPTS) {
81
+ if (pattern.test(optionText) &&
82
+ !markers.some((marker) => hasEvidenceMarker(context.gap.evidence, marker))) {
83
+ return { passed: false, reason: 'Option introduces a domain concept without evidence.' };
84
+ }
85
+ }
86
+ const properNames = optionText.match(/\b(?:[A-Z]{2,}|[A-Z][a-z]+(?:[A-Z][A-Za-z]+)?)\b/g) ?? [];
87
+ const unsupported = properNames.find((name) => {
88
+ const lower = name.toLowerCase();
89
+ return !NEUTRAL_TITLE_WORDS.has(lower) && !evidenceText.includes(lower);
90
+ });
91
+ if (unsupported !== undefined) {
92
+ return {
93
+ passed: false,
94
+ reason: `Option introduces unsupported named candidate: ${unsupported}.`,
95
+ };
96
+ }
97
+ }
98
+ return null;
99
+ }
100
+ function hasEvidenceMarker(evidence, marker) {
101
+ return evidence.some((item) => item.toLowerCase().startsWith(marker));
37
102
  }
38
- const STOP_WORDS = new Set([
39
- 'the',
40
- 'and',
41
- 'for',
42
- 'this',
43
- 'that',
44
- 'with',
45
- 'from',
46
- 'what',
47
- 'when',
48
- 'where',
49
- 'should',
50
- 'action',
51
- 'object',
52
- 'risk',
53
- ]);
54
103
  //# sourceMappingURL=question-grounding-gate.js.map
@@ -1,26 +1,39 @@
1
+ import { hasAnyAffirmedMatch } from '../text-signal-boundaries.js';
1
2
  const CONTRACT_HINTS = {
2
- api: /\b(api|rest|endpoint|openapi|http route|controller)\b/i,
3
- graphql: /\b(graphql|gql|schema\.graphql|resolver)\b/i,
4
- event: /\b(event|webhook|queue|topic|message broker|kafka|rabbitmq|pubsub)\b/i,
5
- ui: /\b(ui contract|component contract|screen contract|design contract)\b/i,
6
- mcp: /\b(mcp|tool contract|model context protocol|registerTool|server tool)\b/i,
3
+ api: [
4
+ /\b(?:public|external|rest|http)[ -]?api\b/i,
5
+ /\bapi\s+(?:endpoint|route|request|response|contract)\b/i,
6
+ /\b(?:rest|http)\s+(?:endpoint|route|contract)\b/i,
7
+ /\bopenapi\b/i,
8
+ ],
9
+ graphql: [/\b(?:graphql|gql)\s+(?:api|schema|resolver|contract)\b/i, /\bschema\.graphql\b/i],
10
+ event: [
11
+ /\b(?:event|message)\s+(?:contract|schema|producer|consumer|handler|queue|topic)\b/i,
12
+ /\b(?:message\s+broker|kafka|rabbitmq|pubsub|webhook\s+(?:delivery|contract|payload))\b/i,
13
+ ],
14
+ ui: [/\b(?:ui|component|screen|design)\s+contract\b/i],
15
+ mcp: [
16
+ /\bmcp\s+(?:tool|contract|server|protocol)\b/i,
17
+ /\b(?:tool\s+contract|model\s+context\s+protocol|registerTool|server\s+tool)\b/i,
18
+ ],
7
19
  };
8
20
  function isNonTrivial(spec) {
9
21
  return spec.scope !== 'trivial';
10
22
  }
11
23
  function normalizeCriterion(value) {
12
- return value
13
- .replace(/^-\s*\[[ x]\]\s*/i, '')
14
- .replace(/^(AC\d+)\s*[:.)-]?\s*/i, '')
24
+ const withoutCheckbox = value.replace(/^-\s*\[[ x]\]\s*/i, '').trim();
25
+ const criterionOnly = /^(AC\d+)\s*[:.)-]?\s*$/i.exec(withoutCheckbox)?.[1];
26
+ if (criterionOnly) {
27
+ return criterionOnly.toLowerCase();
28
+ }
29
+ return withoutCheckbox
30
+ .replace(/^AC\d+\s*[:.)-]?\s*/i, '')
15
31
  .replace(/\s+/g, ' ')
16
32
  .trim()
17
33
  .toLowerCase();
18
34
  }
19
35
  function criterionAliases(value, index) {
20
- const aliases = new Set([
21
- normalizeCriterion(value),
22
- normalizeCriterion(`AC${String(index + 1)}`),
23
- ]);
36
+ const aliases = new Set([normalizeCriterion(value), `ac${String(index + 1)}`]);
24
37
  const explicit = /^(AC\d+)\b/i.exec(value)?.[1];
25
38
  if (explicit) {
26
39
  aliases.add(normalizeCriterion(explicit));
@@ -33,8 +46,8 @@ function hasText(value) {
33
46
  export function inferRequiredContractEvidence(spec, criteria) {
34
47
  const text = `${spec.title} ${spec.tags.join(' ')} ${criteria.join(' ')}`;
35
48
  const required = [];
36
- for (const [kind, pattern] of Object.entries(CONTRACT_HINTS)) {
37
- if (pattern.test(text)) {
49
+ for (const [kind, patterns] of Object.entries(CONTRACT_HINTS)) {
50
+ if (hasAnyAffirmedMatch(text, [...patterns])) {
38
51
  required.push(kind);
39
52
  }
40
53
  }
@@ -155,8 +155,13 @@ function record(kind, value, status, source, reason) {
155
155
  return { kind, value, status, source, ...(reason ? { reason } : {}) };
156
156
  }
157
157
  function normalizeCriterion(value) {
158
- return value
159
- .replace(/^-\s*\[[ x]\]\s*/i, '')
158
+ const withoutCheckbox = value.replace(/^-\s*\[[ x]\]\s*/i, '').trim();
159
+ const criterionOnly = /^(AC\d+)\s*[:.)-]?\s*$/i.exec(withoutCheckbox)?.[1];
160
+ if (criterionOnly) {
161
+ return criterionOnly.toLowerCase();
162
+ }
163
+ return withoutCheckbox
164
+ .replace(/^AC\d+\s*[:.)-]?\s*/i, '')
160
165
  .replace(/\s+/g, ' ')
161
166
  .trim()
162
167
  .toLowerCase();
@@ -2,6 +2,6 @@
2
2
  * Frontmatter fields that become immutable when a spec transitions to a terminal state
3
3
  * (done or discarded). Any change to these fields after the SHA freeze is a tamper signal.
4
4
  */
5
- export declare const FROZEN_FIELDS: readonly ["status", "scenarios", "spec_version", "id", "type", "target"];
5
+ export declare const FROZEN_FIELDS: readonly ['status', 'scenarios', 'spec_version', 'id', 'type', 'target'];
6
6
  export type FrozenField = (typeof FROZEN_FIELDS)[number];
7
7
  //# sourceMappingURL=frozen-fields.d.ts.map
@@ -8,10 +8,10 @@ export declare const RedTestsHandoffSchema: z.ZodObject<{
8
8
  testFiles: z.ZodArray<z.ZodString>;
9
9
  testFramework: z.ZodEnum<{
10
10
  jest: "jest";
11
- vitest: "vitest";
11
+ playwright: "playwright";
12
12
  pytest: "pytest";
13
13
  rspec: "rspec";
14
- playwright: "playwright";
14
+ vitest: "vitest";
15
15
  }>;
16
16
  expectedFailures: z.ZodArray<z.ZodString>;
17
17
  authoredBy: z.ZodString;
@@ -5,11 +5,11 @@ export declare const SchemaVersion: z.ZodString;
5
5
  export declare const IntakeV1Schema: z.ZodObject<{
6
6
  schema_version: z.ZodString;
7
7
  kind: z.ZodEnum<{
8
- idea: "idea";
9
- "quick-fix": "quick-fix";
10
8
  "bug-spec": "bug-spec";
11
- "feature-spec": "feature-spec";
12
9
  epic: "epic";
10
+ "feature-spec": "feature-spec";
11
+ idea: "idea";
12
+ "quick-fix": "quick-fix";
13
13
  }>;
14
14
  description: z.ZodString;
15
15
  confidence: z.ZodNumber;
@@ -38,9 +38,9 @@ export declare const ReviewFeedbackV1Schema: z.ZodObject<{
38
38
  }>;
39
39
  reviewer: z.ZodObject<{
40
40
  kind: z.ZodEnum<{
41
+ automation: "automation";
41
42
  human: "human";
42
43
  "spec-review-agent": "spec-review-agent";
43
- automation: "automation";
44
44
  }>;
45
45
  agent: z.ZodString;
46
46
  verdict: z.ZodEnum<{
@@ -73,8 +73,8 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
73
73
  }, z.core.$strip>>;
74
74
  reviewer: z.ZodObject<{
75
75
  kind: z.ZodEnum<{
76
- human: "human";
77
76
  automation: "automation";
77
+ human: "human";
78
78
  "implementation-review-agent": "implementation-review-agent";
79
79
  }>;
80
80
  agent: z.ZodString;
@@ -104,13 +104,13 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
104
104
  tag: z.ZodString;
105
105
  severity: z.ZodEnum<{
106
106
  blocker: "blocker";
107
- warning: "warning";
108
107
  info: "info";
108
+ warning: "warning";
109
109
  }>;
110
110
  confidence: z.ZodEnum<{
111
+ high: "high";
111
112
  low: "low";
112
113
  medium: "medium";
113
- high: "high";
114
114
  }>;
115
115
  target: z.ZodString;
116
116
  line: z.ZodOptional<z.ZodNumber>;
@@ -119,10 +119,10 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
119
119
  blocksDone: z.ZodBoolean;
120
120
  evidenceSource: z.ZodArray<z.ZodObject<{
121
121
  source: z.ZodEnum<{
122
- spec: "spec";
123
- runtime: "runtime";
124
122
  policy: "policy";
125
123
  "project-config": "project-config";
124
+ runtime: "runtime";
125
+ spec: "spec";
126
126
  }>;
127
127
  key: z.ZodString;
128
128
  value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
@@ -142,10 +142,10 @@ export declare const ValidationReportV1Schema: z.ZodObject<{
142
142
  }, z.core.$strip>>;
143
143
  evidence: z.ZodArray<z.ZodObject<{
144
144
  source: z.ZodEnum<{
145
- spec: "spec";
146
- runtime: "runtime";
147
145
  policy: "policy";
148
146
  "project-config": "project-config";
147
+ runtime: "runtime";
148
+ spec: "spec";
149
149
  }>;
150
150
  key: z.ZodString;
151
151
  value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
@@ -159,11 +159,11 @@ export declare const ARTIFACT_SCHEMAS: {
159
159
  readonly intake: z.ZodObject<{
160
160
  schema_version: z.ZodString;
161
161
  kind: z.ZodEnum<{
162
- idea: "idea";
163
- "quick-fix": "quick-fix";
164
162
  "bug-spec": "bug-spec";
165
- "feature-spec": "feature-spec";
166
163
  epic: "epic";
164
+ "feature-spec": "feature-spec";
165
+ idea: "idea";
166
+ "quick-fix": "quick-fix";
167
167
  }>;
168
168
  description: z.ZodString;
169
169
  confidence: z.ZodNumber;
@@ -192,9 +192,9 @@ export declare const ARTIFACT_SCHEMAS: {
192
192
  }>;
193
193
  reviewer: z.ZodObject<{
194
194
  kind: z.ZodEnum<{
195
+ automation: "automation";
195
196
  human: "human";
196
197
  "spec-review-agent": "spec-review-agent";
197
- automation: "automation";
198
198
  }>;
199
199
  agent: z.ZodString;
200
200
  verdict: z.ZodEnum<{
@@ -227,8 +227,8 @@ export declare const ARTIFACT_SCHEMAS: {
227
227
  }, z.core.$strip>>;
228
228
  reviewer: z.ZodObject<{
229
229
  kind: z.ZodEnum<{
230
- human: "human";
231
230
  automation: "automation";
231
+ human: "human";
232
232
  "implementation-review-agent": "implementation-review-agent";
233
233
  }>;
234
234
  agent: z.ZodString;
@@ -258,13 +258,13 @@ export declare const ARTIFACT_SCHEMAS: {
258
258
  tag: z.ZodString;
259
259
  severity: z.ZodEnum<{
260
260
  blocker: "blocker";
261
- warning: "warning";
262
261
  info: "info";
262
+ warning: "warning";
263
263
  }>;
264
264
  confidence: z.ZodEnum<{
265
+ high: "high";
265
266
  low: "low";
266
267
  medium: "medium";
267
- high: "high";
268
268
  }>;
269
269
  target: z.ZodString;
270
270
  line: z.ZodOptional<z.ZodNumber>;
@@ -273,10 +273,10 @@ export declare const ARTIFACT_SCHEMAS: {
273
273
  blocksDone: z.ZodBoolean;
274
274
  evidenceSource: z.ZodArray<z.ZodObject<{
275
275
  source: z.ZodEnum<{
276
- spec: "spec";
277
- runtime: "runtime";
278
276
  policy: "policy";
279
277
  "project-config": "project-config";
278
+ runtime: "runtime";
279
+ spec: "spec";
280
280
  }>;
281
281
  key: z.ZodString;
282
282
  value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;
@@ -296,10 +296,10 @@ export declare const ARTIFACT_SCHEMAS: {
296
296
  }, z.core.$strip>>;
297
297
  evidence: z.ZodArray<z.ZodObject<{
298
298
  source: z.ZodEnum<{
299
- spec: "spec";
300
- runtime: "runtime";
301
299
  policy: "policy";
302
300
  "project-config": "project-config";
301
+ runtime: "runtime";
302
+ spec: "spec";
303
303
  }>;
304
304
  key: z.ZodString;
305
305
  value: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>;