Skip to content

fix(deferredwork): keep flat appends visible after canonical entries (#304) - #322

Merged
pbean merged 2 commits into
mainfrom
fix/deferred-flat-boundary
Jul 27, 2026
Merged

fix(deferredwork): keep flat appends visible after canonical entries (#304)#322
pbean merged 2 commits into
mainfrom
fix/deferred-flat-boundary

Conversation

@pbean

@pbean pbean commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

Closes #304. Continues #274 by @Haven2026, who found the bug and picked the right seam — their
commit is cherry-picked here with authorship intact, and #274 is closed in favour of this.

The bug

A flat appender block — the - source_spec: / summary: / evidence: shape the inner
bmad-dev-auto review step appends — landing after the last canonical ### DW-<n> entry is absorbed
into that entry's parse_ledger() span. parse_legacy() masks canonical spans before scanning, so
the block is invisible to it, and therefore to the sweep's migration trigger (sweep.py:437), its
leftovers read (sweep.py:261/708), bmad-loop sweep --dry-run (cli.py:1073) and the TUI
legacy view (tui/data.py:917). The session did its job and recorded the defer; nothing downstream
could ever see it.

parse_ledger() is the right place to fix it: it is the single source of the spans parse_legacy's
masking consumes.

What changed relative to #274

Two corrections, both measured. My review on #274 endorsed the strict three-line FLAT_ENTRY_RE
there; that was wrong, and this is the retraction.

1. The boundary must recognize the flat shape exactly as parse_legacy does. Keyed on the full
three-line block, it fixes 2 of 9 shapes the legacy parser accepts. Two of the misses are shapes
main already pins with named tests (test_flat_appender_missing_summary_falls_back,
test_flat_appender_in_done_section_is_done). FLAT_ENTRY_RE and _FLAT_SOURCE_RE now consume one
shared _FLAT_SOURCE_BODY so they cannot drift.

flat block after the last entry main three-line boundary here
source_spec + summary + evidence lost
source_spec + evidence (no summary) lost lost
source_spec + summary (no evidence) lost lost
summary/evidence in the other order lost lost
a field interleaved before summary lost lost
* bullet marker (_BULLET_RE accepts it) lost lost
tab or padded marker (-\t, -␣␣␣) lost lost
SOURCE_SPEC: uppercased lost

2. The boundary is searched from the entry's own status: line, never above it. A flat-shaped
bullet quoted inside an entry ahead of its status truncated the span over the status, leaving the
entry reading as neither open nor done — open_ids() drops it, classify() reports it malformed.
That is a new work-loss mode main does not have: one lost flat block traded for one lost tracked
entry, and in the two-block case the entry's own prose re-emitted as a phantom legacy finding. The
three-line regex did not prevent this; it only dodged the one fixture that happened to omit
summary:. An entry with no status line has nothing to protect, so its whole span stays fair game.

Also kept from #274: append_entry now writes the location: field (default n/a, which is what
both engine refile sites take), closing a real gap against the canonical shape in
deferred-work-format.md. Called out explicitly rather than left as a silent rider.

Not carried over: #274's commits 2–4 added ValueError raises inside the ledger mutators. That
is a different problem with a different shape and it is tracked on #305 — raising there turns
bad LLM output into a mid-sweep crash (sweep.py:911 feeds triage evidence into mark_done's
note, decisions.py:150 feeds resolution/intent, and _close_resolved is called bare from
sweep.py:541), against a documented contract that a close is "never allowed to fail the story or
crash the run". #305 has the layered design and is still open for the taking.

Verification

  • uv run pytest -q -n auto — 3201 passed, 24 skipped. uvx pyright@1.1.411 — 0 errors.
    trunk check --no-fix — clean.
  • Ablations. Remove the boundary → 17 tests fail. Remove only the status anchor → exactly the
    two guard tests fail. Substitute fix(deferredwork): keep flat appends visible after canonical entries #274's three-line boundary → 9 fail (the 7 shape misses plus both
    guard tests), so the suite discriminates this fix from that one.
  • Behavior preserved. All 8 pre-existing module-level fixtures in tests/test_deferredwork.py
    parse identically to main, both views — nothing about legacy or mixed ledgers moves.
  • End to end. bmad-loop sweep --dry-run on a ledger whose tail is a canonical entry followed
    by a partial flat block (no evidence: line — the shape the three-line boundary misses): main
    prints 1 open and nothing else; this branch prints 1 open plus
    1 legacy (pre-DW-format) entries, 1 open — a sweep would first migrate them.
  • Parser-only, no platform seam: no subprocess, git, mux or path construction touched, so nothing
    Linux/macOS/Windows-specific moves. CRLF ledgers are covered by a test.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed deferred-work entries appended after the final canonical entry being omitted during ledger parsing.
    • Improved handling of legacy and flat-format deferred-work blocks, including varied field orderings and line endings.
    • Ensured deferred-work blocks remain visible when positioned between or around canonical entries.
  • New Features

    • Appended entries now include a location field, defaulting to n/a, while preserving supplied locations.

