Repository navigation
fix(add): stop inserting a task between the previous task's title and its body - #49
Merged
Merged
Conversation
… its body The parser records a task's checkbox and annotation lines but not its free-text body, so the emitter now walks the source past the whole block before inserting. Closes #48.
This was referenced Aug 10, 2026
Merged
fohara
added a commit
that referenced
this pull request
Aug 11, 2026
…nalysis (#52) Found by mutation-testing lash 0.3.1 with Flawd 0.8.0 (400 mutants sampled from 3528, per-test targeting, every survivor re-confirmed against the full suite). These eight survivors are all cases where a conditional's two branches produce the same test outcome, so inverting it changed nothing that any test observed. Three sit in code 0.3.1 just changed: - formatter: `normalize_whitespace` was only ever exercised through the helper it gates, never through the flag itself, so inverting the flag passed. The trailing-blank-line trim and the "don't re-emit a description that is already in the source" guard were likewise unasserted. - parser: a blank line between a checkbox and its annotation block. The parsed values did not pin this — annotations are recovered downstream either way — but `annotation_line_count` silently drops to 0, and that count is what the write paths use to find where a task's annotations end. A wrong count there is how `lash add` splices a task into a previous task's block, the data-loss class fixed in 0.3.1 (#49). - blocker analysis: `find_blocker_chains` was only tested on a chain one hop deep, where the walk exits on the "current task is not itself blocked" check before its own progress guard is ever what stops it. A four-node chain distinguishes them. The report's per-blocker status word and its chains section were also unpinned. Each test was verified to fail against its mutation and pass without it. No production code changed. One further survivor is left deliberately untested: in the parser's blank-line branch, `} else if !annotation_lines.is_empty() { break; }` is followed by an unconditional `break`, so both paths break identically and no test can observe the difference. It is an equivalent mutant, not a gap; the redundant branch could be removed but that is a change to production code and not this commit's business.
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.
Closes #48.
The bug
lash add -f <file>put the new- [ ]line directly after the last task's title instead of after that task's whole entry. The parser records a task's checkbox line and its annotation lines and nothing else, so a free-text body underneath — prose, numbered steps, acceptance criteria, indented note bullets — was invisible when the insertion point was computed.The previous task kept its title and lost its body; the new task inherited lines it had nothing to do with. Markdown is the source of truth, so the file itself was wrong for whoever read it next. Nothing surfaced it:
lash lintpasses on the damaged file, andlash showprints ID, title, status, file and labels but never the body. Present since 0.1.0.The fix
Resolved the same way
EndOfTasksSectionwas — the source text is the only thing that knows where a task's block ends, so the answer is computed in the emitter, which is already holding the file.InsertAnchor::AfterTaskBlock(line)carries "start looking here" through from the resolver, and the emitter walks forward past every line that continues the block (indented, not a checkbox) before inserting.Blank lines count as part of the block only when indented content resumes after them, so a body split into paragraphs stays whole while a blank line that genuinely ends the block still stops the walk.
Deriving this from the parsed model instead would have meant a new field on
Task, a fourth thing to keep in sync, and the same class of bug the launch-blocker sweep already fixed twice.Bodyless tasks resolve to exactly the line they did before, so the common case is untouched.
One formatting change
A new task inserted directly below another task's body now gets a blank line above it, matching the issue's expected output — butting it against the tail of someone else's prose reads as more of that prose, which is the confusion the insertion point exists to avoid. Tasks with no body still sit flush against each other.
Verification
The issue's repro script now produces its stated expected result exactly, and the reported line number matches where the task actually landed.
Tests added:
Full workspace suite, clippy and rustfmt are clean.
Found while fixing, filed not folded in
lash formatdeletes task bodies outright — same root cause. #44 stoppedformatfrom deleting whole sections, but the## Tasksspan is still regenerated from the model, and the model has no body to regenerate. A task with prose under it comes back as a bare checkbox line, exit code 0. Fixing that needsTaskFileto carry the body lines the parser drops, which is a larger change than this one. Tracked inlash.index.md; the lint rule suggested on #48 is worth revisiting once the model carries bodies, since until then the linter has nothing to check a body against.