Commit Graph

1 Commits

Author SHA1 Message Date
Rezolv
a788afb120 fix(#4010): confine stateReplaceField to same-line whitespace so an empty field's following line survives (#4021)
* fix(#4010): confine stateReplaceField to same-line whitespace so an empty field's following line survives

stateReplaceField's bold and plain patterns used `\s*` for the label-to-value
gap, which matches the newline after an empty field; `(.*)` then captured the
following line and the rebuild discarded it -- silent STATE.md data loss on any
`state update` against an empty body field (Status:, Stopped at:, Paused at:),
with exit 0 and no warning.

Confine the gap to same-line whitespace (`[ \t]*`), mirroring the already-correct
read side (stateExtractField, src/state-document.cts:404/:409), and pin the
label-value separator to a single space when the label line had none, so an empty
field yields `**Status:** value` rather than a glued `**Status:**value`. Non-empty
and pipe-table replacements are byte-identical to prior behaviour.

ADR-3180 §7.7 makes stateExtractField the same-line-confined owner; this aligns
the writer to it. Regression test fails before / passes after and covers bold and
plain shapes, LF and CRLF, the non-empty byte-identity guard, and an end-to-end
transitionCore characterization at the consumer (ADR-3180 Decision 4(c)).

* chore(#4010): add changeset for the stateReplaceField empty-field fix

* test(#4010): add boundary and property coverage; scope the changeset's unchanged claim

Addresses review on #4021:
- Add boundary tests for the shapes the example tests missed: an empty field at
  end-of-document (no following line, bold + plain), two consecutive empty fields
  (only the target is filled, the other empty field's line survives), and an empty
  new value on an empty field (joinFieldReplacement synthesizes no dangling
  separator and the following line is preserved).
- Add a fast-check property over the bold/plain branches and joinFieldReplacement:
  for any field name, any values (empty fields included), and any new value,
  replacing one field changes only its own line and never the total line count —
  the invariant #4010 violated, now guarded directly.
- Scope the changeset's "unchanged" claim to ordinary space/tab separators (an
  exotic vertical-tab/form-feed separator, which no GSD template emits, now
  normalises to a single space).

* test(#4010): pin glued-separator non-empty field, scope joinFieldReplacement JSDoc

Round-3 review carried forward a Minor finding: joinFieldReplacement's JSDoc
still claimed non-empty replacements are unconditionally "byte-identical to
prior behaviour", but a non-empty field written with no label-to-value
separator (**Status:**value) gains a single inserted space under the narrowed
[ \t]* gap. Round 2 scoped only the changeset prose; the source JSDoc was left
making the false unconditional claim.

- Scope the JSDoc's byte-identity claim to ordinary space/tab separators and
  name the no-separator normalization as the one intentional exception.
- Add a test pinning the glued-separator case (**Status:**Planning): exactly
  one space inserted, following line survives, not byte-identical.

Emitted .cjs is gitignored (class-1), so no emitted-drift-ack applies.
build:lib clean; 74/74 state-document tests pass.

Claude-Session: https://claude.ai/code/session_01Mzmut6aeqZ1APfUBAkBZTR

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
2026-09-03 17:14:12 -04:00