@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.
Files changed (47) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/config/elicitation-questions.json +5 -17
  3. package/dist/engine/elicitation/question-generator.js +10 -3
  4. package/dist/engine/implementation-contract/renderer.js +17 -39
  5. package/dist/engine/lifecycle-hints.d.ts +1 -1
  6. package/dist/engine/lifecycle-hints.js +13 -29
  7. package/dist/engine/spec-format/lean-spec-generator.d.ts +2 -2
  8. package/dist/engine/spec-format/lean-spec-generator.js +9 -10
  9. package/dist/engine/spec-generator/fallback-generator.js +13 -20
  10. package/dist/engine/spec-grounding/contract.d.ts +5 -0
  11. package/dist/engine/spec-grounding/contract.js +10 -6
  12. package/dist/engine/spec-migrator/lean-migration.js +9 -16
  13. package/dist/engine/web-fetcher/stack-advisor.d.ts +2 -2
  14. package/dist/engine/web-fetcher/stack-advisor.js +78 -17
  15. package/dist/tools/challenge-spec/challenge-report.d.ts +13 -0
  16. package/dist/tools/challenge-spec/challenge-report.js +55 -0
  17. package/dist/tools/challenge-spec.js +9 -8
  18. package/dist/tools/clarify-requirements/multiple-choice.js +1 -1
  19. package/dist/tools/clarify-requirements/questions-context.js +11 -11
  20. package/dist/tools/clarify-requirements/questions.js +3 -23
  21. package/dist/tools/clarify-requirements.js +38 -0
  22. package/dist/tools/create-spec/autopilot-analyzer.d.ts +1 -1
  23. package/dist/tools/create-spec/autopilot-analyzer.js +29 -54
  24. package/dist/tools/create-spec/post-creation.js +23 -4
  25. package/dist/tools/create-spec.js +47 -144
  26. package/dist/tools/elicit-requirements-handler.js +20 -12
  27. package/dist/tools/register-spec-tools/core-spec-tools.js +11 -2
  28. package/dist/tools/suggest-stack.js +49 -13
  29. package/dist/tools/update-status/dod-gates.d.ts +8 -0
  30. package/dist/tools/update-status/dod-gates.js +68 -43
  31. package/dist/tools/update-status/index.js +109 -74
  32. package/dist/tools/update-status/qa-gate.js +26 -0
  33. package/dist/tools/update-status/transition-guard.d.ts +2 -2
  34. package/dist/tools/update-status/transition-guard.js +10 -7
  35. package/dist/tools/validate-lint.d.ts +1 -1
  36. package/dist/tools/validate-lint.js +39 -1
  37. package/dist/types/spec/core.d.ts +20 -0
  38. package/dist/types/spec/inputs.d.ts +1 -1
  39. package/dist/types/stack/index.d.ts +1 -1
  40. package/dist/types/stack/recommend.d.ts +10 -0
  41. package/package.json +9 -9
  42. package/planu-native.json +1 -1
  43. package/planu-plugin.json +1 -1
  44. package/dist/engine/elicitation/non-interactive-defaults.d.ts +0 -9
  45. package/dist/engine/elicitation/non-interactive-defaults.js +0 -28
  46. package/dist/tools/clarify-requirements/interview-mode.d.ts +0 -42
  47. package/dist/tools/clarify-requirements/interview-mode.js +0 -342
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [4.11.3] - 2026-07-10
2
+
3
+ ### Bug Fixes
4
+ - fix(release): classify npm 404 visibility correctly
5
+ - fix(planu): harden release and grounded spec flow
6
+
7
+
1
8
  ## [4.11.2] - 2026-07-10
2
9
 
3
10
  ### Bug Fixes
@@ -2,8 +2,7 @@
2
2
  {
3
3
  "id": "users",
4
4
  "category": "users",
5
- "questionTemplate": "Who are the primary users of \"{title}\"?",
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": "How will you know when this feature is successfully implemented?",
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": "What should happen in error or edge cases?",
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": "What data needs to be stored or retrieved for this feature?",
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 a new API endpoint or changes to existing ones?",
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((kw) => lowerDesc.includes(kw));
49
+ return (config.conditionKeywords ?? []).some((keyword) => hasAffirmedKeyword(lowerDesc, keyword));
50
50
  }
