@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
package/CHANGELOG.md
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
{
|
|
3
3
|
"id": "users",
|
|
4
4
|
"category": "users",
|
|
5
|
-
"questionTemplate": "
|
|
6
|
-
"hint": "e.g., \"authenticated users\", \"admin only\", \"all visitors\"",
|
|
5
|
+
"questionTemplate": "Which actors, systems, or processes are explicitly in scope for \"{title}\"?",
|
|
7
6
|
"required": true,
|
|
8
7
|
"answerType": "text",
|
|
9
8
|
"condition": "always"
|
|
@@ -11,8 +10,7 @@
|
|
|
11
10
|
{
|
|
12
11
|
"id": "success",
|
|
13
12
|
"category": "success",
|
|
14
|
-
"questionTemplate": "
|
|
15
|
-
"hint": "Describe the outcome from the user's perspective",
|
|
13
|
+
"questionTemplate": "Which explicit observations or checks will prove that \"{title}\" is complete?",
|
|
16
14
|
"required": true,
|
|
17
15
|
"answerType": "text",
|
|
18
16
|
"condition": "always"
|
|
@@ -34,8 +32,7 @@
|
|
|
34
32
|
{
|
|
35
33
|
"id": "edge_cases",
|
|
36
34
|
"category": "behavior",
|
|
37
|
-
"questionTemplate": "
|
|
38
|
-
"hint": "e.g., \"show error message\", \"redirect to login\", \"fail silently\"",
|
|
35
|
+
"questionTemplate": "Which failure conditions are explicitly in scope?",
|
|
39
36
|
"required": false,
|
|
40
37
|
"answerType": "text",
|
|
41
38
|
"condition": "always"
|
|
@@ -43,8 +40,7 @@
|
|
|
43
40
|
{
|
|
44
41
|
"id": "data",
|
|
45
42
|
"category": "data",
|
|
46
|
-
"questionTemplate": "
|
|
47
|
-
"hint": "List the key fields/entities involved",
|
|
43
|
+
"questionTemplate": "Does this request require a confirmed data change? If yes, describe only the known data boundary.",
|
|
48
44
|
"required": false,
|
|
49
45
|
"answerType": "text",
|
|
50
46
|
"condition": "data",
|
|
@@ -53,18 +49,10 @@
|
|
|
53
49
|
{
|
|
54
50
|
"id": "api_contract",
|
|
55
51
|
"category": "constraints",
|
|
56
|
-
"questionTemplate": "Does this require
|
|
52
|
+
"questionTemplate": "Does this request explicitly require an API contract change?",
|
|
57
53
|
"required": false,
|
|
58
54
|
"answerType": "yes-no",
|
|
59
55
|
"condition": "api",
|
|
60
56
|
"conditionKeywords": ["api", "endpoint"]
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
"id": "performance",
|
|
64
|
-
"category": "constraints",
|
|
65
|
-
"questionTemplate": "Are there performance requirements (response time, load, etc.)?",
|
|
66
|
-
"required": false,
|
|
67
|
-
"answerType": "text",
|
|
68
|
-
"condition": "always"
|
|
69
57
|
}
|
|
70
58
|
]
|
|
@@ -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) */
|
|
@@ -1,43 +1,33 @@
|
|
|
1
1
|
import { hasImplementationContract, IMPLEMENTATION_CONTRACT_SECTION } from './common.js';
|
|
2
2
|
export function buildImplementationContractSection(input) {
|
|
3
|
-
const criteria = input.criteria
|
|
3
|
+
const criteria = input.criteria;
|
|
4
4
|
const testFiles = input.files.test.map((file) => file.path);
|
|
5
|
-
const verificationCommands = input.verificationCommands
|
|
6
|
-
? input.verificationCommands
|
|
7
|
-
: ['pnpm typecheck', 'pnpm lint', 'pnpm test'];
|
|
5
|
+
const verificationCommands = input.verificationCommands;
|
|
8
6
|
const lines = [
|
|
9
7
|
`## ${IMPLEMENTATION_CONTRACT_SECTION}`,
|
|
10
8
|
'### User Outcome',
|
|
11
9
|
firstConcreteSentence(input.description) ??
|
|
12
|
-
'Needs decision: define the exact
|
|
10
|
+
'Needs decision: define the exact observable outcome this spec must deliver.',
|
|
13
11
|
'',
|
|
14
12
|
'### Behavior Contract',
|
|
15
13
|
...renderBehaviorContract(criteria),
|
|
16
|
-
'- Inputs: use the user request and existing project conventions as the source of truth.',
|
|
17
|
-
'- Outputs: update canonical `spec.md` behavior without creating sidecar implementation docs.',
|
|
18
|
-
'- State changes: persist only the files and metadata required by the spec workflow.',
|
|
19
|
-
'- Errors: missing implementation detail must surface as `Needs decision:` instead of generic filler.',
|
|
20
14
|
'',
|
|
21
15
|
'### File-Level Work Plan',
|
|
22
16
|
...renderFilePlan(input.files),
|
|
23
17
|
'',
|
|
24
|
-
'### Minimal Change Guidance',
|
|
25
|
-
...renderMinimalChangeGuidance(),
|
|
26
|
-
'',
|
|
27
18
|
'### Acceptance-To-Verification Map',
|
|
28
19
|
...criteria.map((criterion, index) => renderVerificationRow(criterion.text, index + 1, testFiles, verificationCommands)),
|
|
29
20
|
'',
|
|
30
21
|
'### Edge Cases And Failure Modes',
|
|
31
|
-
'-
|
|
32
|
-
'- Existing specs without this section must remain readable and must not be rewritten by readiness checks.',
|
|
33
|
-
'- A heading with filler text must not count as a usable implementation contract.',
|
|
34
|
-
'- Manual verification must include exact observable steps when no automated proof is practical.',
|
|
22
|
+
'- Needs decision: identify criterion-specific failure modes from user input or project evidence.',
|
|
35
23
|
'',
|
|
36
24
|
'### Non-Goals And Forbidden Approaches',
|
|
37
25
|
...renderNonGoals(input.outOfScope),
|
|
38
26
|
'',
|
|
39
27
|
'### Verification Commands',
|
|
40
|
-
...verificationCommands.
|
|
28
|
+
...(verificationCommands.length > 0
|
|
29
|
+
? verificationCommands.map((command) => `- \`${command}\``)
|
|
30
|
+
: ['- Needs decision: provide a repository-supported verification command.']),
|
|
41
31
|
'',
|
|
42
32
|
];
|
|
43
33
|
return lines.join('\n');
|
|
@@ -48,21 +38,6 @@ export function appendImplementationContractIfMissing(specBody, input) {
|
|
|
48
38
|
}
|
|
49
39
|
return `${specBody.trimEnd()}\n\n${buildImplementationContractSection(input)}`;
|
|
50
40
|
}
|
|
51
|
-
function renderMinimalChangeGuidance() {
|
|
52
|
-
return [
|
|
53
|
-
'- Before implementing, check whether the work is unnecessary, already exists in this codebase, is covered by stdlib, is covered by the native platform, or is covered by an already-installed dependency.',
|
|
54
|
-
'- Add new code only after those checks, and keep the diff to the minimum that satisfies the accepted criteria.',
|
|
55
|
-
'- Do not use minimality to remove security, trust-boundary validation, data-loss handling, accessibility, explicit requirements, or required validation.',
|
|
56
|
-
];
|
|
57
|
-
}
|
|
58
|
-
function fallbackCriteria() {
|
|
59
|
-
return [
|
|
60
|
-
{
|
|
61
|
-
text: 'Needs decision: define at least one acceptance criterion with executable verification.',
|
|
62
|
-
done: false,
|
|
63
|
-
},
|
|
64
|
-
];
|
|
65
|
-
}
|
|
66
41
|
function firstConcreteSentence(description) {
|
|
67
42
|
const body = description
|
|
68
43
|
.replace(/^---[\s\S]*?---/, '')
|
|
@@ -89,20 +64,23 @@ function renderFilePlan(files) {
|
|
|
89
64
|
return lines.length > 0 ? lines : ['- Needs decision: identify expected source and test files.'];
|
|
90
65
|
}
|
|
91
66
|
function renderBehaviorContract(criteria) {
|
|
92
|
-
return criteria.
|
|
67
|
+
return criteria.length > 0
|
|
68
|
+
? criteria.map((criterion, index) => `- Expected behavior AC${String(index + 1)}: ${criterion.text}`)
|
|
69
|
+
: ['- Needs decision: define at least one evidence-grounded acceptance criterion.'];
|
|
93
70
|
}
|
|
94
71
|
function renderVerificationRow(criterion, index, testFiles, commands) {
|
|
95
72
|
const test = testFiles[0];
|
|
96
73
|
if (test) {
|
|
97
|
-
|
|
74
|
+
const command = commands[0];
|
|
75
|
+
return command
|
|
76
|
+
? `- AC${String(index)}: ${criterion} -> unit test \`${test}\` plus \`${command}\`.`
|
|
77
|
+
: `- AC${String(index)}: ${criterion} -> unit test \`${test}\`.`;
|
|
98
78
|
}
|
|
99
79
|
return `- AC${String(index)}: ${criterion} -> Needs decision: choose unit test, integration test, CLI/tool result, snapshot/golden fixture, or exact manual verification.`;
|
|
100
80
|
}
|
|
101
81
|
function renderNonGoals(outOfScope) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'-
|
|
105
|
-
];
|
|
106
|
-
return outOfScope.length > 0 ? [...base, ...outOfScope.map((item) => `- ${item}`)] : base;
|
|
82
|
+
return outOfScope.length > 0
|
|
83
|
+
? outOfScope.map((item) => `- ${item}`)
|
|
84
|
+
: ['- Needs decision: provide explicit non-goals or confirm that none were supplied.'];
|
|
107
85
|
}
|
|
108
86
|
//# sourceMappingURL=renderer.js.map
|
|
@@ -3,7 +3,7 @@ import type { SpecStatus, NextAction, LifecycleMap } from '../types/index.js';
|
|
|
3
3
|
* Returns the guided next action for a given status transition.
|
|
4
4
|
* The hint targets the *newStatus* reached, guiding what to do from there.
|
|
5
5
|
*/
|
|
6
|
-
export declare function buildNextAction(newStatus: SpecStatus): NextAction;
|
|
6
|
+
export declare function buildNextAction(newStatus: SpecStatus): NextAction | null;
|
|
7
7
|
/**
|
|
8
8
|
* Returns the lifecycle map for the given current status.
|
|
9
9
|
* Shows where the spec is, what's next, and what remains.
|
|
@@ -2,53 +2,37 @@
|
|
|
2
2
|
const LIFECYCLE_STAGES = ['draft', 'review', 'approved', 'implementing', 'done'];
|
|
3
3
|
/** Maps each forward transition to a guided next action */
|
|
4
4
|
const NEXT_ACTION_MAP = {
|
|
5
|
+
draft: {
|
|
6
|
+
step: 'Challenge the spec',
|
|
7
|
+
prompt: 'Run challenge_spec and resolve the discovered findings before review',
|
|
8
|
+
suggestedCommand: 'challenge_spec(specId)',
|
|
9
|
+
whenToDoIt: 'Before requesting review',
|
|
10
|
+
},
|
|
5
11
|
review: {
|
|
6
12
|
step: 'Get approval',
|
|
7
13
|
prompt: "Share this plan with your team or just tell me 'approved' to move forward",
|
|
8
|
-
suggestedCommand: 'update_status(approved)',
|
|
14
|
+
suggestedCommand: 'update_status(status="approved")',
|
|
9
15
|
whenToDoIt: "When you're happy with the plan",
|
|
10
16
|
},
|
|
11
17
|
approved: {
|
|
12
|
-
step: '
|
|
13
|
-
prompt: '
|
|
14
|
-
suggestedCommand: '
|
|
18
|
+
step: 'Start implementation',
|
|
19
|
+
prompt: 'Move the approved spec to implementing before writing production code',
|
|
20
|
+
suggestedCommand: 'update_status(status="implementing")',
|
|
15
21
|
whenToDoIt: 'Before writing any code',
|
|
16
22
|
},
|
|
17
23
|
implementing: {
|
|
18
|
-
step: '
|
|
19
|
-
prompt: '
|
|
20
|
-
suggestedCommand: 'validate(specId)',
|
|
21
|
-
whenToDoIt: 'While developing',
|
|
22
|
-
},
|
|
23
|
-
done: {
|
|
24
|
-
step: 'Validate your implementation',
|
|
25
|
-
prompt: 'Run validate to check that your code matches the spec',
|
|
24
|
+
step: 'Validate the implementation',
|
|
25
|
+
prompt: 'Check the implementation against every acceptance criterion',
|
|
26
26
|
suggestedCommand: 'validate(specId)',
|
|
27
27
|
whenToDoIt: 'After all tests pass',
|
|
28
28
|
},
|
|
29
|
-
discarded: {
|
|
30
|
-
step: 'Archive or delete spec',
|
|
31
|
-
prompt: 'This spec has been discarded. You can delete it or keep it for reference.',
|
|
32
|
-
suggestedCommand: 'delete_spec(specId)',
|
|
33
|
-
whenToDoIt: 'When cleanup is needed',
|
|
34
|
-
},
|
|
35
29
|
};
|
|
36
30
|
/**
|
|
37
31
|
* Returns the guided next action for a given status transition.
|
|
38
32
|
* The hint targets the *newStatus* reached, guiding what to do from there.
|
|
39
33
|
*/
|
|
40
34
|
export function buildNextAction(newStatus) {
|
|
41
|
-
|
|
42
|
-
if (hint !== undefined) {
|
|
43
|
-
return hint;
|
|
44
|
-
}
|
|
45
|
-
// Fallback for any unrecognised status
|
|
46
|
-
return {
|
|
47
|
-
step: 'Continue',
|
|
48
|
-
prompt: `Spec is now ${newStatus}. Check Planu for next steps.`,
|
|
49
|
-
suggestedCommand: 'list_specs()',
|
|
50
|
-
whenToDoIt: 'When ready',
|
|
51
|
-
};
|
|
35
|
+
return NEXT_ACTION_MAP[newStatus] ?? null;
|
|
52
36
|
}
|
|
53
37
|
/**
|
|
54
38
|
* Returns the lifecycle map for the given current status.
|
|
@@ -2,7 +2,7 @@ import type { LeanCriterion, LeanSpecInput } from '../../types/index.js';
|
|
|
2
2
|
export type { LeanCriterion, LeanHistoryEntry, LeanSpecInput } from '../../types/index.js';
|
|
3
3
|
/** Generate lean spec.md content: YAML frontmatter + user description only. */
|
|
4
4
|
export declare function generateLeanSpecContent(input: LeanSpecInput): string;
|
|
5
|
-
/** Extract acceptance criteria
|
|
6
|
-
* Priority: checkboxes > GIVEN/WHEN/THEN > plain list items
|
|
5
|
+
/** Extract acceptance criteria explicitly present in the description.
|
|
6
|
+
* Priority: checkboxes > GIVEN/WHEN/THEN > plain list items. */
|
|
7
7
|
export declare function extractCriteria(description: string): LeanCriterion[];
|
|
8
8
|
//# sourceMappingURL=lean-spec-generator.d.ts.map
|
|
@@ -128,8 +128,8 @@ function buildBddLines(description, extraCriteria, criteriaOverride, scenarioTes
|
|
|
128
128
|
}
|
|
129
129
|
return renderBddScenariosYaml(scenarios);
|
|
130
130
|
}
|
|
131
|
-
/** Extract acceptance criteria
|
|
132
|
-
* Priority: checkboxes > GIVEN/WHEN/THEN > plain list items
|
|
131
|
+
/** Extract acceptance criteria explicitly present in the description.
|
|
132
|
+
* Priority: checkboxes > GIVEN/WHEN/THEN > plain list items. */
|
|
133
133
|
export function extractCriteria(description) {
|
|
134
134
|
const checkboxCriteria = extractCheckboxCriteria(description);
|
|
135
135
|
if (checkboxCriteria.length > 0) {
|
|
@@ -143,12 +143,7 @@ export function extractCriteria(description) {
|
|
|
143
143
|
if (plainListCriteria.length > 0) {
|
|
144
144
|
return plainListCriteria;
|
|
145
145
|
}
|
|
146
|
-
return [
|
|
147
|
-
{
|
|
148
|
-
text: 'Define acceptance criteria — autopilot could not infer testable behavior from the description',
|
|
149
|
-
done: false,
|
|
150
|
-
},
|
|
151
|
-
];
|
|
146
|
+
return [];
|
|
152
147
|
}
|
|
153
148
|
/** Extract checkbox-style criteria: `- [ ] text` or `- [x] text`. */
|
|
154
149
|
function extractCheckboxCriteria(description) {
|
|
@@ -198,16 +193,20 @@ function extractGivenWhenThenCriteria(description) {
|
|
|
198
193
|
/** Extract plain list items: `- text` or `* text` (no checkboxes).
|
|
199
194
|
* Only used when no checkboxes or GIVEN/WHEN/THEN are found. */
|
|
200
195
|
function extractPlainListCriteria(description) {
|
|
196
|
+
const acceptanceSection = /(?:^|\n)#{1,6}[ \t]+Acceptance Criteria[ \t]*\r?\n([\s\S]*?)(?=\r?\n#{1,6}[ \t]+|$)/i.exec(description)?.[1] ?? '';
|
|
197
|
+
if (acceptanceSection.length === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
201
200
|
const criteria = [];
|
|
202
201
|
// Plain list: `- text` or `* text`, not followed by `[`
|
|
203
202
|
const plainListRegex = /^[-*]\s+(?!\[)(.+)$/gm;
|
|
204
|
-
let match = plainListRegex.exec(
|
|
203
|
+
let match = plainListRegex.exec(acceptanceSection);
|
|
205
204
|
while (match) {
|
|
206
205
|
const text = match[1]?.trim() ?? '';
|
|
207
206
|
if (text) {
|
|
208
207
|
criteria.push({ text, done: false });
|
|
209
208
|
}
|
|
210
|
-
match = plainListRegex.exec(
|
|
209
|
+
match = plainListRegex.exec(acceptanceSection);
|
|
211
210
|
}
|
|
212
211
|
return criteria;
|
|
213
212
|
}
|
|
@@ -1,15 +1,19 @@
|
|
|
1
|
+
import { extractCriteria } from '../spec-format/lean-spec-generator.js';
|
|
1
2
|
export class FallbackGenerator {
|
|
2
3
|
generate(request) {
|
|
3
|
-
const sourceDescription = normalizeSourceDescription(request.description
|
|
4
|
-
const
|
|
4
|
+
const sourceDescription = normalizeSourceDescription(request.description);
|
|
5
|
+
const hasCriteria = extractCriteria(sourceDescription).length > 0;
|
|
5
6
|
const technicalSection = '';
|
|
6
7
|
const specBody = [
|
|
7
8
|
'## Problem',
|
|
8
9
|
sourceDescription,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
...(hasCriteria
|
|
11
|
+
? []
|
|
12
|
+
: [
|
|
13
|
+
'',
|
|
14
|
+
'## Missing Decisions',
|
|
15
|
+
'Needs decision: provide at least one explicit, verifiable acceptance criterion.',
|
|
16
|
+
]),
|
|
13
17
|
].join('\n');
|
|
14
18
|
return Promise.resolve({
|
|
15
19
|
specBody,
|
|
@@ -19,28 +23,17 @@ export class FallbackGenerator {
|
|
|
19
23
|
generatedAt: new Date().toISOString(),
|
|
20
24
|
},
|
|
21
25
|
qualityWarnings: [
|
|
22
|
-
'Fallback generator used. Source description was preserved;
|
|
26
|
+
'Fallback generator used. Source description was preserved; missing decisions require explicit user input.',
|
|
23
27
|
],
|
|
24
28
|
fallbackReason: 'No external model generator is configured for create_spec.',
|
|
25
29
|
});
|
|
26
30
|
}
|
|
27
31
|
}
|
|
28
|
-
function normalizeSourceDescription(description
|
|
32
|
+
function normalizeSourceDescription(description) {
|
|
29
33
|
const compact = description.replace(/\r\n/g, '\n').trim();
|
|
30
34
|
if (compact.length > 0) {
|
|
31
35
|
return compact;
|
|
32
36
|
}
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
function buildContextLine(request) {
|
|
36
|
-
const parts = [
|
|
37
|
-
request.projectContext?.language,
|
|
38
|
-
request.projectContext?.framework,
|
|
39
|
-
request.projectContext?.architecture,
|
|
40
|
-
].filter((part) => part !== undefined && part.trim().length > 0);
|
|
41
|
-
if (parts.length === 0) {
|
|
42
|
-
return 'Use the existing project architecture and conventions.';
|
|
43
|
-
}
|
|
44
|
-
return `Use the existing ${parts.join(' / ')} project conventions.`;
|
|
37
|
+
return 'Needs decision: provide the source requirement description.';
|
|
45
38
|
}
|
|
46
39
|
//# sourceMappingURL=fallback-generator.js.map
|
|
@@ -6,6 +6,11 @@ export declare function filterGroundedCriteria(criteria: string[]): string[];
|
|
|
6
6
|
export declare function buildCriterionGroundingRecords(args: {
|
|
7
7
|
criteria: string[];
|
|
8
8
|
userInput: string;
|
|
9
|
+
criterionEvidence?: {
|
|
10
|
+
criterion: string;
|
|
11
|
+
evidence: string[];
|
|
12
|
+
}[];
|
|
13
|
+
/** @deprecated Global evidence cannot ground a criterion. Use criterionEvidence. */
|
|
9
14
|
projectEvidence?: string[];
|
|
10
15
|
generatedEvidence?: string[];
|
|
11
16
|
}): CriterionGroundingRecord[];
|
|
@@ -23,8 +23,11 @@ export function filterGroundedCriteria(criteria) {
|
|
|
23
23
|
}
|
|
24
24
|
export function buildCriterionGroundingRecords(args) {
|
|
25
25
|
const userInput = normalizeCriterionText(args.userInput);
|
|
26
|
-
const projectEvidence = args.projectEvidence ?? [];
|
|
27
26
|
const generatedEvidence = args.generatedEvidence ?? [];
|
|
27
|
+
const criterionEvidence = new Map((args.criterionEvidence ?? []).map((record) => [
|
|
28
|
+
normalizeCriterionText(record.criterion),
|
|
29
|
+
record.evidence.filter((item) => item.trim().length > 0),
|
|
30
|
+
]));
|
|
28
31
|
return args.criteria.map((criterion) => {
|
|
29
32
|
const normalized = normalizeCriterionText(criterion);
|
|
30
33
|
if (isGenericCriterion(criterion)) {
|
|
@@ -43,12 +46,13 @@ export function buildCriterionGroundingRecords(args) {
|
|
|
43
46
|
confidence: 'high',
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
|
-
|
|
49
|
+
const directProjectEvidence = criterionEvidence.get(normalized) ?? [];
|
|
50
|
+
if (directProjectEvidence.length > 0) {
|
|
47
51
|
return {
|
|
48
52
|
text: criterion,
|
|
49
53
|
source: 'project_evidence',
|
|
50
|
-
evidence:
|
|
51
|
-
confidence: '
|
|
54
|
+
evidence: directProjectEvidence.slice(0, 8),
|
|
55
|
+
confidence: 'high',
|
|
52
56
|
};
|
|
53
57
|
}
|
|
54
58
|
return {
|
|
@@ -108,7 +112,7 @@ export function checkGroundedSpecContract(spec, content) {
|
|
|
108
112
|
issues.push(`Spec ${spec.id} requires grounding metadata but has no grounded criteria records.`);
|
|
109
113
|
}
|
|
110
114
|
for (const record of records) {
|
|
111
|
-
if (record.source === 'ungrounded_advisory') {
|
|
115
|
+
if (record.source === 'ungrounded_advisory' || record.source === 'documented_assumption') {
|
|
112
116
|
issues.push(`Criterion is advisory-only and cannot be approved as contract: ${record.text}`);
|
|
113
117
|
}
|
|
114
118
|
if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
|
|
@@ -117,7 +121,7 @@ export function checkGroundedSpecContract(spec, content) {
|
|
|
117
121
|
}
|
|
118
122
|
const technicalRecords = parseTechnicalReferenceGroundingRecords(frontmatter);
|
|
119
123
|
for (const record of technicalRecords) {
|
|
120
|
-
if (record.source === 'ungrounded_advisory') {
|
|
124
|
+
if (record.source === 'ungrounded_advisory' || record.source === 'documented_assumption') {
|
|
121
125
|
issues.push(`Technical reference is advisory-only and cannot be contract: ${record.path}`);
|
|
122
126
|
}
|
|
123
127
|
if (record.evidence.length === 0 && record.source !== 'ungrounded_advisory') {
|
|
@@ -113,9 +113,6 @@ function extractOldCriteria(content) {
|
|
|
113
113
|
}
|
|
114
114
|
match = re.exec(content);
|
|
115
115
|
}
|
|
116
|
-
if (criteria.length === 0) {
|
|
117
|
-
criteria.push({ text: 'Implementation complete and tested', done: false });
|
|
118
|
-
}
|
|
119
116
|
return criteria;
|
|
120
117
|
}
|
|
121
118
|
/** Check if a criterion is generic boilerplate (not specific to the feature). */
|
|
@@ -415,10 +412,14 @@ export async function migrateSpecToLean(specDir, projectPath) {
|
|
|
415
412
|
const criteria = extractOldCriteria(specContent);
|
|
416
413
|
const estimation = buildEstimationFromOld(metadata, oldTechContent);
|
|
417
414
|
const spec = buildSpecFromMetadata(metadata, id, title, status, estimation, specPath, techPath);
|
|
418
|
-
// Generate lean spec.md with extracted criteria
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
415
|
+
// Generate lean spec.md with extracted legacy criteria. Do not synthesize defaults:
|
|
416
|
+
// migrations preserve source-backed acceptance criteria only.
|
|
417
|
+
const leanSpecLines = generateLeanSpecContent({
|
|
418
|
+
spec,
|
|
419
|
+
description,
|
|
420
|
+
estimation,
|
|
421
|
+
criteriaOverride: criteria,
|
|
422
|
+
});
|
|
422
423
|
// Generate the legacy technical body, then fold it into unified spec.md.
|
|
423
424
|
const leanTech = generateLeanTechnicalContent({
|
|
424
425
|
specId: id,
|
|
@@ -434,7 +435,7 @@ export async function migrateSpecToLean(specDir, projectPath) {
|
|
|
434
435
|
return `backup failed: ${backupErr instanceof Error ? backupErr.message : String(backupErr)}`;
|
|
435
436
|
}
|
|
436
437
|
// Write one unified spec.md.
|
|
437
|
-
await atomicWriteFile(specPath, buildUnifiedSpecContent(
|
|
438
|
+
await atomicWriteFile(specPath, buildUnifiedSpecContent(leanSpecLines, leanTech));
|
|
438
439
|
// Delete obsolete files
|
|
439
440
|
for (const file of OBSOLETE_FILES) {
|
|
440
441
|
const filePath = join(specDir, file);
|
|
@@ -457,14 +458,6 @@ export async function migrateSpecToLean(specDir, projectPath) {
|
|
|
457
458
|
return err instanceof Error ? err.message : String(err);
|
|
458
459
|
}
|
|
459
460
|
}
|
|
460
|
-
/** Replace criteria in lean spec content with extracted old criteria. */
|
|
461
|
-
function replaceCriteria(content, criteria) {
|
|
462
|
-
// Find the criteria section in YAML and replace it
|
|
463
|
-
const criteriaLines = criteria.map((c) => ` - text: "${c.text.replace(/"/g, '\\"')}"\n done: ${String(c.done)}`);
|
|
464
|
-
const newCriteria = `criteria:\n${criteriaLines.join('\n')}`;
|
|
465
|
-
// Replace the existing criteria block
|
|
466
|
-
return content.replace(/criteria:\n(?: {2}- text:.*\n {4}done:.*\n?)+/, `${newCriteria}\n`);
|
|
467
|
-
}
|
|
468
461
|
/** Migrate all specs in a project to lean format. */
|
|
469
462
|
export async function migrateAllSpecsToLean(projectPath) {
|
|
470
463
|
const result = { migratedCount: 0, skippedCount: 0, errors: [] };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StackRecommendation, SuggestStackResult } from '../../types/index.js';
|
|
1
|
+
import type { StackAdviceGrounding, StackRecommendation, SuggestStackResult } from '../../types/index.js';
|
|
2
2
|
export declare function verifyFrameworkVersion(framework: string): Promise<string | null>;
|
|
3
3
|
export declare function recommendStack(projectType: string, requirements: string[], constraints: string[]): StackRecommendation;
|
|
4
4
|
export declare function recommendAlternativeStack(projectType: string, _requirements: string[]): StackRecommendation;
|
|
@@ -11,5 +11,5 @@ export declare function buildMcpCompatibility(stack: StackRecommendation): {
|
|
|
11
11
|
compatible: boolean;
|
|
12
12
|
notes: string;
|
|
13
13
|
}[];
|
|
14
|
-
export declare function buildSuggestStackResult(projectType: string, requirements: string[], constraints: string[]): Promise<SuggestStackResult>;
|
|
14
|
+
export declare function buildSuggestStackResult(projectType: string, requirements: string[], constraints: string[]): Promise<SuggestStackResult & StackAdviceGrounding>;
|
|
15
15
|
//# sourceMappingURL=stack-advisor.d.ts.map
|