feat: qualifier diff + first-class record --stdin#7
Merged
Conversation
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.
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.
2e300b3 to
3e6cdeb
Compare
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.
|
🔍 Preview deployed: https://7a3b3710.qualifier-dev.pages.dev |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This branch lands two agent-flavoured CLI surfaces —
qualifier diff <ref>and a first-classqualifier record --stdin— plus a review pass that recorded 13 concerns/suggestions against the existing surface. All git access indiffgoes through gitoxide, not by shelling out togit.What's new
qualifier diff <ref>(defaultmain)Three buckets, all reckoned by record
id:HEAD, not present at<ref>. Annotations only; resolve-kind records filtered to avoid double-counting with the closer.<ref>but not onHEAD. The closer record (and its summary) are inlined when one exists, or the entry is markedremovedif not.body.span.content_hashno 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-tipopts back into ref-tip behaviour.CI gating:
--fail-on <KIND[,KIND...]>exits non-zero if Added contains a matching record;--fail-on-driftexits 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(andqualifier emit --stdin) — first-class--format json.stdin line 7: summary must not be empty: {"kind":"pass",...}).--continue-on-errorcollects every failure, writes the records that did pass, exits non-zero with a final count.--dry-runvalidates without writing; output uses the verbwould-record.--format json, errors are JSON objects on stderr ({line, error, input}) followed by a{summary: {...}}trailer. The top-levelqualifier: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 accessThe four
gitinvocations per diff (rev-parse, merge-base, ls-tree, onegit showper.qualfile at<ref>) become in-process calls on a singlegix::Repository. Tree enumeration is one breadth-first walk; each blob is fetched directly from the object database. For a project with N.qualfiles at<ref>, this collapses N+3 subprocess spawns into zero.gixis gated behind the existingclifeature so the lib build remains lean.Review trail
The first commit (
f2bf07b review:) is itself aqualifier-shaped artifact: 13 records authored against this codebase viaqualifier record, committed as.qualfiles alongside the code they annotate. Highlights:emitproduces records with emptyidfor custom record types —Record::Unknown's arm infinalize_recordis a no-op.emit's Unknown records serialize keys alphabetically rather than in canonical Metabox envelope order.qualifier reviewwalks from CWD instead of the project root, so it finds nothing from a subdirectory.ls --unqualifiedis a stub;--kindcounts conflate kinds; superseded records are counted.Plus four refactor suggestions (drop unused
petgraphdep, pin canonical IDs in a regression test, etc.).Run
qualifier diff mainon this branch to see the full review trail rendered.Stats
Test plan
qualifier diff mainfrom this branch's root and read the rendered output as a reviewer would; check that the bucket grouping, header, and drift snippets read well.qualifier agents diffandqualifier agents recordand check the worked examples and flag tables match the implementation.Need help on this PR? Tag
@codesmithwith what you need.