Skip to content

feat(cli): add a shared output renderer; migrate anomalies to real Markdown - #80

Merged
copyleftdev merged 1 commit into
mainfrom
feat/render-abstraction
Jul 30, 2026
Merged

feat(cli): add a shared output renderer; migrate anomalies to real Markdown#80
copyleftdev merged 1 commit into
mainfrom
feat/render-abstraction

Conversation

@copyleftdev

Copy link
Copy Markdown
Owner

Refs #75 — closes the design question blocking its first half, and starts the migration.

The design question, answered

#75 asked for output to route through "the rendering system". The blocker was that no such system existed, and the design question I flagged was whether one model could cover outputs ranging from flat key/value (fingerprint) to nested arrays with per-row severity (separation).

It can, with five block typesHeading, Fields, Table, Note, Nested. Validated against the two most awkward shapes rather than the easiest: anomalies needs a nested list (type instabilities with their type distributions) and three tables with different columns.

Why the gap existed

Each command implemented every format itself, so most implemented one. The renderer inverts that: a command builds a Report, and adding a format costs one implementation here rather than twelve at the call sites.

anomalies migrated

Before, --format markdown was the text output verbatim. Now:

## Anomaly Summary
| Field | Value |
|---|---|
| Type instabilities | 0 |
| Numeric outliers | 1 |
| Rare values | 0 |

## Numeric Outliers
| PATH | VALUE | Z_MAD | MEDIAN | MAD |
|---|---|---|---|---|
| $[*].score | 22.1 | 13.0403 | 10.5000 | 0.6000 |

> Z_MAD is deviation from the median in MAD units, so it is robust to the
> outliers it is detecting.

Real headings, real tables, blockquoted notes. It also gains a summary block it didn't have, because the model made one cheap.

Format tracking is now per-format

Coverage is genuinely uneven — anomalies renders Markdown but has no compact-AI view — so tracking splits:

const RENDERS_MARKDOWN: &[&str] = &["essence", "anomalies"];
const RENDERS_COMPACT_AI: &[&str] = &["essence"];

A command joins a list only when it actually renders that format, so the notice from #79 can never claim more than is true. That makes the migration incremental and self-documenting: each command that lands removes its own warning.

$ vajra anomalies data.json --format markdown     # silent — real renderer
$ vajra anomalies data.json --format compact-ai   # warns — no AI view
$ vajra stats data.json --format markdown         # warns — not migrated

Renderer details that matter

  • Pipes escaped, newlines flattened in Markdown cells — a rule like a | b (which separation emits) would otherwise break the table.
  • Ragged rows don't panic — short rows pad, long rows aren't truncated.
  • Empty tables show their empty-state, not an empty table.
  • Notes survive every format — they carry the caveats that stop a number being over-read ("not a verdict", "rank by MI"), so silently dropping them in one format would be the worst possible loss.

Tests

10 for the renderer: both formats, markdown differs from text, pipe escaping, newline flattening, empty state, notes survive, nested lists, ragged rows, determinism.

