diff --git a/plan2code-metrics/src/collector.ts b/plan2code-metrics/src/collector.ts index 91adacb..5cf5a1b 100644 --- a/plan2code-metrics/src/collector.ts +++ b/plan2code-metrics/src/collector.ts @@ -36,6 +36,29 @@ function readFileSafe(filePath: string): string | null { } } +/** + * Extract METRICS_JSON HTML comment blocks from a file. + * Format: + * A file may contain multiple blocks (e.g., overview.md has document + finalize). + * If `stepFilter` is provided, returns only the block with matching "step" field. + * Returns parsed object or null if not found / invalid. + */ +function extractMetricsJson(content: string, stepFilter?: string): Record | null { + const re = //g; + let match: RegExpExecArray | null; + while ((match = re.exec(content)) !== null) { + try { + const parsed = JSON.parse(match[1]) as Record; + if (!stepFilter || parsed['step'] === stepFilter) { + return parsed; + } + } catch { + // try next match + } + } + return null; +} + /** Strip the "## Success Criteria" section so its checkboxes aren't counted as tasks. */ function stripSuccessCriteriaSection(overview: string): string { return overview.replace(/^## Success Criteria\s*\n[\s\S]*?(?=\n## |\n*$)/m, ''); @@ -53,8 +76,8 @@ function parseCompletionSummaryTaskCount(overview: string): { completed: number; if (!summaryMatch) return null; const summary = summaryMatch[1]; - // "Completion Rate: X% (Y/Z tasks)" - const rateMatch = summary.match(/Completion Rate[:\s*]*\d{1,3}%\s*\((\d+)\/(\d+)\s*tasks?\)/i); + // "Completion Rate: X% (Y/Z tasks)" — handles bold markdown and various separators + const rateMatch = summary.match(/\**Completion(?:\s+Rate)?\**[:\s*]+\d{1,3}%\s*\((\d+)\/(\d+)\s*tasks?\)/i); if (rateMatch) return { completed: parseInt(rateMatch[1], 10), total: parseInt(rateMatch[2], 10) }; // "Total Tasks | 28 (28/28 complete" @@ -119,15 +142,58 @@ export function collectStep1(specDir: string): Step1PlanMetrics { draftFiles.sort(); const latestDraft = readFileSafe(draftFiles[draftFiles.length - 1]) ?? ''; - // Parse "Confidence: XX%" — look for overall or total confidence - let finalConfidence: number | null = null; - const confMatch = latestDraft.match(/(?:Overall|Final|Total)?\s*[Cc]onfidence[:\s]+(\d{1,3})%/); - if (confMatch) { - finalConfidence = parseInt(confMatch[1], 10); - } else { - // Try table format: | Confidence | 92 | - const tableMatch = latestDraft.match(/[|]\s*[Cc]onfidence\s*[|]\s*(\d{1,3})/); - if (tableMatch) finalConfidence = parseInt(tableMatch[1], 10); + // ── Primary source: METRICS_JSON HTML comment ── + // Format: + const metricsJson = extractMetricsJson(latestDraft); + if (metricsJson) { + const num = (key: string): number | null => { + const v = metricsJson[key]; + return typeof v === 'number' ? v : null; + }; + const bd = metricsJson['confidence_breakdown'] as Record | undefined; + return { + present: true, + final_confidence: num('confidence'), + confidence_breakdown: bd ? { + requirements: bd.requirements ?? null, + feasibility: bd.feasibility ?? null, + integration: bd.integration ?? null, + risk: bd.risk ?? null, + } : null, + clarification_rounds: num('clarification_rounds'), + tech_stack_revision_rounds: num('tech_stack_revision_rounds'), + verification_gaps_found: num('verification_gaps_found'), + functional_requirements_count: num('functional_requirements_count'), + non_functional_requirements_count: num('non_functional_requirements_count'), + risk_count: num('risk_count'), + phase_count: num('phase_count'), + }; + } + + // ── Fallback: structured ## Planning Metrics appendix ── + // Handles both plain (confidence: 95) and bold (**confidence:** 95) field formats + // Planning Metrics is typically the last section, so we capture greedily to end, + // but stop at the next ## heading or --- if present. + const metricsSection = latestDraft.match(/^##\s+Planning\s+Metrics\s*\n([\s\S]+?)(?:\n##\s|\n---)/m) + ?? latestDraft.match(/^##\s+Planning\s+Metrics\s*\n([\s\S]+)/m); + const metricsBlock = metricsSection?.[1] ?? ''; + const parseMetricField = (field: string): number | null => { + // Match plain "field: N" or bold "**field:** N" + const m = metricsBlock.match(new RegExp(`^\\**${field}\\**[:\\s*]+?(\\d+)`, 'm')); + return m ? parseInt(m[1], 10) : null; + }; + + // ── Confidence ── + let finalConfidence = parseMetricField('confidence'); + if (finalConfidence === null) { + // Fallback: prose scraping — handle bold markdown like **Confidence:** 95% + const confMatch = latestDraft.match(/(?:Overall|Final|Total)?\s*\**[Cc]onfidence\**[:\s*]+(\d{1,3})%/); + if (confMatch) { + finalConfidence = parseInt(confMatch[1], 10); + } else { + const tableMatch = latestDraft.match(/[|]\s*[Cc]onfidence\s*[|]\s*(\d{1,3})/); + if (tableMatch) finalConfidence = parseInt(tableMatch[1], 10); + } } // Confidence breakdown (requirements, feasibility, integration, risk) @@ -145,34 +211,52 @@ export function collectStep1(specDir: string): Step1PlanMetrics { }; } - // Count ### FR- / ### NFR- headings - const frCount = (latestDraft.match(/###\s+FR-/g) ?? []).length; - const nfrCount = (latestDraft.match(/###\s+NFR-/g) ?? []).length; + // ── Clarification rounds ── + let clarificationRounds = parseMetricField('clarification_rounds'); - // Count risk table rows (lines starting with | that contain risk-level keywords) - const riskRows = latestDraft.match(/^\s*[|][^|]*(?:High|Medium|Low|Critical)[^|]*[|]/gm) ?? []; - const riskCount = riskRows.length || null; + // ── Verification gaps ── + let verificationGaps = parseMetricField('verification_gaps_found'); - // Count ## Phase headings - const phaseCount = (latestDraft.match(/^##\s+Phase\s+\d/gm) ?? []).length || null; + // ── Functional / non-functional requirements ── + let frCount = parseMetricField('functional_requirements_count'); + if (frCount === null) { + frCount = (latestDraft.match(/###\s+FR-/g) ?? []).length || null; + } + let nfrCount = parseMetricField('non_functional_requirements_count'); + if (nfrCount === null) { + nfrCount = (latestDraft.match(/###\s+NFR-/g) ?? []).length || null; + } - // Clarification rounds from conversation file - let clarificationRounds: number | null = null; + // ── Risk count ── + let riskCount = parseMetricField('risk_count'); + if (riskCount === null) { + const riskRows = latestDraft.match(/^\s*[|][^|]*(?:High|Medium|Low|Critical)[^|]*[|]/gm) ?? []; + riskCount = riskRows.length || null; + } + + // ── Phase count ── + let phaseCount = parseMetricField('phase_count'); + if (phaseCount === null) { + phaseCount = (latestDraft.match(/^##\s+Phase\s+\d/gm) ?? []).length || null; + } + + // ── Tech stack revision rounds (conversation file only) ── let techStackRevisions: number | null = null; - let verificationGaps: number | null = null; + // Fall back to conversation file scraping for fields not found in appendix if (convFiles.length > 0) { convFiles.sort(); const latestConv = readFileSafe(convFiles[convFiles.length - 1]) ?? ''; - // Count heading repetitions as clarification rounds (## Clarification or ## Round) - const clarRounds = (latestConv.match(/^##\s+(?:Clarification|Round)\s+\d/gm) ?? []).length; - clarificationRounds = clarRounds || null; - // Tech stack revision rounds + if (clarificationRounds === null) { + const clarRounds = (latestConv.match(/^##\s+(?:Clarification|Round)\s+\d/gm) ?? []).length; + clarificationRounds = clarRounds || null; + } const techRounds = (latestConv.match(/^##\s+(?:Tech\s+Stack|Technology)\s+Revision/gmi) ?? []).length; techStackRevisions = techRounds || null; - // Verification gaps found - const gapMatches = latestConv.match(/(?:verification\s+gap|gap\s+found|missing\s+requirement)/gi) ?? []; - verificationGaps = gapMatches.length || null; + if (verificationGaps === null) { + const gapMatches = latestConv.match(/(?:verification\s+gap|gap\s+found|missing\s+requirement)/gi) ?? []; + verificationGaps = gapMatches.length || null; + } } return { @@ -182,8 +266,8 @@ export function collectStep1(specDir: string): Step1PlanMetrics { clarification_rounds: clarificationRounds, tech_stack_revision_rounds: techStackRevisions, verification_gaps_found: verificationGaps, - functional_requirements_count: frCount || null, - non_functional_requirements_count: nfrCount || null, + functional_requirements_count: frCount, + non_functional_requirements_count: nfrCount, risk_count: riskCount, phase_count: phaseCount, }; @@ -201,6 +285,27 @@ export function collectStep2(specDir: string): Step2DocumentMetrics { requirement_coverage_percent: null, verification_items_added: null }; } + // ── Primary source: METRICS_JSON in overview.md ── + const step2Json = extractMetricsJson(overview, 'document'); + if (step2Json) { + const num = (key: string): number | null => { + const v = step2Json[key]; + return typeof v === 'number' ? v : null; + }; + const tpp = step2Json['tasks_per_phase']; + return { + present: true, + total_tasks: num('total_tasks'), + tasks_per_phase: Array.isArray(tpp) ? tpp : null, + phase_count: num('phase_count'), + parallel_groups_identified: num('parallel_groups_identified') ?? 0, + requirement_coverage_percent: num('requirement_coverage_percent'), + verification_items_added: num('verification_items_added'), + }; + } + + // ── Fallback: regex scraping ── + // Detect Parallel Execution Groups table const parallelGroups = (overview.match(/Parallel\s+Execution\s+Group/gi) ?? []).length; @@ -278,6 +383,28 @@ export function collectStep3(specDir: string): Step3ImplementMetrics { const overview = readFileSafe(overviewPath); const summaryCount = overview ? parseCompletionSummaryTaskCount(overview) : null; + // ── Primary source: METRICS_JSON in overview.md (from finalize step) ── + if (overview) { + const step3Json = extractMetricsJson(overview, 'finalize'); + if (step3Json) { + const num = (key: string): number | null => { + const v = step3Json[key]; + return typeof v === 'number' ? v : null; + }; + const total = num('tasks_total'); + const completed = num('tasks_completed'); + return { + present: true, + task_completion_rate: total && total > 0 && completed != null ? completed / total : null, + tasks_completed: completed, + tasks_total: total, + blocker_count: num('blocker_count') ?? 0, + }; + } + } + + // ── Fallback: regex scraping ── + // present = true if phase files exist OR overview has completion data const present = phaseFiles.length > 0 || summaryCount != null; if (!present) { @@ -359,6 +486,24 @@ export function collectStep4(specDir: string): Step4FinalizeMetrics { archival_succeeded: archivalSucceeded }; } + // ── Primary source: METRICS_JSON in overview.md ── + const step4Json = extractMetricsJson(overview, 'finalize'); + if (step4Json) { + const num = (key: string): number | null => { + const v = step4Json[key]; + return typeof v === 'number' ? v : null; + }; + return { + present: true, + completion_rate_at_audit: num('completion_rate_at_audit'), + verification_failures_found: num('verification_failures_found') ?? 0, + documentation_updates_needed: num('documentation_updates_needed'), + archival_succeeded: archivalSucceeded, + }; + } + + // ── Fallback: regex scraping ── + // Completion rate: prefer Completion Summary, fall back to stripped overview checkboxes let completionRate: number | null = null; const summaryCount = parseCompletionSummaryTaskCount(overview); diff --git a/src/plan2code-1--plan.md b/src/plan2code-1--plan.md index d6384fd..657755f 100644 --- a/src/plan2code-1--plan.md +++ b/src/plan2code-1--plan.md @@ -221,7 +221,14 @@ State assessment and ask user to confirm. 1. Save PLAN-CONVERSATION (see template below) — transcript + decision summary tables 2. Create PLAN-DRAFT (see template below) using same date 3. Verify: re-read conversation as source of truth, cross-reference against PLAN-DRAFT sections (Reqs→2.1/2.2, Tech→3, Architecture→4, Risks→6, Assumptions→9, Criteria→7). Add gaps with `` comments. Output verification summary table. -4. Append `## Planning Metrics` to PLAN-DRAFT with these exact fields: `confidence: [0-100]`, `clarification_rounds: [N]`, `functional_requirements_count: [N]`, `non_functional_requirements_count: [N]`, `risk_count: [N]`, `phase_count: [N]`, `verification_gaps_found: [N]`. Use exact field names — the metrics pipeline parses this section. +4. Append `## Planning Metrics` to PLAN-DRAFT with a `` HTML comment. The metrics pipeline parses this — use exact format: + +``` +## Planning Metrics + +``` + +Replace values with actuals. Optionally add plain `key: value` lines below for readability (no bold/markdown). ## Templates @@ -275,20 +282,11 @@ When complete (PLAN-DRAFT created), tell user: > `/plan2code-2--document` > ```" -## Abort Handling +## Abort / Recovery -If user says "abort", "cancel", "start over": -1. Confirm: "Abort planning? Progress will not be saved." -2. If confirmed, state files created that may need cleanup -3. Stop workflow - -## Recovery - -| Issue | Solution | -|-------|----------| -| Lost context | Attach PLAN-DRAFT, state phase | -| Unclear answers | One follow-up, max 3 rounds | -| Confidence stuck <90% | List blockers, ask targeted questions | +- **Abort:** Confirm with user, list files needing cleanup, stop workflow +- **Lost context:** Attach PLAN-DRAFT, state phase +- **Confidence stuck <90%:** List blockers, ask targeted questions ## Reminders @@ -296,5 +294,5 @@ If user says "abort", "cancel", "start over": - Do NOT implement - design and present plan only - Responses start with: `🤔 [PLANNING PHASE X: Name]` - **Workflow:** Plan -> Document -> Implement -> Finalize. After planning: `/plan2code-2--document` -- Save conversation log (7A) before PLAN-DRAFT (7B) +- Save conversation log (7A) before PLAN-DRAFT (7B), run verification (7C) after - Run verification (7C) after PLAN-DRAFT - conversation log is source of truth diff --git a/src/plan2code-2--document.md b/src/plan2code-2--document.md index b65010e..d6be710 100644 --- a/src/plan2code-2--document.md +++ b/src/plan2code-2--document.md @@ -54,12 +54,14 @@ If no plan exists and user wants to skip: | Criterion | Description | |-----------|-------------| -| Time-boxed | 15-60 minutes | +| Time-boxed | 15-60 min. Split if 5+ logic branches, 2+ integration points, or shared interface mutation. Combine adjacent trivial tasks that form a cohesive unit. | | Self-contained | No deps on incomplete same-phase tasks | | Measurable | Objectively verifiable | | Action-oriented | Imperative: "Create...", "Implement..." | | Specific | File paths, function names, exact requirements | +**Complexity check** — before finalizing each task, consider: logic branches, distinct behaviors, integration points, shared interface impact, and error/edge cases. Tasks that are complex on 3+ of these signals should be split. + **Examples:** | Bad | Good | @@ -83,7 +85,7 @@ If no plan exists and user wants to skip: - Success Criteria (plain bullet list, no checkboxes) (section 7) - Phase Checklist (Implementation Phases) - Quick Reference (Key Files, Environment Variables, External Dependencies) -7. **Write** each `phase-X.md` with detailed tasks +7. **Write** each `phase-X.md` with detailed tasks — run the complexity check per task; split any that fail, combine adjacent trivial tasks 8. **Analyze** parallel execution eligibility 9. **Verify** all PLAN-DRAFT requirements covered: - 9A: Re-read PLAN-DRAFT as source of truth @@ -193,6 +195,14 @@ Total tasks: Y Parallel Execution: [Groups or "None - sequential only"] ``` +Also append a machine-parseable metrics comment to the END of `overview.md` for the metrics pipeline: + +``` + +``` + +Replace values with actuals. `verification_items_added` = total Added column from the Verification Summary table. + **Tell user:** 1. What was created (spec files list) 2. Path for next session: `specs//overview.md` diff --git a/src/plan2code-4--finalize.md b/src/plan2code-4--finalize.md index c04e6fe..bc35386 100644 --- a/src/plan2code-4--finalize.md +++ b/src/plan2code-4--finalize.md @@ -268,6 +268,10 @@ specs--completed/ - **Completion Rate:** [X]% ([Y]/[Z] tasks) - **Archived To:** `specs--completed//` + + +Replace METRICS_JSON values with actuals. `completion_rate_at_audit` = Y/Z as decimal (e.g., 19/20 = 0.95). + ### Finalization Steps Completed - [x] Step 1: Task Completion Audit - [x] Step 2: Implementation Verification