Skip to content

Count annotation lines from the source, not from parsed metadata - #43

Merged
fohara merged 1 commit into
mainfrom
fix/depends-on-line-count
Aug 9, 2026
Merged

fohara merged 1 commit into
mainfrom
fix/depends-on-line-count

Conversation

@fohara

@fohara fohara commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

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, 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.

When the task is last in the file the emitter's clamp hides this, which is why
it only bites when content follows.

@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
record 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 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 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-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 were confirmed to fail without the fix.

Also filed here

Two lash format bugs found while verifying this, filed in lash.index.md
rather than fixed here:

  • lash format deletes every section after ## Tasks. A file with ## Notes
    and ## References comes back with only the header and the tasks. Exit code
    0, no warning.
  • lash format duplicates inline labels on every run, so #docs #infra
    becomes #docs #infra #docs #infra and grows without bound. format --check
    reports the file as needing formatting forever.

…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.
@fohara
fohara merged commit dd1ee6a into main Aug 9, 2026
21 checks passed
@fohara
fohara deleted the fix/depends-on-line-count branch August 9, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant