You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Each ranking stage now reports what it received, kept and how long it took: as a new stages array in check --format json, and per stage and file in --debug. This lets users measure what each stage saves and costs so they can tune the pipeline.
stage.Telemetry (new, internal/analysis/stage/telemetry.go) runs the stages and totals received, kept and time per stage, safe for concurrent files.
check --format json gains a top-level stages array of {name, received, kept, duration_ms}, one entry per stage in pipeline order, including the default rank stage. Existing fields (violations, count, failures) are unchanged.
--debug prints, per stage and file, Stage <name>: N candidate(s) received, M kept, a Kept: line with its score for each survivor, and a reason line (Below threshold or Cut by top-K limit) for every dropped candidate.
README documents the JSON fields; CLAUDE.md and ADRs 0014 and 0022 describe the design.
Out of scope
Token-usage reporting per stage (out of scope in the epic).
Telemetry in text output; default text output is unchanged.
Architectural notes
Accepted deviations from "identical behavior":
--debug no longer caps the Below threshold list at MaxKeep. The issue asks for every dropped candidate with its reason, so all of them are listed. Debug output grows on large corpora.
A stage handed no candidates is not scored (Stage.Apply returns before calling its scorer). A file with no scope-matched ADRs therefore makes no embedding call, and its stages report zeros including duration_ms, as the issue requires. Consequence: with on_error: fail, an embedding outage is no longer detected for a file that has no candidates (main exited 6 for it; this branch exits 0 with a clean run).
duration_ms is summed per file, so with concurrent files it can exceed wall-clock time. Stated in the README.
Adapted to #206 (on_error) during rebase: stage naming uses #206's Stage.Name, and the report carries both stages and failures. A stage that fails (either on_error mode) still appears in stages with its received count and time and kept: 0.
No new ADR: the design amends ADR 0022 (pipeline) and ADR 0014 (JSON document).
Follow-ups
The rank and rerank stage names are string literals in internal/analysis/stages.go and internal/analysis/stage/cosine.go; constants would be tidier.
QA / test plan
Verified locally:
go test ./... passes, including Docker-backed Postgres tests; go test -race on internal/analysis/... and internal/cli/... in a Linux container; containerised golangci-lint reports 0 issues; gofmt is clean; each commit builds and vets on its own.
Differential run of main (38b879a) against this branch using the mock-provider binary: 96 runs per binary across 4 pipelines (none, rank+rerank, rerank-only, on_error: fail), violating/clean/embed-failure files, scope-matching and non-matching ADRs, and text, --format json, --debug and --format json --debug modes. Text output without --debug is byte-identical for files with candidates; JSON differs only by the new stages field; exit codes and the LLM cache contents are identical. The only other differences are the new --debug lines and the skip-empty change above.
Mutation check (30 mutations of the new behavior) run before the rebase; the three surviving mutations now have tests. Not re-run after the rebase.
Not verified here, needs CI or follow-up: --suggest-fixes, baseline mode, --staged and --all with telemetry; embedding-call counts under a single-provider config.
Checklist
Title follows Conventional Commits (feat:, fix:, docs:, refactor:, build(deps):, etc.)
All acceptance criteria from the linked issue are met
go test -race -cover ./... passes (race run locally on the changed packages only; the full race run is left to CI)
golangci-lint run --timeout=5m is clean
CLAUDE.md updated if this changes build/test commands, adds or renames a top-level package, changes a cross-package interface, or adds a footgun
A new ADR added under docs/arch/ if this embodies an architecturally-significant decision (existing ADRs 0014 and 0022 amended instead)
Comments follow the 2-line-max, WHY-only convention
Include zero-valued stage telemetry when file discovery fails
internal/analysis/engine.go:146
Telemetry is created only after Content.GetFiles succeeds. runCheck still emits the JSON report when Run returns that error, so this path produces "stages": [] rather than one zero-valued entry per configured stage, violating the requirement that every pipeline stage be listed. Initialize the stage list/telemetry before file discovery and snapshot its zero stats on this early return.
Document duration_ms as total stage-application time
README.md:332
This describes duration_ms as scorer time, but Telemetry.Apply measures the entire Stage.Apply call, including thresholding, sorting, and generation of all debug lines. Since this metric is intended for cost tuning and can change under --debug, document it as total stage-application time rather than scoring time.
Both findings from the follow-up review were valid and are fixed and pushed (f8d4e5d, c773454).
Stages on a file-discovery failure:Engine.Run now builds the stage list and telemetry before calling GetFiles. On a discovery error it sets CollectedStages (under JSONOutput) to the zero-valued stats, so the JSON report that runCheck still writes lists every configured stage instead of "stages": []. TestEngine_ListsEveryStageWhenFileDiscoveryFails covers it.
duration_ms wording: the README now describes it as the total time spent applying the stage (scoring, thresholding and, under --debug, writing its debug output), since Telemetry.Apply times the whole Stage.Apply call. ADR 0022 already said 'time spent in Apply', so no change was needed there.
Lint, gofmt and go test -race on the changed packages pass locally; CI is re-running.
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
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
Each ranking stage now reports what it received, kept and how long it took: as a new
stagesarray incheck --format json, and per stage and file in--debug. This lets users measure what each stage saves and costs so they can tune the pipeline.Related issue
Closes #201
In scope
stage.Telemetry(new,internal/analysis/stage/telemetry.go) runs the stages and totals received, kept and time per stage, safe for concurrent files.check --format jsongains a top-levelstagesarray of{name, received, kept, duration_ms}, one entry per stage in pipeline order, including the defaultrankstage. Existing fields (violations,count,failures) are unchanged.--debugprints, per stage and file,Stage <name>: N candidate(s) received, M kept, aKept:line with its score for each survivor, and a reason line (Below thresholdorCut by top-K limit) for every dropped candidate.CLAUDE.mdand ADRs 0014 and 0022 describe the design.Out of scope
Architectural notes
Accepted deviations from "identical behavior":
--debugno longer caps theBelow thresholdlist atMaxKeep. The issue asks for every dropped candidate with its reason, so all of them are listed. Debug output grows on large corpora.Stage.Applyreturns before calling its scorer). A file with no scope-matched ADRs therefore makes no embedding call, and its stages report zeros includingduration_ms, as the issue requires. Consequence: withon_error: fail, an embedding outage is no longer detected for a file that has no candidates (mainexited6for it; this branch exits0with a clean run).duration_msis summed per file, so with concurrent files it can exceed wall-clock time. Stated in the README.Adapted to #206 (
on_error) during rebase: stage naming uses #206'sStage.Name, and the report carries bothstagesandfailures. A stage that fails (eitheron_errormode) still appears instageswith its received count and time andkept: 0.No new ADR: the design amends ADR 0022 (pipeline) and ADR 0014 (JSON document).
Follow-ups
rankandrerankstage names are string literals ininternal/analysis/stages.goandinternal/analysis/stage/cosine.go; constants would be tidier.QA / test plan
Verified locally:
go test ./...passes, including Docker-backed Postgres tests;go test -raceoninternal/analysis/...andinternal/cli/...in a Linux container; containerisedgolangci-lintreports 0 issues;gofmtis clean; each commit builds and vets on its own.main(38b879a) against this branch using the mock-provider binary: 96 runs per binary across 4 pipelines (none, rank+rerank, rerank-only,on_error: fail), violating/clean/embed-failure files, scope-matching and non-matching ADRs, and text,--format json,--debugand--format json --debugmodes. Text output without--debugis byte-identical for files with candidates; JSON differs only by the newstagesfield; exit codes and the LLM cache contents are identical. The only other differences are the new--debuglines and the skip-empty change above.Not verified here, needs CI or follow-up:
--suggest-fixes, baseline mode,--stagedand--allwith telemetry; embedding-call counts under a single-provider config.Checklist
feat:,fix:,docs:,refactor:,build(deps):, etc.)go test -race -cover ./...passes (race run locally on the changed packages only; the full race run is left to CI)golangci-lint run --timeout=5mis cleanCLAUDE.mdupdated if this changes build/test commands, adds or renames a top-level package, changes a cross-package interface, or adds a footgundocs/arch/if this embodies an architecturally-significant decision (existing ADRs 0014 and 0022 amended instead)🤖 Generated with Claude Code