Follow-up from #355, where set_frontmatter_status was rewritten to verify its edit (the reader/writer split that let a corrupting or no-op write report success). Two non-preservations were pinned there as characterization rather than fixed, because both need their own evidence and one of them changes bytes on the standard fixture shape. Filing them together: they are the same contract, the same function, and the same line.
The function's contract is "a minimal in-place line replacement (not a YAML round-trip) so the spec's formatting, comments, and field order survive — only the status value changes". These are the two places it does not hold.
1. Line endings are relaid across the whole file
set_frontmatter_status reads with Path.read_text and writes with Path.write_text (src/bmad_loop/frontmatter.py). Universal-newline handling means a CRLF spec is read as LF and written back as LF in its entirety — not just the status line, and not just the frontmatter. For a function whose contract is "formatting survives", rewriting every line ending in the file is the largest possible violation of it, and it has always been there.
The fix is the one already reasoned out for the sibling writer in devcontract.py (see the comment above _atomic_write_spec): read_bytes().decode() + write_bytes(....encode()), with the detected line ending reused.
⚠️ frontmatter.py's module docstring deliberately bars importing platform_util (it pulls in subprocess), which is why this module writes with write_text rather than reusing atomic_replace. So the fix cannot simply borrow the sibling's helper — it needs its own CRLF characterization tests.
2. A trailing inline comment on the status line is dropped
_replace_value keeps everything through the colon and then writes <space><value>, so status: draft # set by hand becomes status: done. Preserving it requires knowing where the scalar ends, and a # inside a quoted value is not a comment — a wrong guess turns a working spec into a FrontmatterWriteError. That is exactly the guesswork the verified-edit rewrite exists to stop making, so it was pinned as a deliberate drop rather than half-solved.
Doing it safely now has an oracle it did not have before: the verified edit already re-parses the candidate block, so a comment-preserving render can be attempted and simply rejected by _verified if it did not mean what it claimed. That is a much better position than the pre-#355 code was in.
While in here
The writer's tests still live in tests/test_resolve.py (~12 references to set_frontmatter_status), left in place during #355 because moving them would have hidden the diff — characterize before restructuring. tests/test_frontmatter.py now exists and is where they belong. AGENTS.md: flat tests/ mirrors src modules by name.
Note on scope
The maintainer decision on #355 was explicitly that the line-ending relay is its own PR, so this should not be folded back into an awaiting-operator change.
Follow-up from #355, where
set_frontmatter_statuswas rewritten to verify its edit (the reader/writer split that let a corrupting or no-op write report success). Two non-preservations were pinned there as characterization rather than fixed, because both need their own evidence and one of them changes bytes on the standard fixture shape. Filing them together: they are the same contract, the same function, and the same line.The function's contract is "a minimal in-place line replacement (not a YAML round-trip) so the spec's formatting, comments, and field order survive — only the status value changes". These are the two places it does not hold.
1. Line endings are relaid across the whole file
set_frontmatter_statusreads withPath.read_textand writes withPath.write_text(src/bmad_loop/frontmatter.py). Universal-newline handling means a CRLF spec is read as LF and written back as LF in its entirety — not just the status line, and not just the frontmatter. For a function whose contract is "formatting survives", rewriting every line ending in the file is the largest possible violation of it, and it has always been there.The fix is the one already reasoned out for the sibling writer in
devcontract.py(see the comment above_atomic_write_spec):read_bytes().decode()+write_bytes(....encode()), with the detected line ending reused.frontmatter.py's module docstring deliberately bars importingplatform_util(it pulls insubprocess), which is why this module writes withwrite_textrather than reusingatomic_replace. So the fix cannot simply borrow the sibling's helper — it needs its own CRLF characterization tests.2. A trailing inline comment on the status line is dropped
_replace_valuekeeps everything through the colon and then writes<space><value>, sostatus: draft # set by handbecomesstatus: done. Preserving it requires knowing where the scalar ends, and a#inside a quoted value is not a comment — a wrong guess turns a working spec into aFrontmatterWriteError. That is exactly the guesswork the verified-edit rewrite exists to stop making, so it was pinned as a deliberate drop rather than half-solved.Doing it safely now has an oracle it did not have before: the verified edit already re-parses the candidate block, so a comment-preserving render can be attempted and simply rejected by
_verifiedif it did not mean what it claimed. That is a much better position than the pre-#355 code was in.While in here
The writer's tests still live in
tests/test_resolve.py(~12 references toset_frontmatter_status), left in place during #355 because moving them would have hidden the diff — characterize before restructuring.tests/test_frontmatter.pynow exists and is where they belong. AGENTS.md: flattests/mirrors src modules by name.Note on scope
The maintainer decision on #355 was explicitly that the line-ending relay is its own PR, so this should not be folded back into an awaiting-operator change.