fix(state): deduplicate extractField into shared helper with regex escaping

Two defensive hardening changes:

1. state.cjs: Replace two identical inline extractField closures
   (cmdStateSnapshot and buildStateFrontmatter) with a shared
   stateExtractField(content, fieldName) helper at module scope.
   The helper uses escapeRegex on fieldName before interpolation
   into RegExp constructors, preventing breakage if a field name
   ever contains regex metacharacters. Net removal of duplicated
   logic.

2. gsd-plan-checker.md: Bound the "relevant" definition in the
   exhaustive cross-check instruction. A requirement is "relevant"
   only if ROADMAP.md explicitly maps it to this phase or the phase
   goal directly implies it — prevents false blocker flags for
   requirements belonging to other phases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Tibsfox
2026-03-03 01:56:47 -08:00
committed by Lex Christopherson
parent 8b8d1074b8
commit 641cdbdae7
2 changed files with 34 additions and 45 deletions

View File

@@ -447,7 +447,7 @@ Session persists | 01 | 3 | COVERED
For each requirement: find covering task(s), verify action is specific, flag gaps.
**Exhaustive cross-check:** Also read PROJECT.md requirements (not just phase goal). Verify no PROJECT.md requirement relevant to this phase is silently dropped. Any unmapped requirement is an automatic blocker — list it explicitly in issues.
**Exhaustive cross-check:** Also read PROJECT.md requirements (not just phase goal). Verify no PROJECT.md requirement relevant to this phase is silently dropped. A requirement is "relevant" if the ROADMAP.md explicitly maps it to this phase or if the phase goal directly implies it — do NOT flag requirements that belong to other phases or future work. Any unmapped relevant requirement is an automatic blocker — list it explicitly in issues.
## Step 5: Validate Task Structure

View File

@@ -4,9 +4,21 @@
const fs = require('fs');
const path = require('path');
const { loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
const { escapeRegex, loadConfig, getMilestoneInfo, getMilestonePhaseFilter, output, error } = require('./core.cjs');
const { extractFrontmatter, reconstructFrontmatter } = require('./frontmatter.cjs');
// Shared helper: extract a field value from STATE.md content.
// Supports both **Field:** bold and plain Field: format.
function stateExtractField(content, fieldName) {
const escaped = escapeRegex(fieldName);
const boldPattern = new RegExp(`\\*\\*${escaped}:\\*\\*\\s*(.+)`, 'i');
const boldMatch = content.match(boldPattern);
if (boldMatch) return boldMatch[1].trim();
const plainPattern = new RegExp(`^${escaped}:\\s*(.+)`, 'im');
const plainMatch = content.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
}
function cmdStateLoad(cwd, raw) {
const config = loadConfig(cwd);
const planningDir = path.join(cwd, '.planning');
@@ -449,30 +461,17 @@ function cmdStateSnapshot(cwd, raw) {
const content = fs.readFileSync(statePath, 'utf-8');
// Helper to extract field values — supports both **Field:** bold format
// and plain Field: format (STATE.md may use either depending on version)
const extractField = (fieldName) => {
// Try **Field:** format first (bold markdown)
const boldPattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
const boldMatch = content.match(boldPattern);
if (boldMatch) return boldMatch[1].trim();
// Fall back to plain Field: format
const plainPattern = new RegExp(`^${fieldName}:\\s*(.+)`, 'im');
const plainMatch = content.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
};
// Extract basic fields
const currentPhase = extractField('Current Phase');
const currentPhaseName = extractField('Current Phase Name');
const totalPhasesRaw = extractField('Total Phases');
const currentPlan = extractField('Current Plan');
const totalPlansRaw = extractField('Total Plans in Phase');
const status = extractField('Status');
const progressRaw = extractField('Progress');
const lastActivity = extractField('Last Activity');
const lastActivityDesc = extractField('Last Activity Description');
const pausedAt = extractField('Paused At');
const currentPhase = stateExtractField(content, 'Current Phase');
const currentPhaseName = stateExtractField(content, 'Current Phase Name');
const totalPhasesRaw = stateExtractField(content, 'Total Phases');
const currentPlan = stateExtractField(content, 'Current Plan');
const totalPlansRaw = stateExtractField(content, 'Total Plans in Phase');
const status = stateExtractField(content, 'Status');
const progressRaw = stateExtractField(content, 'Progress');
const lastActivity = stateExtractField(content, 'Last Activity');
const lastActivityDesc = stateExtractField(content, 'Last Activity Description');
const pausedAt = stateExtractField(content, 'Paused At');
// Parse numeric fields
const totalPhases = totalPhasesRaw ? parseInt(totalPhasesRaw, 10) : null;
@@ -557,26 +556,16 @@ function cmdStateSnapshot(cwd, raw) {
* reliably via `state json` instead of fragile regex parsing.
*/
function buildStateFrontmatter(bodyContent, cwd) {
// Supports both **Field:** bold and plain Field: format (see state-snapshot)
const extractField = (fieldName) => {
const boldPattern = new RegExp(`\\*\\*${fieldName}:\\*\\*\\s*(.+)`, 'i');
const boldMatch = bodyContent.match(boldPattern);
if (boldMatch) return boldMatch[1].trim();
const plainPattern = new RegExp(`^${fieldName}:\\s*(.+)`, 'im');
const plainMatch = bodyContent.match(plainPattern);
return plainMatch ? plainMatch[1].trim() : null;
};
const currentPhase = extractField('Current Phase');
const currentPhaseName = extractField('Current Phase Name');
const currentPlan = extractField('Current Plan');
const totalPhasesRaw = extractField('Total Phases');
const totalPlansRaw = extractField('Total Plans in Phase');
const status = extractField('Status');
const progressRaw = extractField('Progress');
const lastActivity = extractField('Last Activity');
const stoppedAt = extractField('Stopped At') || extractField('Stopped at');
const pausedAt = extractField('Paused At');
const currentPhase = stateExtractField(bodyContent, 'Current Phase');
const currentPhaseName = stateExtractField(bodyContent, 'Current Phase Name');
const currentPlan = stateExtractField(bodyContent, 'Current Plan');
const totalPhasesRaw = stateExtractField(bodyContent, 'Total Phases');
const totalPlansRaw = stateExtractField(bodyContent, 'Total Plans in Phase');
const status = stateExtractField(bodyContent, 'Status');
const progressRaw = stateExtractField(bodyContent, 'Progress');
const lastActivity = stateExtractField(bodyContent, 'Last Activity');
const stoppedAt = stateExtractField(bodyContent, 'Stopped At') || stateExtractField(bodyContent, 'Stopped at');
const pausedAt = stateExtractField(bodyContent, 'Paused At');
let milestone = null;
let milestoneName = null;