3 more for tracking: per-format exactness, migrated command emits real Markdown (asserts ## and |---| present and output differs from text), and unmigrated stdout unchanged.

One test is deliberately a tripwire: unmigrated_command_stdout_is_unchanged points at stats and will fail when stats is migrated. That's the intended signal, and the doc comment says so.

Full workspace suite: 79 test binaries, 0 failures. cargo fmt --check clean. 0 clippy diagnostics in vajra-cli under --all-targets --all-features.

What remains on #75

  • Migrate the other ten commands — now mechanical, one at a time, each removing its own warning.
  • Decide whether compact-ai gets a bespoke per-command view or simply maps to json for non-essence commands. That's a product call, not a technical one.
  • The streaming half is unchanged and still needs the numerical-accuracy decision documented in the issue.

…rkdown

Closes the design question blocking #75's first half, and starts the
migration.

Commands formatted their own output per format with println!, which is
exactly why `--format markdown` and `--format compact-ai` fell through to
text for eleven of twelve commands: each command had to implement every
format, so most implemented one. A command now builds a `render::Report` —
headings, key/value fields, tables, notes, nested lists — and the renderer
emits it per format. Adding a format costs one implementation rather than
twelve.

The design question was whether one model covers outputs ranging from flat
key/value to nested detail lists. It does, with five block types, validated
against the two most awkward shapes rather than the easiest: `anomalies`
needs a nested list (type instabilities with their distributions) and three
tables with distinct columns.

`anomalies` is migrated and now emits genuine Markdown — `##` headings, real
`|---|` tables, `>` blockquoted notes — rather than the text output relabelled.
It also gains a summary block it did not have before, since the model made
one cheap.

Format tracking is now per format rather than per command, because coverage
is genuinely uneven: `anomalies` renders Markdown but has no compact-AI view.
A command joins RENDERS_MARKDOWN only when it actually renders Markdown, so
the notice added in #79 can never claim more than is true. That makes the
migration incremental and self-documenting.

Renderer details worth noting: pipes are escaped and newlines flattened
inside Markdown cells, so a rule like `a | b` cannot break a table; ragged
rows do not panic; empty tables show their empty-state rather than an empty
table; and notes survive every format, since they carry the caveats that
stop a number being over-read.

Tests: 10 for the renderer (both formats, markdown differs from text, pipe
escaping, newline flattening, empty state, notes survive, nested lists,
ragged rows, determinism) and 3 more for format tracking (per-format
exactness, migrated command emits real Markdown, unmigrated stdout
unchanged).

Refs #75. Remaining: migrate the other ten commands, and decide whether
compact-ai gets a bespoke view per command or maps to json.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@copyleftdev, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 24 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 957938df-e6e7-411e-aa01-de5a1d7c5ad4

📥 Commits

Reviewing files that changed from the base of the PR and between 2c38cdd and 2e46d5d.

📒 Files selected for processing (4)
  • docs/src/commands.md
  • vajra-cli/src/main.rs
  • vajra-cli/src/render.rs
  • vajra-cli/tests/format_honesty.rs

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.

@copyleftdev
copyleftdev merged commit b647d14 into main Jul 30, 2026
5 checks passed
@copyleftdev
copyleftdev deleted the feat/render-abstraction branch July 30, 2026 05:45
copyleftdev added a commit that referenced this pull request Jul 30, 2026
…down

Migrates score, core-team, drift and batch, completing the renderer work
started in #80. Markdown is now implemented for all fifteen commands,
verified by measuring each one rather than by reading match arms — the method
that got the claim lists wrong three times earlier in this series.

Each migration is the same shape, and each deletes more than it adds:

- score_to_text becomes score_report, dropping its manual column-width
  computation
- core-team stops calling render_core_team_text from vajra-stats, so the
  import goes; the function itself stays, still used by that crate's own tests
- drift's five hand-rolled sections become tables, and its "rank by effect"
  caveat becomes a note that now explains *why* JSD and Wasserstein values
  are not comparable rather than just saying to prefer effect
- batch's per-document stanza becomes one table, and its skipped-file list
  gains a note telling the reader to check the count before treating the
  result as complete

batch also gains --redact, which it never had. That completes the privacy fix
from the previous commit: all eleven commands with text output now route
through maybe_redact, where six did not before.

Test restructuring, because the end state changed what is true: three tests
asserted that some command falls back to text for markdown. Nothing does any
more, so they are removed rather than repointed at a command that does not
exist. compact-ai is still a genuine fallback for eleven commands, so the
notice path stays covered by a test aimed there.

RENDERS_MARKDOWN is kept as an explicit list even though it now names every
command. The machinery is what stops the claim drifting, and compact-ai still
needs it.

Closes the first half of #75. Remaining there: whether compact-ai gets a
bespoke per-command view or maps to json, and the streaming question.
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