Haven2026 and others added 2 commits July 26, 2026 20:10
The flat-block boundary has to recognize the shape exactly as parse_legacy
does, or the bug survives in the shapes that parser accepts: keyed on the
full three-line block it missed every partial one — including the
no-summary and no-evidence shapes two existing tests already pin — plus
`*` bullets and tab/padded markers. FLAT_ENTRY_RE and _FLAT_SOURCE_RE now
consume one shared opening-line pattern so they cannot drift apart.

The boundary is also searched from the entry's own `status:` line rather
than from the heading. Truncating over the status leaves the entry reading
as neither open nor done — open_ids() drops it, classify() reports it
malformed — which trades one lost flat block for one lost tracked entry,
the same failure class in the other direction. An entry with no status
line has nothing to protect.

Ablations: removing the boundary fails 17 tests; removing only the status
anchor fails exactly the two guard tests.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b49d5964-4fc4-42ea-8d69-ad4339eb9e64

📥 Commits

Reviewing files that changed from the base of the PR and between 7c3615a and b686e59.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/bmad_loop/deferredwork.py
  • tests/test_deferredwork.py

Walkthrough

Deferred-work parsing now excludes flat appended blocks from canonical entry spans, allowing legacy parsing to retain them. Canonical entry writing adds a location field with a default of n/a, with expanded regression coverage and changelog documentation.

Changes

Deferred-work ledger updates

Layer / File(s) Summary
Flat appender boundary parsing
src/bmad_loop/deferredwork.py, tests/test_deferredwork.py
Flat appender blocks are detected and excluded from canonical spans across field orders, bullet formats, CRLF input, malformed entries, and multiple-entry layouts.
Canonical location writing
src/bmad_loop/deferredwork.py, tests/test_deferredwork.py, CHANGELOG.md
append_entry writes location: n/a by default or preserves a supplied location; tests and changelog entries document the behavior.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Poem

A bunny found a ledger line,
Where flat-tail notes were hard to find.
Now spans stop short, locations glow,
And every deferred thought can show.
Hop, hop—the work is safe in sight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The append_entry location-field signature/writing change is not required by #304 and appears to be extra scope. Split the location-field change into a separate PR or justify it as part of #304 before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main fix: keeping flat appends visible after canonical entries.
Linked Issues check ✅ Passed The changes truncate canonical spans at flat appender blocks and preserve them for parse_legacy, matching #304.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/deferred-flat-boundary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a bug (#304) where a flat appender block landing after the last canonical ### DW-<n> entry was silently absorbed into that entry's span, making it invisible to parse_legacy() and every downstream consumer. The fix is applied in parse_ledger() — the single source of the spans that parse_legacy() masks.

  • Core fix in parse_ledger(): canonical spans now end at the first flat appender block found after the entry's own status: line. A shared _FLAT_SOURCE_BODY constant ensures FLAT_ENTRY_RE (boundary) and _FLAT_SOURCE_RE (legacy parser) cannot drift out of sync.
  • append_entry fix: the location: field (defaulting to \"n/a\") is now written to match the canonical format.
  • Test coverage: 12 new tests parametrize all 9 flat-block shapes parse_legacy accepts, both guard conditions, CRLF ledgers, sandwiched blocks, and the location: round-trip.

Confidence Score: 5/5

The change is parser-only, touches no subprocess/git/mux/path construction, and the added location: field is backward-compatible (keyword-only default, ignored by all existing parsers).

The fix correctly narrows parse_ledger()'s span computation, the status-anchor guard prevents the one new failure mode the simpler fix introduced, the shared _FLAT_SOURCE_BODY constant eliminates drift risk between the two recognizer regexes, and the 12 new tests discriminate this fix from the three-line boundary approach and from main.

Files Needing Attention: No files require special attention. deferredwork.py is the only logic-bearing file and its changes are localized to parse_ledger() and append_entry().

Important Files Changed

Filename Overview
src/bmad_loop/deferredwork.py Adds _FLAT_SOURCE_BODY / FLAT_ENTRY_RE to bound canonical spans at flat appender blocks; anchors flat search after the entry's status: line; adds location: field to append_entry output.
tests/test_deferredwork.py Adds 12 new tests covering all flat-block shapes parse_legacy accepts, the status-anchor guard, CRLF handling, sandwiched blocks, two consecutive flat blocks, and the new location: field round-trip.
CHANGELOG.md Adds changelog entry for #304 fix and the location: field addition; prose is accurate and cross-references #274.

Reviews (1): Last reviewed commit: "fix(deferredwork): anchor the flat bound..." | Re-trigger Greptile

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.

deferredwork: flat appender block after the last canonical DW entry is swallowed — invisible to parse_legacy and never swept

2 participants