Repository navigation
Stop lash format deleting sections and duplicating labels - #44
Merged
Merged
Conversation
Two bugs, both of which made `lash format` unsafe to run on a file it did not fully model. `format_file` rebuilt the file from the parsed model alone, and the model holds the header, the Description section and the task tree and nothing else. Every other section was silently deleted: a file with `## Notes` and `## References` came back with only the header and the tasks, exit code 0, no warning. This turned out to be broader than filed. A section *above* `## Tasks` went too, because the parser folds it into "overview" text that `TaskFile` never stored and the formatter never emitted. `format_file` now takes the source alongside the parsed file. It regenerates only the spans it owns — the H1 plus annotation block, the Description section, the Tasks section — and copies every other line through unchanged. Section boundaries come from `parser::header::section_span`, which runs through pulldown-cmark, so a `##` inside a fenced code block neither opens nor closes a section. Passing an empty source keeps the old model-only behaviour, which is what the existing unit tests want. The second bug: the parser records inline labels in metadata without removing them from the title, and the formatter wrote both. Every run appended another copy of every label, so `- [ ] one #docs` became `#docs #docs` and then `#docs #docs #docs`, and `format --check` reported the file as needing formatting forever. The formatter strips inline labels from the title and writes the sorted metadata list as the only place they appear. The parsed title keeps them, so `lash list` and search behave as they do today. Each generated block ends with its own blank separator and the source's is skipped rather than copied. Emitting both would add a blank line per run, which is the same non-idempotence in a different coat. Tests: sections above and below `## Tasks` survive and keep their order, a fenced heading does not split a section, labels are emitted once, an already-canonical file comes back byte-identical, and an idempotence property test over six shapes asserting `format(format(x)) == format(x)`. All six fail against the old behaviour. Also ticks off three items in lash.index.md that were fixed in #34 and #35 but never marked done.
fohara
force-pushed
the
fix/format-destroys-sections
branch
from
August 9, 2026 16:22
ccc55e3 to
cff612f
Compare
This was referenced Aug 9, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #41.
Two bugs, both of which made
lash formatunsafe to run on a file it did notfully model. Found while checking that the empty-Tasks-section fix (#36) left
files lint-clean, and filed in
lash.index.mdin #43.Sections were deleted
format_filerebuilt the file from the parsed model alone, and the model holdsthe header, the Description section and the task tree and nothing else. Every
other section was silently deleted. A file with
## Notesand## Referencescame back with only the header and the tasks, exit code 0, no warning.
Worse than the
lash addbugs it was found next to, becauseformatisdocumented as a normalizer and the README tells people to run it. This repo's
own
lash.index.mdhas a## Descriptionsection, which sits before## Tasksand is one of the two the model does hold, which is probably whynobody hit it.
It also turned out broader than filed. A section above
## Taskswent too: a## Backgroundbetween the header and the tasks is folded by the parser into"overview" text that
TaskFilenever stored and the formatter never emitted.format_filenow takes the source alongside the parsed file. It regeneratesonly the spans it owns (the H1 plus annotation block, the Description section,
the Tasks section) and copies every other line through unchanged. Section
boundaries come from a new
parser::header::section_span, which runs throughpulldown-cmark, so a
##inside a fenced code block neither opens nor closes asection. Passing an empty source keeps the old model-only behaviour, which is
what the existing unit tests want.
Labels were duplicated
The parser records inline labels in metadata without removing them from the
title, and the formatter wrote both. Every run appended another copy:
- [ ] one #docsbecame#docs #docs, then#docs #docs #docs. Formattingnever reached a fixed point, so
format --checkreported the file as needingformatting forever and the line grew without bound.
The formatter strips inline labels from the title and writes the sorted
metadata list as the only place they appear. The parsed title keeps them, so
lash listand search behave as they do today.Idempotence
Each generated block ends with its own blank separator and the source's is
skipped rather than copied. Emitting both would add a blank line per run, which
is the same non-idempotence in a different coat.
Tests
## Taskssurvive, and keep their order.## Tasksinside a code block does not split a section.format(format(x)) == format(x), which is the test that catches both bugs atonce.
All six were confirmed to fail against the old behaviour.
Also here
Three items in
lash.index.mdthat were fixed in #34 and #35 but never tickedoff are marked done, with what was actually verified.