fix(#4243): anchor stateReplaceProgressPercent bold form to line start (#4474)

* fix(#4243): anchor stateReplaceProgressPercent bold form to line start

The bold branch of stateReplaceProgressPercent carried no ^ and no /m flag,
so a bold percent-ish label quoted MID-SENTENCE inside prose — an
Accumulated Context bullet mentioning **Progress:** — captured the
machine-segment rewrite and destroyed the rest of its line, silently, while
the real Progress line stayed stale (and the frontmatter moved on without
it, breaking the #4213 surfaces-agree contract). Every caller
(cmdStateUpdateProgress, syncCore's percent arm, applyPostSyncPreservation)
feeds the whole document, so all three were exposed.

Anchored to ^([ \t]*\*\*Progress:\*\*[ \t]*)([^\r\n]*)$ with /im — the
exact idiom #4453 applied to stateReplaceField's bold branch (same-line
confinement per #4010: the leading class is [ \t]*, deliberately not \s*,
which can consume the newlines before the label into the match; $ is
explicit-and-inert and documents end-of-line).

#2177's recorded requirements all stand: frontmatter is stripped before
matching, the suffix-preserving machine-segment swap is untouched, and
bold-beats-plain priority now governs line-start forms, so an earlier
free-text plain Progress: line still cannot capture the rewrite ahead of the
real bold status line. Per the maintainer ruling (2026-09-07), #2177's
incidental bold-anywhere matching was not load-bearing.

* test(#4243): scope the C4 region check with splitLines, not a bare \n split

lint:ci (local/no-crlf-fragile-split) flagged the free-text-plain-line row's
content.split(/\n## /)[0] — a bare \n split on readFileSync content is
CRLF-fragile under Windows autocrlf. Same scoping via splitLines()
(src/text-lines.cts), which splits on \r?\n.

* chore(#4243): backfill PR number in changeset

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-07 04:27:35 -04:00
committed by GitHub
parent c4b6dbd486
commit 6ebe6372ce
4 changed files with 435 additions and 5 deletions

View File

@@ -55,11 +55,29 @@ export function formatProgressMachineSegment(percent: number): string {
// `syncCore`'s call here.
export function stateReplaceProgressPercent(content: string, percent: number): string | null {
const body = stripFrontmatter(content);
// #2177: bold `**Progress:**` anywhere in the body wins outright; the plain
// `^Progress:` form is the fallback only when no bold line exists, so an
// earlier free-text line starting with `Progress:` cannot capture the
// rewrite ahead of the real status line.
const boldProgressPattern = /(\*\*Progress:\*\*[ \t]*)([^\r\n]*)/i;
// #2177: bold `**Progress:**` takes priority over the plain `^Progress:`
// form, so an earlier free-text line starting with `Progress:` cannot
// capture the rewrite ahead of the real status line.
//
// #4243 (follow-up to #4453, maintainer ruling 2026-09-07): the bold form
// is also ANCHORED to line start, with same-line leading whitespace only —
// the exact idiom #4453 applied to stateReplaceField's bold branch. The
// pre-fix pattern carried no `^` and no `m` flag, so a bold percent-ish
// label quoted MID-SENTENCE inside prose (an Accumulated Context bullet
// mentioning `**Progress:**`) captured the machine-segment rewrite and
// destroyed the rest of its line, silently, while the real Progress line
// stayed stale — every caller (cmdStateUpdateProgress, syncCore's percent
// arm, applyPostSyncPreservation) feeds the whole document. #2177's own
// recorded requirements are unaffected: the frontmatter is stripped before
// matching (its defect was the YAML `progress:` key shadowing the body
// line), the suffix-preserving machine-segment swap is untouched, and the
// bold-beats-plain priority now governs LINE-START forms. The leading class
// is `[ \t]*`, deliberately NOT `\s*` — `^\s*\*\*` can consume the newlines
// before the label into the match and drop them on rebuild (#4010's
// same-line confinement hazard). `$` is explicit-and-inert (`[^\r\n]*`
// never crosses line terminators) and documents that the match ends at
// end-of-line.
const boldProgressPattern = /^([ \t]*\*\*Progress:\*\*[ \t]*)([^\r\n]*)$/im;
const plainProgressPattern = /^(Progress:[ \t]*)([^\r\n]*)/im;
const pattern = boldProgressPattern.test(body)
? boldProgressPattern