Repository navigation
Count annotation lines from the source, not from parsed metadata - #43
Merged
Merged
Conversation
…adata Appending after a task that carried a hand-written `@depends-on: a, b, c` put the new task two lines too far down, outside the `## Tasks` section entirely. In the repro the Notes heading ended up separated from its own prose. `count_annotation_lines` added one line per parsed dependency. That holds for what `MarkdownEmitter::format_task_annotations` writes, one reference per line, but the parser splits a comma-separated value into several references off a single line, so the count ran two lines long. `@labels` had the same problem from the other direction and was simply skipped, with a comment saying it could not be counted, because labels can be inline on the checkbox line or in an `@labels:` block and the parsed metadata does not say which. Both are the same defect: the number of source lines cannot be recovered from parsed metadata, because the same metadata can be written in more than one shape. So record it at parse time instead. The parser already collects the annotation lines for a task and therefore already knows the count; it now keeps it on `Task::annotation_line_count`, and the resolver uses that. This settles the comma form, `@labels`, and any future annotation with more than one written shape, without the resolver having to know about any of them. Tasks built in memory rather than parsed (the TUI's in-memory tasks, test fixtures) have no source lines to have counted, so `annotation_line_count` is 0 and the resolver falls back to deriving a count from the metadata, assuming the emitter's shape. That keeps existing behaviour for callers that never touch a file. Tests: three integration tests appending after a task with comma-form `@depends-on`, with one `@depends-on` per line, and with block-form `@labels`, each asserting the new task lands inside `## Tasks` and the following section keeps its content. Two unit tests covering the recorded count winning over the derived estimate, and the fallback for an unparsed task. The comma-form and block-labels tests fail without the fix. Also files two `lash format` bugs found while verifying: it deletes every section after `## Tasks`, and it duplicates inline labels on every run so the output never reaches a fixed point.
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.
Replaces #39, which GitHub closed when its base branch was deleted on merge of
#40. Same change, rebased onto main.
What was wrong
Appending after a task carrying a hand-written
@depends-on: a, b, cput thenew task two lines too far down, outside the
## Taskssection entirely. Inthe repro the
## Notesheading ended up separated from its own prose.count_annotation_linesadded one line per parsed dependency. That holds forwhat
MarkdownEmitter::format_task_annotationswrites, one reference per line,but the parser splits a comma-separated value into several references off a
single line, so the count ran two lines long.
When the task is last in the file the emitter's clamp hides this, which is why
it only bites when content follows.
@labelshad the same problem from the other direction and was simply skipped,with a comment saying it could not be counted, because labels can be inline on
the checkbox line or in an
@labels:block and the parsed metadata does notrecord which.
The fix
Both are one defect: the number of source lines cannot be recovered from parsed
metadata, because the same metadata can be written in more than one shape. So
record it at parse time. The parser already collects a task's annotation lines
and therefore already knows the count; it now keeps it on
Task::annotation_line_count, and the resolver uses that.This settles the comma form,
@labels, and any future annotation with morethan one written shape, without the resolver having to know about any of them.
Tasks built in memory rather than parsed (the TUI's in-memory tasks, test
fixtures) have no source lines to have counted, so the field is 0 and the
resolver falls back to deriving a count from the metadata on the emitter's
one-line-per-annotation assumption. That keeps existing behaviour for callers
that never touch a file.
Tests
Three integration tests appending after a task with comma-form
@depends-on,with one
@depends-onper line, and with block-form@labels, each assertingthe new task lands inside
## Tasksand the following section keeps itscontent. Two unit tests covering the recorded count winning over the derived
estimate, and the fallback for an unparsed task.
The comma-form and block-labels tests were confirmed to fail without the fix.
Also filed here
Two
lash formatbugs found while verifying this, filed inlash.index.mdrather than fixed here:
lash formatdeletes every section after## Tasks. A file with## Notesand
## Referencescomes back with only the header and the tasks. Exit code0, no warning.
lash formatduplicates inline labels on every run, so#docs #infrabecomes
#docs #infra #docs #infraand grows without bound.format --checkreports the file as needing formatting forever.