51
51
  // condition === 'api' is the only remaining case
52
52
  if (context?.isApi === true) {
53
53
  return true;
54
54
  }
55
- return (config.conditionKeywords ?? []).some((kw) => lowerDesc.includes(kw));
55
+ return (config.conditionKeywords ?? []).some((keyword) => hasAffirmedKeyword(lowerDesc, keyword));
56
+ }
57
+ const NEGATION_RE = /\b(?:no|not|never|without|exclude|excluding|forbid|forbidden|avoid|must not|should not|do not|does not|don't|doesn't|out[- ]of[- ]scope)\b/i;
58
+ function hasAffirmedKeyword(description, keyword) {
59
+ return description.split(/(?:\r?\n|[.;!?]+)/).some((segment) => {
60
+ const index = segment.indexOf(keyword.toLowerCase());
61
+ return index >= 0 && !NEGATION_RE.test(segment.slice(0, index));
62
+ });
56
63
  }
57
64
  /** Generate standard clarification questions for a feature spec */
58
65
  export function generateSpecQuestions(title, description, context) {
@@ -101,7 +108,7 @@ export function formatQuestionsAsPrompt(session) {
101
108
  lines.push('');
102
109
  });
103
110
  lines.push('---');
104
- lines.push('*Reply with your answers (e.g., "1. Admin users 2. User sees dashboard with charts...")*');
111
+ lines.push('*Reply with numbered answers that reflect only confirmed requirements.*');
105
112
  return lines.join('\n');
106
113
  }
107
114
  /** Parse answers from free-form text (numbered list format) */
@@ -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.length > 0 ? input.criteria : fallbackCriteria();
3
+ const criteria = input.criteria;
4
4
  const testFiles = input.files.test.map((file) => file.path);
5
- const verificationCommands = input.verificationCommands.length > 0
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 user-visible outcome this spec must deliver.',
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
- '- Generated specs with too little context must use `Needs decision:` placeholders.',
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.map((command) => `- \`${command}\``),
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.map((criterion, index) => `- Expected behavior AC${String(index + 1)}: ${criterion.text}`);
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
- return `- AC${String(index)}: ${criterion} -> unit test \`${test}\` plus \`${commands[0] ?? 'pnpm test'}\`.`;
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
- const base = [
103
- '- Do not create durable sidecar implementation docs outside canonical `spec.md`.',
104
- '- Do not treat headings or filler text as implementation-ready detail.',
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: 'Create your git branch',
13
- prompt: 'Run: git checkout -b feat/<spec-slug>',
14
- suggestedCommand: 'manage_git(branch)',
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: 'Start coding',
19
- prompt: 'Implement the spec following the technical plan',
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
- const hint = NEXT_ACTION_MAP[newStatus];
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 from description. Looks for checkbox patterns or generates a default.
6
- * Priority: checkboxes > GIVEN/WHEN/THEN > plain list items > fallback. */
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 from description. Looks for checkbox patterns or generates a default.
132
- * Priority: checkboxes > GIVEN/WHEN/THEN > plain list items > fallback. */
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(description);
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(description);
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, request.title);
4
- const contextLine = buildContextLine(request);
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
- '## Solution',
11
- 'Use the preserved source description above as the authoritative requirements body.',
12
- contextLine,
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; technical ownership must come from explicit source paths or local project analysis.',
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, title) {
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 `The project needs ${title} defined with enough implementation detail to proceed safely.`;
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
- if (projectEvidence.length > 0) {
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: projectEvidence.slice(0, 8),
51
- confidence: 'medium',
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
- const leanSpecLines = generateLeanSpecContent({ spec, description, estimation });
420
- // Inject real criteria (replace the default one)
421
- const leanSpecWithCriteria = replaceCriteria(leanSpecLines, criteria);
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(leanSpecWithCriteria, leanTech));
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