@planu/cli 4.11.2 → 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 +7 -0
- package/dist/config/elicitation-questions.json +5 -17
- package/dist/engine/elicitation/question-generator.js +10 -3
- package/dist/engine/implementation-contract/renderer.js +17 -39
- package/dist/engine/lifecycle-hints.d.ts +1 -1
- package/dist/engine/lifecycle-hints.js +13 -29
- 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-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/web-fetcher/stack-advisor.d.ts +2 -2
- package/dist/engine/web-fetcher/stack-advisor.js +78 -17
- 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.js +9 -8
- 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/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 +47 -144
- package/dist/tools/elicit-requirements-handler.js +20 -12
- package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
- package/dist/tools/suggest-stack.js +49 -13
- 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/index.js +109 -74
- package/dist/tools/update-status/qa-gate.js +26 -0
- package/dist/tools/update-status/transition-guard.d.ts +2 -2
- package/dist/tools/update-status/transition-guard.js +10 -7
- package/dist/tools/validate-lint.d.ts +1 -1
- package/dist/tools/validate-lint.js +39 -1
- 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 +9 -9
- package/planu-native.json +1 -1
- package/planu-plugin.json +1 -1
- package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
- package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
- package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
- package/dist/tools/clarify-requirements/interview-mode.js +0 -342
|
@@ -96,20 +96,24 @@ function getBaseRecommendations() {
|
|
|
96
96
|
},
|
|
97
97
|
};
|
|
98
98
|
}
|
|
99
|
-
function applyRequirementOverrides(rec,
|
|
99
|
+
function applyRequirementOverrides(rec, requirements, constraints) {
|
|
100
100
|
let result = rec;
|
|
101
|
-
if (
|
|
101
|
+
if (hasAffirmedTerm(requirements, /\bpython\b/i) ||
|
|
102
|
+
hasAffirmedTerm(requirements, /\bml\b/i) ||
|
|
103
|
+
hasAffirmedTerm(requirements, /\bai\b/i)) {
|
|
104
|
+
const requestsApi = hasAffirmedTerm(requirements, /\bapi\b/i);
|
|
102
105
|
result = {
|
|
103
106
|
...result,
|
|
104
107
|
language: 'python',
|
|
105
|
-
framework:
|
|
106
|
-
version:
|
|
107
|
-
ecosystem:
|
|
108
|
+
framework: requestsApi ? 'fastapi' : 'django',
|
|
109
|
+
version: requestsApi ? '0.100+' : '5+',
|
|
110
|
+
ecosystem: requestsApi
|
|
108
111
|
? ['fastapi', 'pydantic', 'sqlalchemy', 'alembic']
|
|
109
112
|
: ['django', 'django-rest-framework', 'celery', 'pytest'],
|
|
110
113
|
};
|
|
111
114
|
}
|
|
112
|
-
if (
|
|
115
|
+
if (hasAffirmedTerm(requirements, /\bgo(?:lang)?\b/i) ||
|
|
116
|
+
hasAffirmedTerm(requirements, /\bperformance\b/i)) {
|
|
113
117
|
result = {
|
|
114
118
|
...result,
|
|
115
119
|
language: 'go',
|
|
@@ -118,7 +122,7 @@ function applyRequirementOverrides(rec, reqStr, constStr) {
|
|
|
118
122
|
ecosystem: ['gin', 'gorm', 'wire', 'testify'],
|
|
119
123
|
};
|
|
120
124
|
}
|
|
121
|
-
if (
|
|
125
|
+
if (hasAffirmedTerm(requirements, /\bsupabase\b/i)) {
|
|
122
126
|
result = {
|
|
123
127
|
...result,
|
|
124
128
|
database: 'supabase',
|
|
@@ -126,22 +130,21 @@ function applyRequirementOverrides(rec, reqStr, constStr) {
|
|
|
126
130
|
auth: 'supabase-auth',
|
|
127
131
|
};
|
|
128
132
|
}
|
|
129
|
-
if (
|
|
133
|
+
if (hasAffirmedTerm(requirements, /\bfirebase\b/i)) {
|
|
130
134
|
result = { ...result, database: 'firebase', auth: 'firebase-auth' };
|
|
131
135
|
}
|
|
132
|
-
if (
|
|
136
|
+
if (constraints.some((constraint) => constraint.toLowerCase().includes('no-vendor-lock')) ||
|
|
137
|
+
hasAffirmedTerm(constraints, /\bself[- ]host(?:ed|ing)?\b/i)) {
|
|
133
138
|
result = { ...result, deployment: 'docker + railway', database: 'postgresql' };
|
|
134
139
|
}
|
|
135
140
|
return result;
|
|
136
141
|
}
|
|
137
142
|
export function recommendStack(projectType, requirements, constraints) {
|
|
138
|
-
const reqStr = requirements.join(' ').toLowerCase();
|
|
139
|
-
const constStr = constraints.join(' ').toLowerCase();
|
|
140
143
|
const recommendations = getBaseRecommendations();
|
|
141
144
|
const base =
|
|
142
145
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- 'web' key always exists in getBaseRecommendations()
|
|
143
146
|
recommendations[projectType] ?? recommendations.web;
|
|
144
|
-
return applyRequirementOverrides(base,
|
|
147
|
+
return applyRequirementOverrides(base, requirements, constraints);
|
|
145
148
|
}
|
|
146
149
|
export function recommendAlternativeStack(projectType, _requirements) {
|
|
147
150
|
const alternatives = {
|
|
@@ -208,23 +211,23 @@ export function buildWhyNotOthers(_projectType, chosenFramework) {
|
|
|
208
211
|
const allAlternatives = [
|
|
209
212
|
{
|
|
210
213
|
name: 'Ruby on Rails',
|
|
211
|
-
reason: '
|
|
214
|
+
reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
|
|
212
215
|
},
|
|
213
216
|
{
|
|
214
217
|
name: 'PHP/Laravel',
|
|
215
|
-
reason: '
|
|
218
|
+
reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
|
|
216
219
|
},
|
|
217
220
|
{
|
|
218
221
|
name: 'Java/Spring',
|
|
219
|
-
reason: '
|
|
222
|
+
reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
|
|
220
223
|
},
|
|
221
224
|
{
|
|
222
225
|
name: 'C#/.NET',
|
|
223
|
-
reason: '
|
|
226
|
+
reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
|
|
224
227
|
},
|
|
225
228
|
{
|
|
226
229
|
name: 'Elixir/Phoenix',
|
|
227
|
-
reason: '
|
|
230
|
+
reason: 'Optional candidate; compare it against the confirmed requirements before selection.',
|
|
228
231
|
},
|
|
229
232
|
];
|
|
230
233
|
return allAlternatives
|
|
@@ -307,6 +310,8 @@ export async function buildSuggestStackResult(projectType, requirements, constra
|
|
|
307
310
|
// Offline: keep static versions, mark confidence as low
|
|
308
311
|
confidence = 'low';
|
|
309
312
|
}
|
|
313
|
+
const detectedEvidence = collectDetectedEvidence(requirements, constraints);
|
|
314
|
+
const confirmedChoices = collectConfirmedChoices(requirements);
|
|
310
315
|
return {
|
|
311
316
|
primary,
|
|
312
317
|
alternative,
|
|
@@ -314,6 +319,62 @@ export async function buildSuggestStackResult(projectType, requirements, constra
|
|
|
314
319
|
mcpCompatibility: buildMcpCompatibility(primary),
|
|
315
320
|
lastChecked: new Date().toISOString(),
|
|
316
321
|
confidence,
|
|
322
|
+
selectionStatus: confirmedChoices.length > 0 ? 'partially-confirmed' : 'unconfirmed',
|
|
323
|
+
detectedEvidence,
|
|
324
|
+
confirmedChoices,
|
|
325
|
+
candidateNotice: 'Recommendations are optional candidates. No candidate is selected or persisted without user confirmation.',
|
|
317
326
|
};
|
|
318
327
|
}
|
|
328
|
+
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;
|
|
329
|
+
const NEGATIVE_SUFFIX_RE = /\b(?:forbidden|excluded|not allowed|out[- ]of[- ]scope)\b/i;
|
|
330
|
+
function hasAffirmedTerm(values, pattern) {
|
|
331
|
+
return values.some((value) => {
|
|
332
|
+
const withoutQuotedText = value
|
|
333
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
334
|
+
.replace(/`[^`]+`/g, ' ')
|
|
335
|
+
.replace(/"[^"]+"|'[^']+'/g, ' ');
|
|
336
|
+
return withoutQuotedText.split(/(?:\r?\n|[.,;!?]+)/).some((segment) => {
|
|
337
|
+
const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
|
|
338
|
+
if (!match) {
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
const prefix = segment.slice(0, match.index);
|
|
342
|
+
const suffix = segment.slice(match.index + match[0].length);
|
|
343
|
+
return !NEGATION_RE.test(prefix) && !NEGATIVE_SUFFIX_RE.test(suffix);
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
function collectDetectedEvidence(requirements, constraints) {
|
|
348
|
+
const definitions = [
|
|
349
|
+
['language:python', /\bpython\b/i],
|
|
350
|
+
['language:go', /\bgo(?:lang)?\b/i],
|
|
351
|
+
['database:supabase', /\bsupabase\b/i],
|
|
352
|
+
['database:firebase', /\bfirebase\b/i],
|
|
353
|
+
['capability:api', /\bapi\b/i],
|
|
354
|
+
['capability:ml', /\b(?:ml|ai)\b/i],
|
|
355
|
+
];
|
|
356
|
+
const evidence = definitions
|
|
357
|
+
.filter(([, pattern]) => hasAffirmedTerm(requirements, pattern))
|
|
358
|
+
.map(([name]) => `requirement:${name}`);
|
|
359
|
+
if (constraints.some((constraint) => constraint.toLowerCase().includes('no-vendor-lock'))) {
|
|
360
|
+
evidence.push('constraint:no-vendor-lock');
|
|
361
|
+
}
|
|
362
|
+
if (hasAffirmedTerm(constraints, /\bself[- ]host(?:ed|ing)?\b/i)) {
|
|
363
|
+
evidence.push('constraint:self-host');
|
|
364
|
+
}
|
|
365
|
+
return evidence;
|
|
366
|
+
}
|
|
367
|
+
function collectConfirmedChoices(requirements) {
|
|
368
|
+
const confirmationText = requirements.join(' ');
|
|
369
|
+
const choices = [
|
|
370
|
+
['language', 'python', /\b(?:must use|use|selected|confirmed|requires?)\s+python\b/i],
|
|
371
|
+
['language', 'go', /\b(?:must use|use|selected|confirmed|requires?)\s+go(?:lang)?\b/i],
|
|
372
|
+
['database', 'supabase', /\b(?:must use|use|selected|confirmed|requires?)\s+supabase\b/i],
|
|
373
|
+
['database', 'firebase', /\b(?:must use|use|selected|confirmed|requires?)\s+firebase\b/i],
|
|
374
|
+
];
|
|
375
|
+
return choices
|
|
376
|
+
.filter(([, , pattern]) => pattern.test(confirmationText))
|
|
377
|
+
.filter(([, value]) => hasAffirmedTerm(requirements, new RegExp(`\\b${value}\\b`, 'i')))
|
|
378
|
+
.map(([field, value]) => ({ field, value, source: 'user_confirmation' }));
|
|
379
|
+
}
|
|
319
380
|
//# sourceMappingURL=stack-advisor.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ChallengeReport, ChallengeResolutionEvidence, FailureScenario } from '../../types/index.js';
|
|
2
|
+
export declare const MINIMUM_RESOLVED_CHALLENGES = 3;
|
|
3
|
+
/** Count only evidence tied to findings persisted by the same challenge report. */
|
|
4
|
+
export declare function getResolvedChallengeEvidence(report: ChallengeReport | undefined): ChallengeResolutionEvidence[];
|
|
5
|
+
/** Build a report whose resolution state is derived exclusively from explicit evidence. */
|
|
6
|
+
export declare function buildChallengeReport(input: {
|
|
7
|
+
scenarios: FailureScenario[];
|
|
8
|
+
focusAreas: string[];
|
|
9
|
+
overallRisk: ChallengeReport['overallRisk'];
|
|
10
|
+
previousReport?: ChallengeReport;
|
|
11
|
+
runAt?: string;
|
|
12
|
+
}): ChallengeReport;
|
|
13
|
+
//# sourceMappingURL=challenge-report.d.ts.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export const MINIMUM_RESOLVED_CHALLENGES = 3;
|
|
2
|
+
function isExplicitResolutionEvidence(value) {
|
|
3
|
+
if (typeof value !== 'object' || value === null) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const candidate = value;
|
|
7
|
+
return (typeof candidate.scenario === 'string' &&
|
|
8
|
+
candidate.scenario.trim().length > 0 &&
|
|
9
|
+
(candidate.resolution === 'mitigated' || candidate.resolution === 'accepted') &&
|
|
10
|
+
typeof candidate.evidence === 'string' &&
|
|
11
|
+
candidate.evidence.trim().length > 0 &&
|
|
12
|
+
typeof candidate.resolvedAt === 'string' &&
|
|
13
|
+
candidate.resolvedAt.trim().length > 0);
|
|
14
|
+
}
|
|
15
|
+
function uniqueMatchingEvidence(report, scenarioNames) {
|
|
16
|
+
const evidence = Array.isArray(report?.resolutionEvidence) ? report.resolutionEvidence : [];
|
|
17
|
+
const matched = new Map();
|
|
18
|
+
for (const item of evidence) {
|
|
19
|
+
if (isExplicitResolutionEvidence(item) && scenarioNames.has(item.scenario)) {
|
|
20
|
+
matched.set(item.scenario, item);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return [...matched.values()];
|
|
24
|
+
}
|
|
25
|
+
/** Count only evidence tied to findings persisted by the same challenge report. */
|
|
26
|
+
export function getResolvedChallengeEvidence(report) {
|
|
27
|
+
if (!Array.isArray(report?.findings)) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
const scenarioNames = new Set(report.findings
|
|
31
|
+
.map((finding) => finding.scenario)
|
|
32
|
+
.filter((scenario) => typeof scenario === 'string' && scenario.trim().length > 0));
|
|
33
|
+
return uniqueMatchingEvidence(report, scenarioNames);
|
|
34
|
+
}
|
|
35
|
+
/** Build a report whose resolution state is derived exclusively from explicit evidence. */
|
|
36
|
+
export function buildChallengeReport(input) {
|
|
37
|
+
const scenarioNames = new Set(input.scenarios.map((scenario) => scenario.scenario));
|
|
38
|
+
const resolutionEvidence = uniqueMatchingEvidence(input.previousReport, scenarioNames);
|
|
39
|
+
const resolvedScenarios = new Set(resolutionEvidence.map((item) => item.scenario));
|
|
40
|
+
const addressedCount = resolvedScenarios.size;
|
|
41
|
+
return {
|
|
42
|
+
runAt: input.runAt ?? new Date().toISOString(),
|
|
43
|
+
totalScenarios: input.scenarios.length,
|
|
44
|
+
addressedCount,
|
|
45
|
+
focusAreas: input.focusAreas,
|
|
46
|
+
overallRisk: input.overallRisk,
|
|
47
|
+
passed: addressedCount >= MINIMUM_RESOLVED_CHALLENGES,
|
|
48
|
+
findings: input.scenarios.map((scenario) => ({
|
|
49
|
+
scenario: scenario.scenario,
|
|
50
|
+
resolved: resolvedScenarios.has(scenario.scenario),
|
|
51
|
+
})),
|
|
52
|
+
resolutionEvidence,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=challenge-report.js.map
|
|
@@ -18,6 +18,7 @@ import { calculateTokenBudget, injectBudgetIntoPrompt } from '../engine/token-bu
|
|
|
18
18
|
import { analyzeMinimalImplementation, loadMinimalImplementationPolicy, } from '../engine/minimality/index.js';
|
|
19
19
|
import { detectChallengeCapabilities, } from './challenge-spec/scenarios-utils.js';
|
|
20
20
|
import { collectCapabilityScenarios } from './challenge-spec/scenario-collector.js';
|
|
21
|
+
import { buildChallengeReport } from './challenge-spec/challenge-report.js';
|
|
21
22
|
const ALL_FOCUS_AREAS = [
|
|
22
23
|
'failures',
|
|
23
24
|
'concurrency',
|
|
@@ -193,6 +194,12 @@ export async function handleChallengeSpec(args, server) {
|
|
|
193
194
|
scalabilityAssessment,
|
|
194
195
|
overallRisk,
|
|
195
196
|
};
|
|
197
|
+
const challengeReport = buildChallengeReport({
|
|
198
|
+
scenarios: failureScenariosScored,
|
|
199
|
+
focusAreas,
|
|
200
|
+
overallRisk,
|
|
201
|
+
previousReport: spec.challengeReport,
|
|
202
|
+
});
|
|
196
203
|
const rawHumanSummary = buildChallengeSpecSummary(prioritized, overallRisk);
|
|
197
204
|
// SPEC-620: Inject token budget tag into the LLM-facing summary prompt
|
|
198
205
|
const specDevHours = spec.estimation.devHours;
|
|
@@ -232,18 +239,12 @@ export async function handleChallengeSpec(args, server) {
|
|
|
232
239
|
checkedAt: new Date().toISOString(),
|
|
233
240
|
}
|
|
234
241
|
: null,
|
|
242
|
+
challengeReport,
|
|
235
243
|
};
|
|
236
244
|
// SPEC-964: Persist challenge report so update_status can gate on it
|
|
237
245
|
try {
|
|
238
246
|
await updateSpec(projectId, specId, {
|
|
239
|
-
challengeReport
|
|
240
|
-
runAt: new Date().toISOString(),
|
|
241
|
-
totalScenarios: actionableFailureScenarios.length,
|
|
242
|
-
addressedCount: actionableFailureScenarios.length,
|
|
243
|
-
focusAreas: focusAreas,
|
|
244
|
-
overallRisk: overallRisk,
|
|
245
|
-
passed: true,
|
|
246
|
-
},
|
|
247
|
+
challengeReport,
|
|
247
248
|
});
|
|
248
249
|
}
|
|
249
250
|
catch {
|
|
@@ -277,7 +277,7 @@ export function attachOptions(question) {
|
|
|
277
277
|
* The returned string is suitable for passing directly to create_spec.
|
|
278
278
|
*/
|
|
279
279
|
export function buildSuggestedDescription(topic, answers, questions) {
|
|
280
|
-
const parts = [`
|
|
280
|
+
const parts = [`Topic: ${topic}.`];
|
|
281
281
|
for (const q of questions) {
|
|
282
282
|
const answer = answers[q.id];
|
|
283
283
|
if (answer === undefined || answer.trim() === '') {
|
|
@@ -6,13 +6,12 @@ import { buildSuggestedAnswers } from './questions.js';
|
|
|
6
6
|
*/
|
|
7
7
|
export function generateQuestions(topic, _context, knowledge, maxQuestions) {
|
|
8
8
|
const questions = [];
|
|
9
|
-
const lower = topic.toLowerCase();
|
|
10
9
|
let idx = 0;
|
|
11
10
|
const nextId = () => `q-${++idx}`;
|
|
12
11
|
// === Core question: What do you want? (always relevant) ===
|
|
13
12
|
questions.push({
|
|
14
13
|
id: nextId(),
|
|
15
|
-
question: `What
|
|
14
|
+
question: `What observable behavior or result should "${topic}" produce?`,
|
|
16
15
|
category: 'scope',
|
|
17
16
|
importance: 'critical',
|
|
18
17
|
});
|
|
@@ -26,16 +25,13 @@ export function generateQuestions(topic, _context, knowledge, maxQuestions) {
|
|
|
26
25
|
// === Acceptance: What does done look like? ===
|
|
27
26
|
questions.push({
|
|
28
27
|
id: nextId(),
|
|
29
|
-
question: `
|
|
28
|
+
question: `Which explicit observations or checks will prove that "${topic}" is done?`,
|
|
30
29
|
category: 'acceptance',
|
|
31
30
|
importance: 'critical',
|
|
32
31
|
});
|
|
33
32
|
// === Context-specific (only if topic mentions specific domains) ===
|
|
34
33
|
// Auth related — only ask which method, nothing else
|
|
35
|
-
if (
|
|
36
|
-
lower.includes('login') ||
|
|
37
|
-
lower.includes('register') ||
|
|
38
|
-
lower.includes('password')) {
|
|
34
|
+
if (hasAffirmedTerm(topic, /\b(?:auth|authentication|login|register|password)\b/i)) {
|
|
39
35
|
questions.push({
|
|
40
36
|
id: nextId(),
|
|
41
37
|
question: `What login methods? (email/password, Google, Apple, magic link, etc.)`,
|
|
@@ -44,10 +40,7 @@ export function generateQuestions(topic, _context, knowledge, maxQuestions) {
|
|
|
44
40
|
});
|
|
45
41
|
}
|
|
46
42
|
// UI related — only ask if there are mockups
|
|
47
|
-
if (
|
|
48
|
-
lower.includes('page') ||
|
|
49
|
-
lower.includes('screen') ||
|
|
50
|
-
lower.includes('dashboard')) {
|
|
43
|
+
if (hasAffirmedTerm(topic, /\b(?:ui|page|screen|dashboard)\b/i)) {
|
|
51
44
|
questions.push({
|
|
52
45
|
id: nextId(),
|
|
53
46
|
question: `Are there mockups or a reference design to follow?`,
|
|
@@ -71,4 +64,11 @@ export function generateQuestions(topic, _context, knowledge, maxQuestions) {
|
|
|
71
64
|
suggestedAnswers: buildSuggestedAnswers(q, knowledge),
|
|
72
65
|
}));
|
|
73
66
|
}
|
|
67
|
+
const NEGATION_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|remove|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope)\b/i;
|
|
68
|
+
function hasAffirmedTerm(text, pattern) {
|
|
69
|
+
return text.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
|
|
70
|
+
const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
|
|
71
|
+
return match !== null && !NEGATION_RE.test(segment.slice(0, match.index));
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
74
|
//# sourceMappingURL=questions-context.js.map
|
|
@@ -7,28 +7,22 @@ export function generateMetaQuestions() {
|
|
|
7
7
|
return [
|
|
8
8
|
{
|
|
9
9
|
id: 'meta-1',
|
|
10
|
-
question: '
|
|
10
|
+
question: 'Which change type best describes the request?',
|
|
11
11
|
category: 'scope',
|
|
12
12
|
importance: 'critical',
|
|
13
13
|
suggestedAnswers: ['New feature', 'Improvement to existing feature', 'Bug fix', 'Refactor'],
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
id: 'meta-2',
|
|
17
|
-
question: '
|
|
17
|
+
question: 'What observable outcome must this work produce?',
|
|
18
18
|
category: 'scope',
|
|
19
19
|
importance: 'critical',
|
|
20
|
-
suggestedAnswers: ['End user', 'Admin', 'Developer/internal', 'All users'],
|
|
21
20
|
},
|
|
22
21
|
{
|
|
23
22
|
id: 'meta-3',
|
|
24
|
-
question: '
|
|
23
|
+
question: 'Which boundaries are explicitly in scope and out of scope?',
|
|
25
24
|
category: 'scope',
|
|
26
25
|
importance: 'critical',
|
|
27
|
-
suggestedAnswers: [
|
|
28
|
-
'Just the core happy path',
|
|
29
|
-
'Happy path + error handling',
|
|
30
|
-
'Full feature with edge cases',
|
|
31
|
-
],
|
|
32
26
|
},
|
|
33
27
|
];
|
|
34
28
|
}
|
|
@@ -97,20 +91,6 @@ export function buildSuggestedAnswers(question, knowledge) {
|
|
|
97
91
|
];
|
|
98
92
|
}
|
|
99
93
|
}
|
|
100
|
-
// Performance questions
|
|
101
|
-
if (lower.includes('performance') ||
|
|
102
|
-
lower.includes('response time') ||
|
|
103
|
-
lower.includes('throughput')) {
|
|
104
|
-
return ['< 200ms response', '< 1s response', 'No strict requirement', 'Depends on load'];
|
|
105
|
-
}
|
|
106
|
-
// Auth questions
|
|
107
|
-
if (lower.includes('authentication') || lower.includes('auth')) {
|
|
108
|
-
return ['JWT tokens', 'Session-based', 'OAuth 2.0', 'No authentication needed', 'API key'];
|
|
109
|
-
}
|
|
110
|
-
// Timeline questions
|
|
111
|
-
if (lower.includes('deadline') || lower.includes('timeline')) {
|
|
112
|
-
return ['No strict deadline', 'Within 1 week', 'Within 1 sprint (2 weeks)', 'Specific date'];
|
|
113
|
-
}
|
|
114
94
|
return [];
|
|
115
95
|
}
|
|
116
96
|
//# sourceMappingURL=questions.js.map
|
|
@@ -6,6 +6,12 @@ import { isShortTopic, generateMetaQuestions, detectContradictions, buildSuggest
|
|
|
6
6
|
import { generateQuestions } from './clarify-requirements/questions-context.js';
|
|
7
7
|
import { detectStack, shouldSkipQuestion, attachOptions, buildSuggestedDescription, } from './clarify-requirements/multiple-choice.js';
|
|
8
8
|
import { compactJson, compactResult, compactError } from './output-formatter.js';
|
|
9
|
+
function buildConfirmedChoices(questions, answers) {
|
|
10
|
+
return questions.flatMap((question) => {
|
|
11
|
+
const value = answers[question.id]?.trim();
|
|
12
|
+
return value ? [{ questionId: question.id, value, source: 'user_confirmation' }] : [];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
9
15
|
/** Resume an existing clarification session with new answers. */
|
|
10
16
|
async function handleResumeSession(projectId, sessionId, newAnswers, knowledge) {
|
|
11
17
|
const existing = await knowledgeStore.getClarification(projectId, sessionId).catch(() => null);
|
|
@@ -50,11 +56,14 @@ async function handleResumeSession(projectId, sessionId, newAnswers, knowledge)
|
|
|
50
56
|
category: q.category,
|
|
51
57
|
importance: q.importance,
|
|
52
58
|
suggestedAnswers: q.suggestedAnswers,
|
|
59
|
+
...(q.suggestedAnswers.length > 0 ? { suggestedAnswersClassification: 'candidate' } : {}),
|
|
53
60
|
options: q.options,
|
|
61
|
+
...(q.options ? { optionsClassification: 'candidate' } : {}),
|
|
54
62
|
skipped: q.skipped,
|
|
55
63
|
answered: mergedAnswers[q.id] !== undefined,
|
|
56
64
|
})),
|
|
57
65
|
specReady,
|
|
66
|
+
confirmedChoices: buildConfirmedChoices(enriched, mergedAnswers),
|
|
58
67
|
...(suggestedDescription !== undefined ? { suggestedDescription } : {}),
|
|
59
68
|
answeredCount: Object.keys(mergedAnswers).length,
|
|
60
69
|
...(contradictions.length > 0
|
|
@@ -126,6 +135,29 @@ export async function handleClarifyRequirements(params, server) {
|
|
|
126
135
|
const skipped = shouldSkipQuestion(q, stack);
|
|
127
136
|
return skipped ? { ...withOptions, skipped: true } : withOptions;
|
|
128
137
|
});
|
|
138
|
+
const detectedEvidence = [
|
|
139
|
+
...(stack.language
|
|
140
|
+
? [{ field: 'language', value: stack.language, source: 'project_manifest' }]
|
|
141
|
+
: knowledge?.language && knowledge.language !== 'unknown'
|
|
142
|
+
? [{ field: 'language', value: knowledge.language, source: 'project_knowledge' }]
|
|
143
|
+
: []),
|
|
144
|
+
...(stack.framework
|
|
145
|
+
? [{ field: 'framework', value: stack.framework, source: 'project_manifest' }]
|
|
146
|
+
: knowledge?.framework
|
|
147
|
+
? [
|
|
148
|
+
{
|
|
149
|
+
field: 'framework',
|
|
150
|
+
value: knowledge.framework,
|
|
151
|
+
source: 'project_knowledge',
|
|
152
|
+
},
|
|
153
|
+
]
|
|
154
|
+
: []),
|
|
155
|
+
...(stack.database
|
|
156
|
+
? [{ field: 'database', value: stack.database, source: 'project_manifest' }]
|
|
157
|
+
: knowledge?.database && knowledge.database !== 'unknown'
|
|
158
|
+
? [{ field: 'database', value: knowledge.database, source: 'project_knowledge' }]
|
|
159
|
+
: []),
|
|
160
|
+
];
|
|
129
161
|
// Determine if spec is ready (all non-skipped critical questions have non-empty answers)
|
|
130
162
|
const criticalIds = questions
|
|
131
163
|
.filter((q) => q.importance === 'critical' && q.skipped !== true)
|
|
@@ -170,9 +202,15 @@ export async function handleClarifyRequirements(params, server) {
|
|
|
170
202
|
suggestedAnswer: q.suggestedAnswer,
|
|
171
203
|
/* v8 ignore next 1 */
|
|
172
204
|
suggestedAnswers: q.suggestedAnswers ?? [],
|
|
205
|
+
...(q.suggestedAnswers && q.suggestedAnswers.length > 0
|
|
206
|
+
? { suggestedAnswersClassification: 'candidate' }
|
|
207
|
+
: {}),
|
|
173
208
|
options: q.options,
|
|
209
|
+
...(q.options ? { optionsClassification: 'candidate' } : {}),
|
|
174
210
|
skipped: q.skipped,
|
|
175
211
|
})),
|
|
212
|
+
detectedEvidence,
|
|
213
|
+
confirmedChoices: buildConfirmedChoices(questions, currentAnswers),
|
|
176
214
|
specReady,
|
|
177
215
|
...(suggestedDescription !== undefined ? { suggestedDescription } : {}),
|
|
178
216
|
instructions: specReady
|
|
@@ -20,7 +20,7 @@ export declare function getEmptyAutopilotResult(): AutopilotAnalysis;
|
|
|
20
20
|
* Analyze the project to enrich a spec with real file paths and patterns.
|
|
21
21
|
* Runs automatically during create_spec — no user interaction needed.
|
|
22
22
|
*/
|
|
23
|
-
export declare function analyzeProjectForSpec(projectPath: string, description: string, title: string,
|
|
23
|
+
export declare function analyzeProjectForSpec(projectPath: string, description: string, title: string, _knowledge: ProjectKnowledge | null): Promise<AutopilotAnalysis>;
|
|
24
24
|
/**
|
|
25
25
|
* SPEC-619 AC1: Detect broad-scope descriptions that mention 2+ major subsystems.
|
|
26
26
|
* When true, create_spec should ask clarifying questions before proceeding.
|
|
@@ -19,7 +19,7 @@ export function getEmptyAutopilotResult() {
|
|
|
19
19
|
* Analyze the project to enrich a spec with real file paths and patterns.
|
|
20
20
|
* Runs automatically during create_spec — no user interaction needed.
|
|
21
21
|
*/
|
|
22
|
-
export async function analyzeProjectForSpec(projectPath, description, title,
|
|
22
|
+
export async function analyzeProjectForSpec(projectPath, description, title, _knowledge) {
|
|
23
23
|
const result = {
|
|
24
24
|
suggestedFiles: { create: [], modify: [], test: [] },
|
|
25
25
|
detectedPatterns: [],
|
|
@@ -68,10 +68,8 @@ export async function analyzeProjectForSpec(projectPath, description, title, kno
|
|
|
68
68
|
catch {
|
|
69
69
|
// Best-effort — file scanning failure doesn't block spec creation
|
|
70
70
|
}
|
|
71
|
-
// Detect
|
|
72
|
-
|
|
73
|
-
result.detectedPatterns = detectPatterns(knowledge, description);
|
|
74
|
-
}
|
|
71
|
+
// Detect patterns only from affirmed request wording.
|
|
72
|
+
result.detectedPatterns = detectPatterns(description);
|
|
75
73
|
return result;
|
|
76
74
|
}
|
|
77
75
|
/** Check if description contains technical terms (stack, framework, or code references). */
|
|
@@ -89,7 +87,23 @@ function hasTechnicalTerms(description) {
|
|
|
89
87
|
/\.\w{2,4}\b/, // file extensions like .ts, .py
|
|
90
88
|
/src\/|tests\/|lib\/|app\//, // file paths
|
|
91
89
|
];
|
|
92
|
-
return techTerms.some((
|
|
90
|
+
return techTerms.some((pattern) => hasAffirmedPattern(description, pattern));
|
|
91
|
+
}
|
|
92
|
+
const NEGATIVE_CONTEXT_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|remove|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope|unsupported|unrelated|false positive)\b/i;
|
|
93
|
+
const INCIDENTAL_CONTEXT_RE = /\b(?:example|for example|e\.g\.|quoted|regression|incorrectly inferred|must not infer)\b/i;
|
|
94
|
+
function hasAffirmedPattern(description, pattern) {
|
|
95
|
+
const withoutQuotedText = description
|
|
96
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
97
|
+
.replace(/`[^`]+`/g, ' ')
|
|
98
|
+
.replace(/"[^"]+"|'[^']+'/g, ' ');
|
|
99
|
+
return withoutQuotedText.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
|
|
100
|
+
const match = new RegExp(pattern.source, pattern.flags.replace('g', '')).exec(segment);
|
|
101
|
+
if (!match) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
const prefix = segment.slice(0, match.index);
|
|
105
|
+
return !NEGATIVE_CONTEXT_RE.test(prefix) && !INCIDENTAL_CONTEXT_RE.test(prefix);
|
|
106
|
+
});
|
|
93
107
|
}
|
|
94
108
|
/**
|
|
95
109
|
* SPEC-619 AC1: Detect broad-scope descriptions that mention 2+ major subsystems.
|
|
@@ -100,8 +114,7 @@ function hasTechnicalTerms(description) {
|
|
|
100
114
|
* one feature, not two independent systems).
|
|
101
115
|
*/
|
|
102
116
|
export function detectBroadScope(description) {
|
|
103
|
-
const
|
|
104
|
-
const wordCount = lower.trim().split(/\s+/).length;
|
|
117
|
+
const wordCount = description.trim().split(/\s+/).length;
|
|
105
118
|
const subsystems = [
|
|
106
119
|
/\b(auth|authentication|login|signup|sign.?up|register|oauth|sso)\b/,
|
|
107
120
|
/\b(payment|billing|invoice|subscription|stripe|checkout|pricing)\b/,
|
|
@@ -114,7 +127,7 @@ export function detectBroadScope(description) {
|
|
|
114
127
|
/\b(user.?management|profile|account|role|permission|rbac)\b/,
|
|
115
128
|
/\b(chat|messaging|real.?time|websocket|feed|timeline)\b/,
|
|
116
129
|
];
|
|
117
|
-
const matched = subsystems.filter((
|
|
130
|
+
const matched = subsystems.filter((pattern) => hasAffirmedPattern(description, pattern));
|
|
118
131
|
// Longer descriptions are specific enough to reference adjacent domain terms
|
|
119
132
|
// without being genuinely broad (e.g. "Stripe billing + webhook handler" = one feature).
|
|
120
133
|
const threshold = wordCount >= 20 ? 3 : 2;
|
|
@@ -135,62 +148,24 @@ function inferTestPath(srcPath) {
|
|
|
135
148
|
return `tests/${withoutExt}.test${ext}`;
|
|
136
149
|
}
|
|
137
150
|
/** Detect relevant patterns from project knowledge. */
|
|
138
|
-
function detectPatterns(
|
|
151
|
+
function detectPatterns(description) {
|
|
139
152
|
const patterns = [];
|
|
140
|
-
const
|
|
141
|
-
const hasDatabaseIntent = desc.includes('data') ||
|
|
142
|
-
desc.includes('database') ||
|
|
143
|
-
desc.includes('tabla') ||
|
|
144
|
-
desc.includes('schema') ||
|
|
145
|
-
desc.includes('rls') ||
|
|
146
|
-
desc.includes('rpc') ||
|
|
147
|
-
desc.includes('migration') ||
|
|
148
|
-
desc.includes('persistence') ||
|
|
149
|
-
desc.includes('data access');
|
|
150
|
-
const hasUiIntent = desc.includes('component') ||
|
|
151
|
-
desc.includes('ui') ||
|
|
152
|
-
desc.includes('page') ||
|
|
153
|
-
desc.includes('screen') ||
|
|
154
|
-
desc.includes('form') ||
|
|
155
|
-
desc.includes('layout') ||
|
|
156
|
-
desc.includes('style') ||
|
|
157
|
-
desc.includes('button') ||
|
|
158
|
-
desc.includes('modal') ||
|
|
159
|
-
desc.includes('dialog');
|
|
153
|
+
const hasDatabaseIntent = hasAffirmedPattern(description, /\b(?:data|database|tabla|schema|rls|rpc|migration|persistence|data access)\b/i);
|
|
160
154
|
if (hasDatabaseIntent) {
|
|
161
155
|
patterns.push('database');
|
|
162
156
|
}
|
|
163
|
-
if (
|
|
164
|
-
patterns.push('nextjs-pages');
|
|
165
|
-
}
|
|
166
|
-
if (knowledge.framework?.toLowerCase().includes('react') && hasUiIntent) {
|
|
167
|
-
patterns.push('react-components');
|
|
168
|
-
}
|
|
169
|
-
if (desc.includes('api') || desc.includes('endpoint')) {
|
|
157
|
+
if (hasAffirmedPattern(description, /\b(?:api|endpoint)\b/i)) {
|
|
170
158
|
patterns.push('api-endpoint');
|
|
171
159
|
}
|
|
172
|
-
if (
|
|
160
|
+
if (hasAffirmedPattern(description, /\b(?:auth|authentication|login)\b/i)) {
|
|
173
161
|
patterns.push('authentication');
|
|
174
162
|
}
|
|
175
|
-
if (
|
|
163
|
+
if (hasAffirmedPattern(description, /\b(?:test|testing|coverage)\b/i)) {
|
|
176
164
|
patterns.push('testing');
|
|
177
165
|
}
|
|
178
|
-
if (knowledge.stack.some((s) => s.toLowerCase().includes('supabase')) && hasDatabaseIntent) {
|
|
179
|
-
patterns.push('supabase');
|
|
180
|
-
}
|
|
181
166
|
// SPEC-535: Detect LLM/foundation model features for EU AI Act Article 53-55 compliance
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
desc.includes(' ai') ||
|
|
185
|
-
desc.includes('claude') ||
|
|
186
|
-
desc.includes('openai') ||
|
|
187
|
-
desc.includes('gpt') ||
|
|
188
|
-
desc.includes('gemini') ||
|
|
189
|
-
desc.includes('anthropic') ||
|
|
190
|
-
desc.includes('chatbot') ||
|
|
191
|
-
desc.includes('language model') ||
|
|
192
|
-
desc.includes('foundation model') ||
|
|
193
|
-
(desc.includes('chat') && desc.includes('model'))) {
|
|
167
|
+
if (hasAffirmedPattern(description, /\b(?:llm|ai|claude|openai|gpt|gemini|anthropic|chatbot|language model|foundation model)\b/i) ||
|
|
168
|
+
(hasAffirmedPattern(description, /\bchat\b/i) && hasAffirmedPattern(description, /\bmodel\b/i))) {
|
|
194
169
|
patterns.push('llm-feature');
|
|
195
170
|
}
|
|
196
171
|
return patterns;
|