fix(cli): say which runs a rate was counted over when filtered runs are set aside (#204) - #212
Conversation
…re set aside (#204) #202 taught `Vanished` to set a `dotnet test --filter` run aside, and stopped there on purpose. What it left behind is a report that publishes `population: allExecutions` — rendered `all runs`, "nothing was set aside" — on the one kind that sets something aside. `PopulationRules.For` was not touched by that change, so the marker whose whole job is to say which runs a rate was counted over said the opposite of what the provider did, and two shipped documents asserted it in words: `finding-populations.md` ("none — nothing is set aside") and `command-reference.md` ("an `all runs` finding carries no discount counts at all"). This is the failure `PopulationRule`'s own remark exists to prevent, arriving through the one route it does not guard — an existing kind changing what it counts. So `Vanished` maps to a fourth rule, `ExcludesPartialRuns`, rendered `-partial`. Eight columns, the same width as `all runs`, so the trailer budget and the source path are unmoved. A fourth member rather than a second, orthogonal marker: the dimensions are mutually exclusive in practice, because a kind either reads execution outcomes — and must then count every run the test appeared in, since a filtered run's outcomes are as true as any other run's — or it reads appearances, where discounting executions means nothing. There is no `…AndClusteredAndPartial` waiting to be written. ## Measured before changed, as the issue asked Seventeen full runs of a seventeen-test suite, then three `--filter` runs of one test, against the same store with the filtered runs removed: | | 17 full | +3 filtered | |---|---|---| | `tests` | 17 | 17 | | `excludedLowEvidence` | 0 | **0** | | `DurationRegression` on the planted slow test | present | **gone** | | `notMeasured.DurationRegression.awaitingRuns` | 0 | **0** | **The issue's third bullet is false as stated.** Filtered runs do not push tests under the per-test evidence floor, and cannot: `MeetsReportingFloor` reads the subject's own runs, and the window arm beside it is logically implied by the per-test arm while both constants are 5, so the count a filtered run inflates is not the count that decides. Recorded in `finding-populations.md` and pinned by a test that fails if either constant moves, because it is true *while* they are equal and not in general. ## What the measurement did find `DurationProvider` skips any test with no execution in the current slice, charging it to nothing, on the stated grounds that `Vanished` reports the disappearance instead. After #202 that is false exactly when it matters: `Vanished` sets filtered runs aside so it makes no claim about them, so with three filtered runs at the head of the store both duration kinds go silent across the suite and the summary goes on reporting that they read every test they were offered. The identical false inference #204 is about, in the one other place it lives. Where every run in the current slice covered part of the suite, both kinds now count the test as `awaitingRuns` — a measurement waiting on a run of the whole suite, which is what it is. The predicate is the slice and not the window: one full run in the "now" did ask, so a test missing from it has genuinely stopped and belongs to `Vanished` again. No arm arithmetic moves; a tally that already existed starts counting. On the real store, `awaitingRuns` goes 0 → 20 for both kinds. ## Deliberately not changed - **`healthy`**, neither recounted nor renamed. Counting it over covering runs breaks `tests − healthy = flagged`, because a test seen only in filtered runs stays in `tests` and could not be healthy. The number is unmoved by partiality; what moved it in the measurement was the lost duration finding, now fixed. - **The header**, which still says `20 runs`. Twenty runs were analysed. The caveat line gains the denominator instead — `3 of 20 runs covered part of the suite` — which is the only thing a header change would have added. - **`DurationProvider.ReferenceMedian`**, which handles filtered runs per test and needs no threshold. Unifying it onto `IsPartial` would discard a valid speed reading for the test that *was* selected. Recorded as a remark. Schema moves to **1.18**: `population` is a string consumers switch on and there is now a fourth value. ## Verification - `dotnet build Xping.Sdk.sln` — 0 warnings, 0 errors. - 986 CLI + 928 Core + 295 adapter tests pass. - 6 new tests: the marker and its exclusivity, the token table, the caveat denominator, the floor invariant, both duration slice shapes, and the issue's reproduction end to end in both directions. - On the real store, end to end: the caveat reads `3 of 20`, `partialSessions` 0 → 3, and the duration tallies stop claiming full coverage. Every "in N of 17 runs" becoming "in N of 20 runs" is the outcome-reading kinds keeping filtered runs by design — the decision #202 recorded, now visible per finding rather than inferred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
Codecov Report❌ Patch coverage is
... and 2 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🟡 Changes recommended
Requested documentation and API contract wording updates remain unresolved.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Updates filtered-run reporting with a new -partial population marker, improved duration handling, schema 1.18, and related tests and documentation.
Changes:
- Adds
ExcludesPartialRuns/-partial. - Tracks duration findings awaiting full-suite runs.
- Updates caveats, schema, tests, and documentation.
File summaries
| File | Summary |
|---|---|
tests/Xping.Cli.Tests/Report/ShareableOutputTests.cs |
Tests markers and caveat denominators. |
tests/Xping.Cli.Tests/Report/ReportVocabularyTests.cs |
Tests population token coverage. |
tests/Xping.Cli.Tests/Report/ReportEnvelopeTests.cs |
Tests schema and filtered-run behavior. |
tests/Xping.Cli.Tests/Report/EvidenceLevelResolverTests.cs |
Pins evidence-floor invariants. |
tests/Xping.Cli.Tests/Report/DurationProviderTests.cs |
Tests partial-slice duration behavior. |
tests/Xping.Cli.Tests/Commands/CliSurfaceTests.cs |
Updates CLI schema assertions. |
src/Xping.Cli/Report/Rendering/TextReportRenderer.cs |
Renders partial-session caveats. |
src/Xping.Cli/Report/Rendering/ReportVocabulary.cs |
Maps the new population token. |
src/Xping.Cli/Report/Providers/DurationProvider.cs |
Tracks awaiting runs for partial slices. |
src/Xping.Cli/Report/Model/PopulationRule.cs |
Adds the partial-run rule. |
src/Xping.Cli/Report/Contract/ReportEnvelope.cs |
Bumps the report schema. |
src/Xping.Cli/Report/AnalysisContext.cs |
Documents partial-session semantics. |
docs/known-limitations.md |
Documents duration limitations. |
docs/internals/finding-populations.md |
Updates population rules and rationale. |
docs/cli/command-reference.md |
Documents the new marker and schema value. |
Review details
Suppressed comments (5)
docs/cli/command-reference.md:105
- This sample now labels the finding
-partial, but its headline still saysearlier runs/last 3 runs. WhenVanishedhas a partial run to set aside, the renderer emitsfull runsin both clauses; otherwise the example contradicts the output it is documenting. Please update the sample wording to match the new marker.
ran in 12 of 17 earlier runs, absent from the last 3
evidence moderate | -partial | f_1d77e3f5 | .../ImportTests.cs:41
docs/cli/command-reference.md:365
- The new
excludesPartialRunsdescription says the population covers all counts insideevidence, butVanishedEvidence.ExecutionsInWindowintentionally remains a whole-window count, including partial sessions; only the appearance/rate denominator is restricted to covering runs. Narrow this to the rate/denominator (or explicitly call out the exception) so consumers do not misinterpret that field.
Every finding carries a `headline` — the same sentence the rendered report prints — plus `metrics`, the labelled pairs behind it, and the raw `evidence` the two were resolved from. It also carries `population`, which is one of `allExecutions`, `excludesEnvironmental`, `excludesEnvironmentalAndClustered` or `excludesPartialRuns` and says which executions — or, for `excludesPartialRuns`, which runs — the counts inside `evidence` were taken over, and `evidenceSessions`, the number `evidenceLevel` was banded from.
docs/known-limitations.md:363
- The new heading is missing the relationship between the test and the selected run; please add “When No Recent Run Has Selected It” so the limitation is grammatically clear.
### The Duration Kinds Cannot Measure A Test No Recent Run Selected, And Say So Rather Than Reporting It
docs/known-limitations.md:369
- The final sentence overstates the new behavior: one full-suite run only removes the
currentSliceIsAllPartialspecial case.DurationRegressionstill requires three current comparable sessions (MinimumCurrentSessions), so after one full run it can remain inawaitingRuns;DurationUnstablecan also still be awaiting or unreadable. Please say that a full-suite run makes the test eligible for the normal duration gates, after which the usual evidence requirements apply.
tests are counted in `summary.notMeasured.DurationRegression.awaitingRuns` and
`.DurationUnstable.awaitingRuns`, and one run of the whole suite restores them.
src/Xping.Cli/Report/AnalysisContext.cs:102
- This new remark says the window's session count is “the one every rate below is read against,” but the
-partialmarker introduced here means Vanished rates use only full runs, while the other markers use execution populations. Please describe this as the denominator of the caveat/header and leave each finding's population marker as the rate denominator.
/// Reported in the summary as an observation rather than a discount, against the window's own
/// run count so the reader has the fraction and not just the fact. Only the kinds that read
/// absence set these sessions aside — every other kind reads outcomes of executions that
/// happened, and a filtered run's outcomes are as true as any other's — so the line says what
/// the window contains and does not claim every number was adjusted.
- Files reviewed: 15/15 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| Those three add up to the number of times the test ran in the window, which is the figure `executionsConsidered` is *not*. `discountedEnvironmental` and `discountedClustered` are published apart because they are set aside for unrelated reasons, and a reader chasing a surprising rate needs to know which one narrowed it. | ||
|
|
||
| The filter is there because an `all runs` finding carries no discount counts at all — there is nothing for it to report having set aside. Kinds counting runs rather than executions name their counts accordingly: `retryExhausted` and `retryDeepening` publish `discountedEnvironmentalRuns`, and `timeSensitive` publishes both that and `runsWithoutClock`, which is not a discount but a run whose session recorded no clock to place it on. | ||
| The filter is there because an `all runs` finding carries no discount counts at all — there is nothing for it to report having set aside. Kinds counting runs rather than executions name their counts accordingly: `retryExhausted` and `retryDeepening` publish `discountedEnvironmentalRuns`, `timeSensitive` publishes both that and `runsWithoutClock`, which is not a discount but a run whose session recorded no clock to place it on, and `stopped running` publishes `partialSessionsSetAside`. |
There was a problem hiding this comment.
Valid, and verified — thank you. select(.population != "allExecutions") did start matching Vanished the moment it left allExecutions, and the projection asked it for executionsConsidered and the two execution discounts, none of which it publishes. The output was a row of nulls.
Fixed in 8ec1028 by naming the execution populations positively rather than excluding one, and adding a second example for the run-based kind over the fields it actually carries. Both run against a real envelope:
$ jq ... select(.population | startswith("excludesEnvironmental")) ...
{"kind":"Flaky","population":"excludesEnvironmentalAndClustered","evidence":{"executionsConsidered":17,"discountedEnvironmental":0,"discountedClustered":0}}
$ jq ... select(.population == "excludesPartialRuns") ...
{"kind":"Vanished","population":"excludesPartialRuns","evidence":{"baselineSessions":14,"baselineSessionCount":14,"currentSessionCount":3,"partialSessionsSetAside":3}}
The line 365 half of this is addressed too, in the same commit — see the reply on that thread.
| | `ParallelSensitive` | excluded | kept | `ExcludesEnvironmental` | | ||
| | `TimeSensitive` | excluded (whole run) | kept | `ExcludesEnvironmental` | | ||
| | `Vanished` | **kept** | kept | `AllExecutions` | | ||
| | kind | environmental sessions | clustered failures | partial runs | rule | |
There was a problem hiding this comment.
Valid — the matrix gained a run-based rule and the prose around it did not follow. Fixed in 8ec1028.
The title is now "Which executions, or runs, each finding kind counts", and the opening paragraph says which kinds do which and why: most count executions, Vanished counts runs because it reads appearances rather than outcomes. The introduction to the not-measured section at the old line 158 had the same problem and is fixed with it.
| /// of the window rather than allowed to date it. 1.18 is where <c>population</c> gained | ||
| /// <c>excludesPartialRuns</c>, the rule <c>Vanished</c> had been applying since 1.13 while | ||
| /// still publishing <c>allExecutions</c> — a consumer switching on the value sees a fourth one. |
There was a problem hiding this comment.
Valid — PopulationRule's own summary was widened in the first commit and FindingDto.Population was missed. Fixed in 8ec1028.
Taken slightly further than asked, because reviewing it surfaced the sharper version of the same problem (also raised on command-reference.md:365): the field qualifies the denominator the rate was counted out of, not every figure in evidence beside it. VanishedEvidence.ExecutionsInWindow is deliberately a whole-window count with nothing set aside — "how often did this test run" and "over how many runs was its absence measured" are different questions, and folding both under one marker would make it mean less rather than more. The contract now says so explicitly instead of leaving the exception to be found.
…to every field beside it Review follow-up on #204. Six of the eight comments were valid and share one root: adding a run-based population rule left several places still asserting that every population is counted in executions. - **The `jq` example swept the new kind in and projected nothing.** It selected `.population != "allExecutions"`, which now matches `Vanished`, and then asked for `executionsConsidered` and the two execution discounts — a row of nulls. It now names the two execution populations positively, and a second example projects the run-based one over the fields it actually publishes. Both verified against a real envelope. - **`population` over-claimed its own scope.** It qualifies the denominator the finding's rate was counted out of, and not every figure in `evidence` beside it: `VanishedEvidence.ExecutionsInWindow` is deliberately a whole-window count with nothing set aside, because "how often did this test run" and "over how many runs was its absence measured" are different questions. Said so in `FindingDto.Population` and in the command reference rather than leaving the exception to be discovered. - **`finding-populations.md` contradicted its own new matrix** in its title and in the introduction to the not-measured section, both of which still said executions only. - **The caveat's comment claimed too much.** "the one every rate below is read against" is exactly what is no longer true, and is what the per-finding marker exists to say. The line scopes the window; the marker scopes the finding. - **"one run of the whole suite restores them" overstated the fix.** A full run lifts the partial-slice case and nothing else — the ordinary duration gates still apply after it, so a test can clear this and stay in `awaitingRuns` for the usual reason. - Heading grammar in `known-limitations.md`. Not taken: the sample finding in the command reference carries `-partial` beside a headline reading "earlier runs" rather than "full runs". That pair is correct and is a window holding no filtered run — the marker states the rule and not whether it fired, which the same page says two sections earlier and which is why `-env-cluster` appears on findings that had nothing clustered out of them. 986 CLI + 928 Core tests pass; no behaviour changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
|
Went through all eight review comments. Six were valid and are fixed in 8ec1028; one was already covered; one I am declining, with reasoning below. No behaviour changed — 986 CLI + 928 Core tests still pass. The three inline comments are answered on their own threads. The five suppressed ones have no thread to reply to, so:
Happy to revisit that last one if you read the sample differently. |
…er slice Review catch on the duration change in 6c66aaa, and a real one. The new arm decided `awaitingRuns` from a property of the window — is every run in the current slice partial? — and then charged every test missing from that slice. A slice of nothing but filtered runs holds two kinds of absent test at once, and only one of them is waiting on anything: - a test a `--filter` passed over, which still runs whenever the suite is run in full; - a test that had already stopped, which `Vanished` re-slices over the covering runs precisely so it can still report. Charging both put every vanished test in the tally *and* in a `Vanished` finding — the same disappearance under two names, which is the thing this skip exists to avoid and which the code comment and `finding-populations.md` both claimed it did avoid. The PR's own test baked it in: `FilteredRunsMoveWhatCouldBeMeasuredAndNotTheEvidenceFloor` asserted 16 over a 17-test suite where the 16 were the fifteen `Stable` tests *plus* `Removed`, the vanished test the same test asserts is reported as `Vanished`. So the question is now put per test, to the runs that did cover the suite: a test the most recent of those ran is waiting on a full run; a test they asked about and did not find has genuinely gone and stays with `Vanished` alone. The set comes from `AnalysisWindowSlices.From`, the same re-split `Vanished` reads, so the two cannot disagree about which runs were in a position to ask — and it is derived only when the slice is wholly partial, which is the only time the distinction is needed. The envelope test drops to 15 and now also pins that the missing one is `Removed`. A new provider test stops a test three covering runs before the filtered tail and asserts it is absent from the tally; it fails at 12 against the old per-slice predicate and passes at 11 against this one, so it pins the distinction rather than the arithmetic. 987 CLI + 928 Core tests pass. On the real store the figure is unchanged at 20, because nothing vanished there — which is the point: this only ever removes tests that another kind is already reporting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc
|
Code review found one real defect in the duration change. Fixed in 5b088da. The arm decided from a property of the window, not of the test.
Charging both put every vanished test in the tally and in a The PR's own test had baked it in, which is the part worth flagging: The fix asks per test, against the runs that did cover the suite: a test the most recent of those ran is waiting on a full run; a test they asked about and did not find has genuinely gone and stays with The envelope test drops to 15 and now also pins that the one missing is 987 CLI + 928 Core tests pass. End to end on the real Everything else in the review came back clean: the marker plumbing end to end, the |
Closes #204.
The defect
#202 taught
Vanishedto set adotnet test --filterrun aside, and stopped there on purpose. What it left behind is a report that publishespopulation: allExecutions— renderedall runs, "nothing was set aside" — on the one kind that sets something aside.PopulationRules.Forwas not touched by that change (git show --stat c6d073e), so the marker whose whole job is to say which runs a rate was counted over said the opposite of what the provider did. Two shipped documents asserted it in words:docs/internals/finding-populations.md—Vanished… "none — nothing is set aside"docs/cli/command-reference.md— "anall runsfinding carries no discount counts at all — there is nothing for it to report having set aside"This is the failure
PopulationRule's own remark exists to prevent, arriving through the one route it does not guard: an existing kind changing what it counts, rather than a new kind arriving without a decision.The fix
Vanishedmaps to a fourth rule,ExcludesPartialRuns, rendered-partial. Eight columns — the same width asall runs— so the trailer budget and the source path are unmoved.A fourth enum member rather than a second, orthogonal marker on the finding: the dimensions are mutually exclusive in practice. A kind either reads execution outcomes, and must then count every run the test appeared in because a filtered run's outcomes are as true as any other run's, or it reads appearances, where discounting executions means nothing to it. There is no
ExcludesEnvironmentalAndClusteredAndPartialwaiting to be written. A second marker would also double the comparison a marker exists to make cheap.Measured before changed, as the issue asked
Seventeen full runs of a seventeen-test suite, then three
--filterruns of one test, against the same store with the filtered runs removed:testsexcludedLowEvidenceDurationRegressionon the planted slow testnotMeasured.DurationRegression.awaitingRunsThe issue's third bullet is false as stated. Filtered runs do not push tests under the per-test evidence floor, and cannot:
MeetsReportingFloorreads the subject's own runs, and the window arm beside it is logically implied by the per-test arm while both constants are 5 — so the count a filtered run inflates is not the count that decides. Recorded infinding-populations.mdand pinned by a test that fails if either constant moves, because it is true while they are equal and not in general.What the measurement did find
DurationProviderskips any test with no execution in the current slice, charging it to nothing, on the stated grounds thatVanishedreports the disappearance instead. After #202 that is false exactly when it matters:Vanishedsets filtered runs aside so it makes no claim about them, so with three filtered runs at the head of the store both duration kinds go silent across the whole suite and the summary goes on reporting that they read every test they were offered. The identical false inference #204 is about, in the one other place it lives.Where every run in the current slice covered part of the suite, both kinds now count the test as
awaitingRuns— a measurement waiting on a run of the whole suite, which is what it is. The predicate is the slice and not the window: one full run in the "now" did ask, so a test missing from it has genuinely stopped and belongs toVanishedagain. No arm arithmetic moves; a tally that already existed starts counting.Deliberately not changed
healthy, neither recounted nor renamed. Counting it over covering runs breakstests − healthy = flagged, because a test seen only in filtered runs stays intestsand could not be healthy. The number is unmoved by partiality; what moved it in the measurement was the lost duration finding, now fixed.20 runs. Twenty runs were analysed. The caveat line gains the denominator instead —3 of 20 runs covered part of the suite— which is the only thing a header change would have added.DurationProvider.ReferenceMedian, which handles filtered runs per test and needs no threshold. Unifying it ontoIsPartialwould discard a valid speed reading for the test that was selected in the filtered run. Recorded as a remark rather than changed.Schema moves to 1.18:
populationis a string consumers switch on and there is now a fourth value.Verification
dotnet build Xping.Sdk.sln— 0 warnings, 0 errors.Vanishedis the only kind carrying it, the token table, the caveat denominator, the floor invariant, both duration slice shapes, and the issue's reproduction end to end in both directions.SampleApp.MSTeststore, seventeen full runs then three--filterruns:notMeasured.DurationRegression.awaitingRunsand.DurationUnstable.awaitingRunsgo 0 → 20;partialSessionsgoes 0 → 3.One thing worth a reviewer's eye
In that real-store diff, every outcome-reading kind's
sessionsConsideredgoes 17 → 20 and every headline reads "in N of 20 runs" rather than "in N of 17". That is not a regression — it is the decision #202 recorded, that a filtered run's outcomes are as true as any other run's, and it was already the behaviour before this change. What is new is that each finding now carries a marker saying so, instead of leaving a reader to infer which of two denominators they are looking at.docs/internals/implementation-specs/is untracked onmain; I amendedcli-report-format-spec.mdthere to record the fourth token, but left it out of the commit since tracking those specs is a separate call.🤖 Generated with Claude Code
https://claude.ai/code/session_01TMQcZLBGFTd8wz3mcfn9vc