Skip to content

refactor(cli): extract the --json document builders into documents.py - #211

Merged
pbean merged 1 commit into
mainfrom
refactor/documents-module
Jul 20, 2026
Merged

refactor(cli): extract the --json document builders into documents.py#211
pbean merged 1 commit into
mainfrom
refactor/documents-module

Conversation

@pbean

@pbean pbean commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

Pure relocation, no issue (precedent: #197, #206).

Why

The six per-command --json builders and their schema constants lived as private functions in cli.py:

builder constant
_validate_document VALIDATE_SCHEMA_VERSION
_decisions_document DECISIONS_SCHEMA_VERSION
_status_document STATUS_SCHEMA_VERSION
_list_document LIST_SCHEMA_VERSION
_cleanup_document CLEANUP_SCHEMA_VERSION
_clean_document CLEAN_SCHEMA_VERSION

That was ~314 lines of the dispatch module, growing by a few hundred with every command that adopts the flag, and reachable only through argparse. But the contract they serve (machine.py) is not a CLI feature — the planned web app's Python backend needs to import these as a library, not shell out to the CLI and parse its stdout.

probe.py / diagnostics.py already make this split ("one finding, two render targets"). This generalizes it to the commands whose document is a dict rather than a rendered string.

What

New src/bmad_loop/documents.py: the library-level read-model projection layer — domain object in, contract document dict out, obeying machine.py's pure-document contract. No I/O, no process state, no printing, no exit codes: the caller loads, this layer projects, machine.emit writes. New commands add their builder here, not in cli.py.

cli.py re-imports the names explicitly, so cli.STATUS_SCHEMA_VERSION and cli._status_document keep resolving for existing callers.

_run_token_totals moves too — _status_document calls it, and leaving it behind would have made documents.py import the CLI. cmd_status still uses it for text rendering, through the same re-import.

This is a pure relocation

Zero behavior change, zero schema change, and no test file was touched. The existing tests are the proof: they pin doc["schema_version"] == cli.X_SCHEMA_VERSION, so they only pass if both the re-export and the document shape survived.

  • Verbatim bodies — AST-extracted each moved definition from git show HEAD:src/bmad_loop/cli.py and from the new module, compared as text: all 13 (7 functions + 6 constants) byte-identical, and none still defined in cli.py.
  • Full suite green, zero test edits — 2577 passed, 1 skipped; git diff -- tests/ empty.
  • Byte-identical outputstatus --json and list --json diffed before vs after on a fixture run covering all four terminal statuses (finished/stopped/paused/crashed), a persisted cache_read_weight snapshot, and a deferred task with a defer reason. Identical stdout, rc and stderr.
  • trunk check (no filter) clean.

One note for review

The six constants are re-exported but not referenced in cli.py, so ruff F401 flags them — and its autofix removes the re-export, breaking every cli.*_SCHEMA_VERSION reference. Caught here by the test suite. Each import carries a # noqa: F401 — re-export with a comment saying why; worth keeping when the next command lands.

import bmad_loop.documents costs ~35ms and pulls no textual, so the library layer is usable from a non-CLI frontend without dragging in the TUI.

Summary by CodeRabbit

  • Refactor
    • Centralized JSON output generation across validation, decisions, status, listing, cleanup, and clean commands.
    • Preserved existing command usage and output contracts while improving consistency across JSON responses.
    • JSON responses now include stable schema version information for supported commands.

The six per-command `--json` builders and their schema constants lived as
private functions in cli.py, where they had grown to ~314 lines of the
dispatch module and were reachable only through argparse. The contract they
serve (machine.py) is not a CLI feature: the planned web backend needs to
call these directly rather than shell out to the CLI and parse its stdout.

Move them verbatim to a new documents.py — the library-level read-model
projection layer, domain object in, contract document dict out. This is the
split probe.py and diagnostics.py already make ("one finding, two render
targets"), generalized to the commands whose document is a dict rather than
a rendered string.

Pure relocation: zero behavior change, zero schema change. The moved bodies
are byte-identical to their previous definitions, and the existing tests —
which pin doc["schema_version"] == cli.X_SCHEMA_VERSION — pass untouched,
which is what proves it. cli.py re-imports the names so those references
keep resolving.

_run_token_totals moves too: _status_document calls it, and leaving it in
cli.py would have made documents.py import the CLI. cmd_status still uses
it for the text rendering, via the same re-import.

Verified: full suite green with zero test edits (2577 passed), and
`status --json` / `list --json` byte-identical before vs after on a fixture
run covering all four terminal statuses, a weighted-token snapshot and a
deferred task.
@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

The CLI’s JSON document builders and schema constants were moved into a new library-level documents.py projection module. CLI handlers now import and re-export these shared builders without changing command signatures or subcommand wiring.

Changes

JSON projection centralization

Layer / File(s) Summary
Projection module and CLI wiring
src/bmad_loop/documents.py, src/bmad_loop/cli.py
The new projection module defines shared dependencies, while the CLI imports and re-exports JSON builders and schema-version constants.
Validation and decision documents
src/bmad_loop/documents.py
Validation reports and pending decisions are converted into versioned dictionaries containing findings, counts, options, and recommendations.
Run and maintenance documents
src/bmad_loop/documents.py
Status, list, cleanup, and clean outputs are built as versioned dictionaries with run metadata, token totals, and maintenance results.

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

Possibly related PRs

Poem

A rabbit found JSON tucked in the CLI,
And moved its bright builders where libraries lie.
Status and clean now share one clear way,
Decisions and findings hop neatly in array.
“Less digging,” it laughs, “more carrots today!”

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: moving JSON document builders from cli.py into documents.py.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 refactor/documents-module

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.

@pbean

pbean commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@CodeRabbit review

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/bmad_loop/documents.py (1)

46-349: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verbatim relocation confirmed; signatures match every cli.py call site.

Cross-checked _validate_document, _decisions_document, _run_token_totals, _status_document, _list_document, _cleanup_document, and _clean_document against their call sites in cli.py (lines 501, 1518, 1565, 1575, 1621, 1758-1762, 1878-1889) — parameters, defaults, and return shapes all line up, consistent with the stated pure-move objective.

One design note worth considering (not a defect): every builder is named with a leading underscore (_validate_document, etc.), yet the module docstring explicitly states the goal is for a future non-CLI frontend (the planned web backend) imports these builders directly and serializes them itself. A leading underscore conventionally signals "internal, do not import from outside this module," which works against that stated goal — external tooling/linters will flag the import as a private-member access. Since this module is explicitly meant to be a library-level public surface, consider dropping the underscore prefix (or introducing public aliases) before non-CLI consumers start depending on it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/bmad_loop/documents.py` around lines 46 - 349, Consider exposing the
document builders as public library APIs by removing the leading underscore from
`_validate_document`, `_decisions_document`, `_run_token_totals`,
`_status_document`, `_list_document`, `_cleanup_document`, and
`_clean_document`, or add public aliases for them. Update all `cli.py` call
sites and related references consistently while preserving each function’s
signature and return shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/bmad_loop/documents.py`:
- Around line 46-349: Consider exposing the document builders as public library
APIs by removing the leading underscore from `_validate_document`,
`_decisions_document`, `_run_token_totals`, `_status_document`,
`_list_document`, `_cleanup_document`, and `_clean_document`, or add public
aliases for them. Update all `cli.py` call sites and related references
consistently while preserving each function’s signature and return shape.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0a50554-26f7-44ab-ae96-56532dbf172c

📥 Commits

Reviewing files that changed from the base of the PR and between a86abd0 and 947ef02.

📒 Files selected for processing (2)
  • src/bmad_loop/cli.py
  • src/bmad_loop/documents.py

@pbean

pbean commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Valid, and the argument is stronger than stated — but deliberately out of scope here. Filed as #212.

You're right that the leading underscore contradicts the module docstring's own stated purpose. What sharpens it: the precedent this layer is modeled on names its whole projection surface public. probe.py exposes scan / probe / render_markdown / render_json / SCHEMA_VERSION; diagnostics.py exposes collect / collect_run / render_json / render_markdown / SCHEMA_VERSION. Not one underscore between them. And in documents.py the six *_SCHEMA_VERSION constants are already public while the builders are not — a split with no rationale behind it, just an artifact of what the names were inside cli.py.

I also checked the constraint your suggestion would run into, and it isn't there: no test calls a builder directly (the only grep hit is prose inside a docstring), so a rename would not force test edits.

Not doing it in this PR, for a reason specific to what this PR is. Its deliverable is a provable pure relocation — the bodies were verified byte-identical by AST-extracting each definition from git show HEAD:src/bmad_loop/cli.py and comparing as text, with the pre-existing doc["schema_version"] == cli.X_SCHEMA_VERSION assertions passing untouched as the proof. A rename in the same commit voids both halves of that and breaks the requirement that cli._x_document keep resolving. It is a separable change and reads better as one.

#212 covers it, including the one real judgement call you didn't have the context to see: _run_token_totals is also used by cmd_status for text rendering, so it may belong as a private sibling rather than part of the document API.

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@pbean Agreed. Keeping this PR as a mechanically provable relocation—including the existing cli._*_document compatibility surface—is the right boundary. #212 is the appropriate place to make the public projection API consistent with probe.py and diagnostics.py, while deciding independently whether _run_token_totals remains private due to its text-rendering use.

🐇🔍


✏️ Learnings added
Learnt from: pbean
URL: https://github.com/bmad-code-org/bmad-loop/pull/211

Timestamp: 2026-07-20T05:08:11.581Z
Learning: In `src/bmad_loop/documents.py`, the underscore-prefixed document builders were intentionally retained during PR `#211` as part of a byte-identical relocation from `src/bmad_loop/cli.py`; `cli._*_document` compatibility must remain intact in that relocation. Renaming the builders into a public document API is intentionally deferred to GitHub issue `#212`.

Learnt from: pbean
URL: https://github.com/bmad-code-org/bmad-loop/pull/211

Timestamp: 2026-07-20T05:08:11.581Z
Learning: In `src/bmad_loop/cli.py`, `_run_token_totals` is used by `cmd_status` for text rendering as well as by `_status_document`; therefore, it may appropriately remain a private helper rather than being exposed as part of the `documents.py` API.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@pbean
pbean merged commit 7afca39 into main Jul 20, 2026
9 checks passed
@pbean
pbean deleted the refactor/documents-module branch July 20, 2026 08:26
dracic pushed a commit to dracic/bmad-auto that referenced this pull request Jul 20, 2026
…#212)

documents.py is the library-level read-model projection layer, and its
docstring says outright that a non-CLI frontend (the planned web backend)
imports these builders directly. Every builder nonetheless kept the leading
underscore it had as a private function inside cli.py, which says the
opposite. The six *_SCHEMA_VERSION constants beside them were already
public; the split had no rationale, it was an artifact of the origin.

Deferred out of bmad-code-org#211 because that PR's deliverable was a provable verbatim
relocation (AST-identical bodies, untouched tests as the proof) — renaming
in the same commit would have voided both.

run_token_totals goes public too. The issue framed it as an open question,
on the grounds that it is a shared helper rather than document API and
could stay a private sibling. It cannot: cmd_status imports it across the
module boundary and calls it on the text path, and a private name imported
by another module is the exact contradiction being removed. The run_ prefix
stays — it marks run-level vs per-task aggregation, which is what the
function exists to get right.

No back-compat cli._x_document aliases: nothing in-tree needs them and they
would re-create the private surface this removes (same call the Unreleased
notes already record for runs.tmux_sessions -> mux_sessions). No __all__:
no leaf module in the package has one, including probe.py and diagnostics.py,
which this module is modelled on. No new CHANGELOG entry — nothing
user-visible changes — but the Unreleased entry naming _run_token_totals is
updated, since it is unshipped notes rather than history.

Also adds the library path's first coverage. Every other --json test reaches
the builders through cli.main, so nothing pinned the docstring's central
promise that the two paths agree. Two parity tests drive one fixture down
both and assert the same dict comes back. Equality is against the raw
builder return, not a json round-trip: a consumer holds this dict before
serializing, so a tuple where a list belongs is a real defect a round-trip
would hide — verified by injecting exactly that and watching the test fail.

conftest.make_validate_document (new in bmad-code-org#215) was already calling the
builder directly, so it needed the rename too. Note make_validate_document
contains _validate_document as a substring — the rename is word-boundary
anchored so it and its 13 call sites are untouched.
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