From 7156f02ed5c8d07962d68fbaf9d34276caa87bef Mon Sep 17 00:00:00 2001 From: 0Shard Date: Tue, 17 Mar 2026 17:18:48 +0200 Subject: [PATCH] fix: semver 3+ segment parsing and CRLF frontmatter corruption recovery - fix(core): getMilestoneInfo() version regex `\d+\.\d+` only matched 2-segment versions (v1.2). Changed to `\d+(?:\.\d+)+` to support 3+ segments (v1.2.1, v2.0.1). Same fix in roadmap.cjs milestone extraction pattern. - fix(state): stripFrontmatter() used `^---\n` (LF-only) which failed to strip CRLF frontmatter blocks. When STATE.md had dual frontmatter blocks from prior CRLF corruption, each writeStateMd() call preserved the stale block and prepended a new wrong one. Now handles CRLF and strips all stacked frontmatter blocks. - fix(frontmatter): extractFrontmatter() always used the first --- block. When dual blocks exist from corruption, the first is stale. Now uses the last block (most recent sync). --- get-shit-done/bin/lib/core.cjs | 10 ++++++---- get-shit-done/bin/lib/frontmatter.cjs | 6 +++++- get-shit-done/bin/lib/roadmap.cjs | 2 +- get-shit-done/bin/lib/state.cjs | 12 +++++++++++- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/get-shit-done/bin/lib/core.cjs b/get-shit-done/bin/lib/core.cjs index 8c1b427f8..79bdb08c2 100644 --- a/get-shit-done/bin/lib/core.cjs +++ b/get-shit-done/bin/lib/core.cjs @@ -512,7 +512,8 @@ function getMilestoneInfo(cwd) { // First: check for list-format roadmaps using 🚧 (in-progress) marker // e.g. "- 🚧 **v2.1 Belgium** — Phases 24-28 (in progress)" - const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+\.\d+)\s+([^*]+)\*\*/); + // e.g. "- 🚧 **v1.2.1 Tech Debt** — Phases 1-8 (in progress)" + const inProgressMatch = roadmap.match(/🚧\s*\*\*v(\d+(?:\.\d+)+)\s+([^*]+)\*\*/); if (inProgressMatch) { return { version: 'v' + inProgressMatch[1], @@ -523,15 +524,16 @@ function getMilestoneInfo(cwd) { // Second: heading-format roadmaps — strip shipped milestones in
blocks const cleaned = stripShippedMilestones(roadmap); // Extract version and name from the same ## heading for consistency - const headingMatch = cleaned.match(/## .*v(\d+\.\d+)[:\s]+([^\n(]+)/); + // Supports 2+ segment versions: v1.2, v1.2.1, v2.0.1, etc. + const headingMatch = cleaned.match(/## .*v(\d+(?:\.\d+)+)[:\s]+([^\n(]+)/); if (headingMatch) { return { version: 'v' + headingMatch[1], name: headingMatch[2].trim(), }; } - // Fallback: try bare version match - const versionMatch = cleaned.match(/v(\d+\.\d+)/); + // Fallback: try bare version match (greedy — capture longest version string) + const versionMatch = cleaned.match(/v(\d+(?:\.\d+)+)/); return { version: versionMatch ? versionMatch[0] : 'v1.0', name: 'milestone', diff --git a/get-shit-done/bin/lib/frontmatter.cjs b/get-shit-done/bin/lib/frontmatter.cjs index e5f500a68..d7bb698dd 100644 --- a/get-shit-done/bin/lib/frontmatter.cjs +++ b/get-shit-done/bin/lib/frontmatter.cjs @@ -10,7 +10,11 @@ const { safeReadFile, normalizeMd, output, error } = require('./core.cjs'); function extractFrontmatter(content) { const frontmatter = {}; - const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---/); + // Find ALL frontmatter blocks at the start of the file. + // If multiple blocks exist (corruption from CRLF mismatch), use the LAST one + // since it represents the most recent state sync. + const allBlocks = [...content.matchAll(/(?:^|\n)\s*---\r?\n([\s\S]+?)\r?\n---/g)]; + const match = allBlocks.length > 0 ? allBlocks[allBlocks.length - 1] : null; if (!match) return frontmatter; const yaml = match[1]; diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 3164b702c..466039284 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -181,7 +181,7 @@ function cmdRoadmapAnalyze(cwd, raw) { // Extract milestone info const milestones = []; - const milestonePattern = /##\s*(.*v(\d+\.\d+)[^(\n]*)/gi; + const milestonePattern = /##\s*(.*v(\d+(?:\.\d+)+)[^(\n]*)/gi; let mMatch; while ((mMatch = milestonePattern.exec(content)) !== null) { milestones.push({ diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 40bf8d2cc..f96c73e19 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -664,7 +664,17 @@ function buildStateFrontmatter(bodyContent, cwd) { } function stripFrontmatter(content) { - return content.replace(/^---\n[\s\S]*?\n---\n*/, ''); + // Strip ALL frontmatter blocks at the start of the file. + // Handles CRLF line endings and multiple stacked blocks (corruption recovery). + // Greedy: keeps stripping ---...--- blocks separated by optional whitespace. + let result = content; + // eslint-disable-next-line no-constant-condition + while (true) { + const stripped = result.replace(/^\s*---\r?\n[\s\S]*?\r?\n---\s*/, ''); + if (stripped === result) break; + result = stripped; + } + return result; } function syncStateFrontmatter(content, cwd) {