fix(#4469): anchor beginPhaseCore's focusPattern regex to line start (#4577)

* fix(#4469): anchor beginPhaseCore's focusPattern regex to line start

focusPattern (src/state-transition.cts, beginPhaseCore's first-time-
execution branch) rewrites the **Current focus:** body line via a regex
with no line anchor and \s* (crosses newlines) instead of a same-line
whitespace class -- the same defect class already fixed for a different
function, stateReplaceField, in #4243/PR #4453. A bold **Current focus:**
quoted mid-sentence anywhere in the body (e.g. an Accumulated Context
bullet documenting the field) matched first and had the rest of its line
silently overwritten with the new focus label -- the #4010 data-loss
class, applied to a different field.

Anchored using the exact same idiom PR #4453 established:
^([ \t]*\*\*Current focus:\*\*[ \t]*)(.*)$ with the /m flag. [ \t]* (not
\s*) avoids consuming the newlines before the label into the match; $
documents the match ends at end-of-line.

Added a regression test to tests/state-transition.test.cjs: a body with
NO real **Current focus:** field yet present, but a mid-sentence prose
mention of the same bold text in an Accumulated Context bullet -- proving
the unanchored pattern would corrupt the prose (empirically verified via
direct node -e execution before wiring the test) while the anchored
pattern's .test() correctly returns false and leaves it untouched.

stateReplaceProgressPercent's bold branch (~line 51-71) is explicitly NOT
touched -- the issue itself flags it as entangled with the recorded #2177
form-priority decision, needing a maintainer call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* docs(#4469): backfill changeset PR number

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: sim <sim@local>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-09-09 14:02:59 -04:00
committed by GitHub
parent 37b965c0d1
commit e0e7531530
3 changed files with 61 additions and 1 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 4577
---
**`state begin-phase` no longer rewrites prose that merely quotes the Current-focus field label** — the bold-form rewrite was unanchored, so a bold label quoted mid-sentence elsewhere in the body (e.g. a historical note documenting the format) captured the update and silently destroyed the rest of its line while the real field went unset. Same fix shape as the #4243 fix to the shared field-replacement helper: anchored to line start, same-line whitespace only.

View File

@@ -1252,7 +1252,17 @@ function beginPhaseCore(
const focusLabel = intent.phaseName
? `Phase ${intent.phaseNumber} — ${intent.phaseName}`
: `Phase ${intent.phaseNumber}`;
const focusPattern = /(\*\*Current focus:\*\*\s*).*/i;
// #4469: anchored to line start with same-line whitespace only, mirroring
// #4243/PR #4453's fix to stateReplaceField's bold branch. The pre-fix
// pattern carried no `^`/`m` and used `\s*` (crosses newlines), so a bold
// `**Current focus:**` quoted mid-sentence elsewhere in the body (e.g. an
// Accumulated Context bullet) matched first and had the rest of its line
// silently overwritten with the new focus label -- the same #4010
// data-loss class. `[ \t]*` (not `\s*`) avoids consuming the newlines
// before the label into the match; `$` documents the match ends at
// end-of-line (inert here since `.` never crosses line terminators
// without `/s`, which is not set).
const focusPattern = /^([ \t]*\*\*Current focus:\*\*[ \t]*)(.*)$/im;
if (focusPattern.test(body)) {
body = body.replace(focusPattern, (_match, prefix: string) => `${prefix}${focusLabel}`);
updated.push('Current focus');

View File

@@ -374,6 +374,51 @@ describe('ADR-1769 Phase 1: beginPhase first-time body field updates', () => {
assert.ok(result.updated.includes('Current focus'),
`updated should include 'Current focus'; got ${JSON.stringify(result.updated)}`);
});
// #4469: same defect class as #4243/PR #4453's stateReplaceField bold-branch
// fix, applied here to beginPhaseCore's own standalone `focusPattern` regex,
// which carried no `^` anchor / `/m` flag and used `\s*` (crosses newlines)
// rather than same-line whitespace. A body with NO real `**Current focus:**`
// field yet present at the top, but a mid-sentence prose mention of the same
// bold text further down (an Accumulated Context bullet documenting the
// format), demonstrates the corruption: the unanchored pattern's first (and
// only, since it's not global) match is the prose occurrence, and its rest-
// of-line gets silently overwritten with the new focus label.
test('anchored **Current focus:** rewrite leaves a mid-sentence prose lookalike untouched (#4469)', () => {
const body = [
'# Project State',
'',
'**Status:** Planning',
'**Current Phase:** 02',
'**Current Phase Name:** Previous Phase',
'**Current Plan:** 02',
'**Total Plans in Phase:** 3',
'**Last Activity:** 2026-06-20',
'**Last Activity Description:** previous work',
'',
'## Accumulated Context',
'',
'### Decisions',
'',
'- Archived phases historically rendered `**Current focus:**` inline in prose. Must not change.',
'',
'## Current Position',
'',
'Phase: 2 (Previous Phase)',
'Plan: 2 of 3',
'Status: Planning',
'Last activity: 2026-06-20 — context gathered',
'',
].join('\n');
const result = transitionCore(body, intent, deps);
const proseLine = result.content.split('\n').find((l) => l.includes('Archived phases historically'));
assert.strictEqual(
proseLine,
'- Archived phases historically rendered `**Current focus:**` inline in prose. Must not change.',
`a mid-sentence prose mention of **Current focus:** must survive byte-identically, got: ${JSON.stringify(proseLine)}`,
);
});
});
// Fixture for resume: a STATE.md body where Status already contains