@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.
- package/CHANGELOG.md +16 -0
- package/dist/config/elicitation-questions.json +5 -17
- package/dist/engine/cascade-hooks/registry.js +0 -2
- package/dist/engine/challenge-scenarios-loader.d.ts +3 -3
- package/dist/engine/compliance-injector.d.ts +3 -3
- package/dist/engine/dashboard/kanban-logic.d.ts +1 -1
- package/dist/engine/doc-generator/portal/risk-matrix-svg.d.ts +1 -1
- package/dist/engine/doc-generator/proposal/proposal-generator.d.ts +1 -1
- package/dist/engine/elicitation/answer-extractor.d.ts +1 -4
- package/dist/engine/elicitation/answer-extractor.js +197 -68
- package/dist/engine/elicitation/decision-gap-detector.js +59 -79
- package/dist/engine/elicitation/index.d.ts +0 -1
- package/dist/engine/elicitation/index.js +0 -1
- package/dist/engine/elicitation/question-generator.js +10 -3
- package/dist/engine/elicitation/question-grounding-gate.js +83 -34
- package/dist/engine/evidence-gates/lifecycle-gate.js +27 -14
- package/dist/engine/evidence-index/index-builder.js +7 -2
- package/dist/engine/frontmatter-sha/frozen-fields.d.ts +1 -1
- package/dist/engine/handoff-artifacts/schemas/red-tests-handoff.d.ts +2 -2
- package/dist/engine/handoff-artifacts/schemas.d.ts +22 -22
- package/dist/engine/human-summary.js +3 -3
- package/dist/engine/implementation-contract/common.d.ts +1 -1
- package/dist/engine/implementation-contract/renderer.js +17 -39
- package/dist/engine/keyword-matcher/stopwords.d.ts +2 -2
- package/dist/engine/lifecycle-hints.d.ts +1 -1
- package/dist/engine/lifecycle-hints.js +13 -29
- package/dist/engine/provider-adapters/adapters/claude.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/gemini.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/gpt4.d.ts +1 -1
- package/dist/engine/provider-adapters/adapters/markdown.d.ts +1 -1
- package/dist/engine/safety/atomic-write-file.d.ts +1 -1
- package/dist/engine/safety/cross-process-lock.d.ts +1 -1
- package/dist/engine/safety/path-sanitizer.d.ts +1 -1
- package/dist/engine/security-analyzer/profile-detector.js +48 -16
- package/dist/engine/spec-format/copula-blacklist.d.ts +2 -2
- package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
- package/dist/engine/spec-format/lean-spec-generator.js +9 -10
- package/dist/engine/spec-format/metadata-value-policy.d.ts +20 -20
- package/dist/engine/spec-generator/fallback-generator.js +13 -20
- package/dist/engine/spec-grounding/contract.d.ts +5 -0
- package/dist/engine/spec-grounding/contract.js +10 -6
- package/dist/engine/spec-migrator/lean-migration.js +9 -16
- package/dist/engine/text-signal-boundaries.js +45 -12
- package/dist/engine/universal-rules/rules/planu-modes.js +0 -2
- package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
- package/dist/engine/web-fetcher/stack-advisor.js +78 -17
- package/dist/engine/well-known/manifest-schema.d.ts +33 -33
- package/dist/engine/workers/schema.d.ts +3 -3
- package/dist/hosts/claude-code/ux/mcp-resources.d.ts +4 -4
- package/dist/hosts/codex/rest-shim.d.ts +1 -1
- package/dist/native/lightweight-command-catalog.d.ts +30 -30
- package/dist/server/openapi-spec.d.ts +132 -132
- package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
- package/dist/tools/challenge-spec/challenge-report.js +55 -0
- package/dist/tools/challenge-spec/event-challenge-scenarios.js +8 -27
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-a.js +16 -32
- package/dist/tools/challenge-spec/resilience-challenge-scenarios-b.js +15 -20
- package/dist/tools/challenge-spec/scenario-collector.d.ts +13 -0
- package/dist/tools/challenge-spec/scenario-collector.js +86 -0
- package/dist/tools/challenge-spec/scenarios-data.js +10 -9
- package/dist/tools/challenge-spec/scenarios-failure.js +38 -54
- package/dist/tools/challenge-spec/scenarios-scale.js +14 -15
- package/dist/tools/challenge-spec/scenarios-security.d.ts +1 -1
- package/dist/tools/challenge-spec/scenarios-security.js +18 -11
- package/dist/tools/challenge-spec/scenarios-utils.d.ts +31 -5
- package/dist/tools/challenge-spec/scenarios-utils.js +102 -6
- package/dist/tools/challenge-spec/security-challenge-scenarios.d.ts +1 -1
- package/dist/tools/challenge-spec/security-challenge-scenarios.js +20 -25
- package/dist/tools/challenge-spec.js +46 -124
- package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
- package/dist/tools/clarify-requirements/questions-context.js +11 -11
- package/dist/tools/clarify-requirements/questions.js +3 -23
- package/dist/tools/clarify-requirements.js +38 -0
- package/dist/tools/compliance-gate-handler.d.ts +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
- package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
- package/dist/tools/create-spec/post-creation.js +23 -4
- package/dist/tools/create-spec.js +51 -168
- package/dist/tools/delete-first/suggest-deletions.d.ts +1 -1
- package/dist/tools/elicit-requirements-handler.js +20 -12
- package/dist/tools/generate-batch-script.d.ts +2 -2
- package/dist/tools/init-project/rules-generator.js +0 -1
- package/dist/tools/oauth-handler.d.ts +1 -1
- package/dist/tools/register-sdd-tools.d.ts +1 -1
- package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
- package/dist/tools/render-spec-for-provider.d.ts +2 -2
- package/dist/tools/scan-orphan-spec-refs.d.ts +6 -6
- package/dist/tools/schemas/agents.d.ts +7 -7
- package/dist/tools/schemas/analysis.d.ts +11 -11
- package/dist/tools/schemas/delete.d.ts +5 -5
- package/dist/tools/schemas/github.d.ts +4 -4
- package/dist/tools/schemas/ide-config.d.ts +12 -12
- package/dist/tools/schemas/infra.d.ts +9 -9
- package/dist/tools/schemas/lifecycle.d.ts +50 -50
- package/dist/tools/schemas/llm-provider-schemas.d.ts +7 -7
- package/dist/tools/schemas/plugins-schemas.d.ts +12 -12
- package/dist/tools/schemas/project-graph.d.ts +6 -6
- package/dist/tools/schemas/rbac.d.ts +2 -2
- package/dist/tools/schemas/registry.d.ts +3 -3
- package/dist/tools/schemas/runtime-security.d.ts +2 -2
- package/dist/tools/schemas/session.d.ts +2 -2
- package/dist/tools/schemas/spec.d.ts +15 -15
- package/dist/tools/schemas/token-intelligence.d.ts +6 -6
- package/dist/tools/schemas/token-optimization.d.ts +2 -2
- package/dist/tools/schemas/workers-schema.d.ts +4 -4
- package/dist/tools/skill-bootstrap-handler.d.ts +7 -7
- package/dist/tools/suggest-stack.js +49 -13
- package/dist/tools/tool-registry/deprecated-stubs.d.ts +1 -1
- package/dist/tools/triage-request.d.ts +3 -3
- package/dist/tools/update-status/dod-gates.d.ts +8 -0
- package/dist/tools/update-status/dod-gates.js +68 -43
- package/dist/tools/update-status/file-sync.d.ts +1 -1
- package/dist/tools/update-status/file-sync.js +10 -45
- package/dist/tools/update-status/index.js +133 -97
- package/dist/tools/update-status/qa-gate.js +26 -0
- package/dist/tools/update-status/response-builder.d.ts +1 -1
- package/dist/tools/update-status/response-builder.js +11 -19
- package/dist/tools/update-status/transition-guard.d.ts +2 -2
- package/dist/tools/update-status/transition-guard.js +11 -8
- package/dist/tools/update-status-actions.d.ts +0 -1
- package/dist/tools/update-status-actions.js +10 -26
- package/dist/tools/update-status-convention-gate.d.ts +1 -6
- package/dist/tools/update-status-convention-gate.js +9 -33
- package/dist/tools/validate-lint.d.ts +1 -1
- package/dist/tools/validate-lint.js +39 -1
- package/dist/types/docs.d.ts +0 -4
- package/dist/types/env.d.ts +0 -4
- package/dist/types/estimation.d.ts +0 -2
- package/dist/types/multi-teammate-review.d.ts +4 -4
- package/dist/types/plugin-install.d.ts +7 -7
- package/dist/types/spec/core.d.ts +20 -0
- package/dist/types/spec/inputs.d.ts +1 -1
- package/dist/types/stack/index.d.ts +1 -1
- package/dist/types/stack/recommend.d.ts +10 -0
- package/package.json +21 -18
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/src/i18n/messages/en.json +0 -72
- package/src/i18n/messages/es.json +0 -72
- package/src/i18n/messages/pt.json +0 -72
- package/dist/config/elicitation-dimensions.json +0 -51
- package/dist/engine/actuals-estimator.d.ts +0 -14
- package/dist/engine/actuals-estimator.js +0 -64
- package/dist/engine/cascade-hooks/hooks/living-specs.hook.d.ts +0 -3
- package/dist/engine/cascade-hooks/hooks/living-specs.hook.js +0 -19
- package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
- package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
- package/dist/engine/elicitation/option-builder.d.ts +0 -9
- package/dist/engine/elicitation/option-builder.js +0 -85
- package/dist/engine/progress-writer.d.ts +0 -10
- package/dist/engine/progress-writer.js +0 -63
- package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
- package/dist/tools/clarify-requirements/interview-mode.js +0 -342
- package/dist/tools/update-status/mode-hints.d.ts +0 -4
- 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 &&
|
|
11
|
-
gaps.push(
|
|
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-
|
|
20
|
+
id: 'permission-boundary',
|
|
22
21
|
kind: 'permission',
|
|
23
|
-
header: '
|
|
24
|
-
question: `For ${subject},
|
|
25
|
-
evidence: evidenceFrom(signals, ['permission
|
|
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
|
-
['
|
|
29
|
-
[
|
|
30
|
-
|
|
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},
|
|
44
|
-
evidence: evidenceFrom(signals, ['billing
|
|
45
|
-
impact: 'Changes integration
|
|
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
|
-
'
|
|
51
|
-
'
|
|
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-
|
|
66
|
+
id: 'data-boundary',
|
|
82
67
|
kind: 'data',
|
|
83
68
|
header: 'Data',
|
|
84
|
-
question: `For ${subject},
|
|
85
|
-
evidence: evidenceFrom(signals, ['
|
|
86
|
-
impact: 'Changes
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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-
|
|
86
|
+
id: 'failure-boundary',
|
|
106
87
|
kind: 'failure',
|
|
107
88
|
header: 'Failure',
|
|
108
|
-
question: `For ${subject},
|
|
109
|
-
evidence: evidenceFrom(signals,
|
|
110
|
-
impact: 'Changes error
|
|
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
|
-
[
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
101
|
+
function buildScopeGap(signals) {
|
|
119
102
|
const subject = formatSubject(signals);
|
|
120
103
|
return makeGap({
|
|
121
|
-
id: '
|
|
122
|
-
kind: '
|
|
123
|
-
header: '
|
|
124
|
-
question: `For ${subject},
|
|
125
|
-
evidence: evidenceFrom(signals, ['short
|
|
126
|
-
impact: 'Changes the
|
|
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
|
-
['
|
|
129
|
-
['
|
|
130
|
-
['
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
|
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
|
|
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|\
|
|
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
|
-
|
|
73
|
+
const optionFailure = validateOptions(question, context);
|
|
74
|
+
return optionFailure ?? { passed: true };
|
|
31
75
|
}
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
.
|
|
36
|
-
|
|
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:
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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,
|
|
37
|
-
if (
|
|
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
|
-
|
|
159
|
-
|
|
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 [
|
|
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
|
-
|
|
11
|
+
playwright: "playwright";
|
|
12
12
|
pytest: "pytest";
|
|
13
13
|
rspec: "rspec";
|
|
14
|
-
|
|
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]>>;
|