Skip to content

feat: qualifier diff + first-class record --stdin#7

Merged
akesling merged 7 commits into
mainfrom
akesling/feedback
May 7, 2026
Merged

feat: qualifier diff + first-class record --stdin#7
akesling merged 7 commits into
mainfrom
akesling/feedback

Conversation

@akesling
Copy link
Copy Markdown
Contributor

@akesling akesling commented May 7, 2026

Summary

This branch lands two agent-flavoured CLI surfaces — qualifier diff <ref> and a first-class qualifier record --stdin — plus a review pass that recorded 13 concerns/suggestions against the existing surface. All git access in diff goes through gitoxide, not by shelling out to git.

What's new

qualifier diff <ref> (default main)

Three buckets, all reckoned by record id:

  • Added — active on HEAD, not present at <ref>. Annotations only; resolve-kind records filtered to avoid double-counting with the closer.
  • Resolved — active at <ref> but not on HEAD. The closer record (and its summary) are inlined when one exists, or the entry is marked removed if not.
  • Drifted — present at both refs but body.span.content_hash no longer matches the file's current content. Drift on records freshly added on this branch is suppressed (you just authored them; their span IS the current code). The drifted line is rendered as a compiler-style snippet.

Compares against the merge-base of HEAD with <ref> by default — what this branch introduces, not what's different right now. --from-tip opts back into ref-tip behaviour.

CI gating: --fail-on <KIND[,KIND...]> exits non-zero if Added contains a matching record; --fail-on-drift exits non-zero on any drift. Both compose. The diff body is always printed before the failure exit so the build log shows what triggered it.

Filters: --kind, --issuer-type, --subjects-only. Stable JSON output: { ref, base, from_tip, added, resolved, drifted }.

qualifier record --stdin (and qualifier emit --stdin) — first-class

  • Per-record stdout (one line per recorded entry); compact human form or full JSONL under --format json.
  • Line-numbered errors that echo the offending input (stdin line 7: summary must not be empty: {"kind":"pass",...}).
  • --continue-on-error collects every failure, writes the records that did pass, exits non-zero with a final count.
  • --dry-run validates without writing; output uses the verb would-record.
  • Under --format json, errors are JSON objects on stderr ({line, error, input}) followed by a {summary: {...}} trailer. The top-level qualifier: text line is suppressed so consumers can parse stderr line-by-line.

Trailing summary moved to stderr so JSON pipes stay clean.

gix-backed git access

The four git invocations per diff (rev-parse, merge-base, ls-tree, one git show per .qual file at <ref>) become in-process calls on a single gix::Repository. Tree enumeration is one breadth-first walk; each blob is fetched directly from the object database. For a project with N .qual files at <ref>, this collapses N+3 subprocess spawns into zero.

gix is gated behind the existing cli feature so the lib build remains lean.

Review trail

The first commit (f2bf07b review:) is itself a qualifier-shaped artifact: 13 records authored against this codebase via qualifier record, committed as .qual files alongside the code they annotate. Highlights:

  • emit produces records with empty id for custom record types — Record::Unknown's arm in finalize_record is a no-op.
  • emit's Unknown records serialize keys alphabetically rather than in canonical Metabox envelope order.
  • qualifier review walks from CWD instead of the project root, so it finds nothing from a subdirectory.
  • ls --unqualified is a stub; --kind counts conflate kinds; superseded records are counted.

Plus four refactor suggestions (drop unused petgraph dep, pin canonical IDs in a regression test, etc.).

Run qualifier diff main on this branch to see the full review trail rendered.

Stats

  • 5 commits: review findings → core feature → S-tier polish → gitoxide migration → docs.
  • 24 new integration tests, locking in every new flag and behaviour.
  • 192 tests passing total (90 lib + 88 cli_integration + 14 integration).

Test plan

  • Run qualifier diff main from this branch's root and read the rendered output as a reviewer would; check that the bucket grouping, header, and drift snippets read well.
  • Open qualifier agents diff and qualifier agents record and check the worked examples and flag tables match the implementation.

View in Codesmith
Need help on this PR? Tag @codesmith with what you need.

  • Let Codesmith autofix CI failures and bot reviews

akesling added 5 commits May 6, 2026 23:39
Recorded via `qualifier record` during a thorough pass over the codebase.
Highlights (concerns):

- emit produces records with empty `id` for custom record types — the
  Unknown arm of finalize_record is a no-op (src/cli/commands/emit.rs)
- emit's Unknown records serialize keys in alphabetical order rather than
  canonical Metabox envelope order
- `qualifier review` walks from CWD instead of the project root, so it
  finds nothing from a subdirectory (src/cli/commands/freshness.rs)
- ls --unqualified is a stub; --kind counts conflate kinds; superseded
  records are counted (src/cli/commands/ls.rs)
- figment config errors silently swallowed (src/cli/config.rs)
- HOME-based user config silently skipped on Windows
- subject_name() yields backslash-suffixed subjects on Windows

Plus four suggestions: drop unused petgraph dep, pin canonical IDs in a
test so AnnotationBody field reorders break loudly, extract shared
issuer/cycle plumbing across record/reply/resolve/emit, add a unit test
that HELP_TEMPLATE matches the Commands enum, and standardize
show/praise exit codes with ls.
Two new agent-flavoured surfaces:

`qualifier diff <ref>` (default `main`) compares the active record set on
HEAD against a git ref and reports three buckets: Added (new records on
this branch, annotations only, resolve-kind filtered to avoid
double-counting with the closer), Resolved (records active at <ref> but
not at HEAD, with the closer named when one exists), and Drifted
(records present at both refs whose body.span.content_hash no longer
matches current file content). Validates the ref up-front via
`git rev-parse --verify --quiet`, enumerates ref-side .qual paths via
`git ls-tree`, and pulls each via `git show <ref>:<path>`. Resolves the
project root from the absolute CWD so it works from subdirectories.
Both human and JSON output are stable.

`qualifier record --stdin` and `qualifier emit --stdin` now emit one
stdout line per recorded entry (compact human form or full JSONL under
--format json). Errors are reported as `stdin line N: <reason>` and
abort the batch. The trailing summary count moved to stderr so a
--format json pipe stays clean. The `--stdin` clap doc carries the full
JSONL shape inline; src/cli/commands/agents/pages/record.md adds a
worked example.

Eight new integration tests cover both surfaces (per-record output,
JSON line shape, line-numbered errors, added/resolved cycle, no-op,
bad ref, missing git repo, JSON shape).

176 tests pass; clippy clean.
`qualifier diff <ref>`:
- Default is now merge-base of HEAD with <ref>, not the ref tip. This
  matches what a PR proposes to introduce: records that landed on <ref>
  after the branch forked are treated as "old" and don't appear under
  Added. `--from-tip` opts back into the literal-tip behavior.
- `--fail-on <KIND[,KIND...]>` exits non-zero if Added contains any
  record matching one of the kinds. The diff body is still printed, so
  CI logs show what triggered the failure.
- `--fail-on-drift` exits non-zero if any record drifted.
- `--kind <K[,K...]>` and `--issuer-type <T>` filter all three buckets
  (Added, Resolved, Drifted) before display.
- `--subjects-only` prints affected subjects deduplicated and sorted,
  one per line. Pipes cleanly into `xargs qualifier show`.
- Resolved entries inline the closer's summary, not just its id.
- Drifted entries render the current span content as a compiler-style
  snippet (via existing span_context helper), so the user sees the
  lines that moved instead of just hash deltas.
- JSON output gains `base` (resolved sha) and `from_tip` fields; `ref`
  remains the user's input string. Header line on human output names
  the comparison point explicitly.

`qualifier record --stdin`:
- `--continue-on-error` collects every failed line, writes the records
  that did pass, and exits non-zero with a final count. Previously the
  batch aborted on the first error.
- `--dry-run` validates every line but writes nothing; output uses the
  verb `would-record` so a glance confirms nothing was committed.
- Errors always echo the offending input (truncated to 200 chars) so
  the user can see what was sent without re-piping.
- Under `--format json`, errors are emitted on stderr as JSON objects
  ({line, error, input}), followed by a `{"summary": {...}}` trailer.
  The top-level `qualifier:` text line is suppressed in JSON mode so
  consumers can parse stderr line-by-line.

16 new integration tests:
- diff: merge-base default, --from-tip, --fail-on (single/multi kind),
  --fail-on-drift, --kind filter, --issuer-type filter, --subjects-only
  (sorted+deduped), Resolved closer-summary inline, Drift span snippet,
  JSON shape (`base` + `from_tip`).
- record --stdin: --continue-on-error keeps valid lines, --dry-run
  writes nothing, --dry-run still validates, JSON errors are
  structured, default abort still echoes input.

Agents pages updated (record.md, diff.md) with worked examples for
every new flag and the merge-base default explained.

192 tests pass; clippy clean.
… out

The four `git` invocations per diff (rev-parse, merge-base, ls-tree,
and one `git show` per .qual file at <ref>) become in-process calls
on a single `gix::Repository`. Tree enumeration is now one
breadth-first walk that yields (path, blob_oid) pairs for every .qual
file at the comparison commit; each blob is then fetched directly
from the object database without spawning a process.

For a project with N .qual files at <ref>, this collapses N+3
subprocess spawns into zero — meaningful on Windows and on hot
CI loops, even at small N.

`gix` is gated behind the existing `cli` feature, so the lib build
remains lean for embedded callers.

All 88 existing diff/stdin integration tests pass unchanged; the
public CLI behaviour and JSON shape are byte-identical.
Module doc on `cli::commands::diff` describes the three-bucket semantics
(Added, Resolved, Drifted) and the two non-obvious nuances rustdoc
readers would otherwise miss: the resolve-kind dedupe in Added, and
that drift on freshly added records is suppressed.

Inline `<ref>` and `<location>` references in arg doc-comments wrapped
in backticks so rustdoc stops parsing them as HTML tags.
@akesling akesling changed the title feat: qualifier diff + first-class record --stdin (gitoxide-backed) feat: qualifier diff + first-class record --stdin May 7, 2026
Long paths and long summaries previously rendered diff rows at 100-200
chars wide, unreadable on a standard 80-col terminal. Each row now
tries a single-line form (`marker  KIND  LOC  SUMMARY  (ID)`); if it
overflows the width budget, the summary plus any extras (closer line
for Resolved, span snippet for Drifted) move to indented continuations
below an unbroken header line. Continuations are themselves truncated
with `…` if they would overflow.

Width is read from \$COLUMNS, defaulting to 80. The Drifted bucket no
longer prints a redundant "original:" prefix — the summary on the
header line covers it.

Locked in by a regression test that constructs a worst-case row
(deeply-nested path + 130-char summary + drifted span) and asserts
no rendered line exceeds 80 chars.
@akesling akesling force-pushed the akesling/feedback branch from 2e300b3 to 3e6cdeb Compare May 7, 2026 15:35
pnpm@latest (11.x) requires Node ≥22.5; the site job was pinned at 20
and started failing with `ERR_UNKNOWN_BUILTIN_MODULE: node:sqlite`.
Bumping to the active LTS unblocks the build.
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 7, 2026

🔍 Preview deployed: https://7a3b3710.qualifier-dev.pages.dev

@akesling akesling merged commit a6f6813 into main May 7, 2026
2 checks passed
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