From 56f588a8e70a598490afcb186906bcc13fd1d68a Mon Sep 17 00:00:00 2001 From: Josh Terry Date: Mon, 17 Aug 2026 09:41:30 -0700 Subject: [PATCH] Enforce the tier floor on verification; fix the stale refutation comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Evidence.Verified now requires Tier > T0: a pass at T0 confers nothing, because T0 IS the unverified tier — counting such a pass would mint StatusVerified with an effective tier that means "nothing checked this". Latent until now (no runner declares MaxTier T0), but the dispatcher's tier clamp made it reachable the moment one did. The StatusRefuted comment claimed refutation could be outvoted by a pass at a higher tier; weigh() has never worked that way — refutation dominates unconditionally, which is load-bearing. The comment now says what the code does. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp --- internal/receipt/receipt.go | 6 ++++-- internal/receipt/receipt_test.go | 23 +++++++++++++++++++++++ schema/schema.go | 16 ++++++++++------ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/internal/receipt/receipt.go b/internal/receipt/receipt.go index 6954a26..fdf4f40 100644 --- a/internal/receipt/receipt.go +++ b/internal/receipt/receipt.go @@ -193,8 +193,10 @@ func sanitizePaths(detail, repoRoot string) string { // by five tests where four pass and one fails does NOT hold, and letting // passing probes outvote a failing one would launder a defect into a // verified row. -// - verified: no refutation, and some probe ran and passed; effective tier is -// the highest a passing probe conferred. +// - verified: no refutation, and some probe ran, passed, and conferred a +// tier above T0 (Evidence.Verified enforces the tier floor — a T0 pass +// raises nothing); effective tier is the highest a passing probe +// conferred. // - unverified: nothing ran that could raise the claim. Remainder. func weigh(evs []schema.Evidence) (schema.Status, schema.Tier) { best := schema.T0Unverified diff --git a/internal/receipt/receipt_test.go b/internal/receipt/receipt_test.go index 0af1d67..0772992 100644 --- a/internal/receipt/receipt_test.go +++ b/internal/receipt/receipt_test.go @@ -96,6 +96,29 @@ func TestINV007_RefutationDominatesVerification(t *testing.T) { } } +// TestT0PassConfersNoVerification: a probe result that ran and passed but +// confers only T0 raises nothing — T0 IS the unverified tier, so accepting +// such a pass would mint a verified row whose effective tier means "nothing +// checked this". The claim stays in the remainder, and the pass does not +// dilute an honest refutation either. +func TestT0PassConfersNoVerification(t *testing.T) { + claims := []schema.Claim{{ID: "Z1", Shape: schema.ShapeInvariant, + ProbeIDs: []string{"noop:x"}}} + evidence := [][]schema.Evidence{{ + {ClaimID: "Z1", ProbeID: "noop:x", Tier: schema.T0Unverified, Ran: true, Passed: true}, + }} + r := Assemble(gitdiff.Change{}, claims, evidence, schema.Coverage{}) + if r.Results[0].Status != schema.StatusUnverified { + t.Fatalf("status = %q, want unverified (a T0 pass confers nothing)", r.Results[0].Status) + } + if len(r.Remainder) != 1 || r.Remainder[0].Claim.ID != "Z1" { + t.Fatalf("claim with only a T0 pass must land in the remainder") + } + if r.Summary.Verified != 0 { + t.Fatalf("summary.Verified = %d, want 0", r.Summary.Verified) + } +} + // TestRemainderSectionAlwaysRenders: the text receipt states the remainder even // when it is empty, so its absence is a declared result, not an omission. func TestRemainderSectionAlwaysRenders(t *testing.T) { diff --git a/schema/schema.go b/schema/schema.go index 70ee9a1..08698e5 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -192,10 +192,13 @@ type Evidence struct { Binding string `json:"binding,omitempty"` } -// Verified reports whether this evidence raises its claim: the probe ran and -// passed. A probe that ran and FAILED refutes the claim; a probe that did not -// run says nothing. -func (e Evidence) Verified() bool { return e.Ran && e.Passed } +// Verified reports whether this evidence raises its claim: the probe ran, +// passed, AND confers a tier above T0. A pass at T0 confers nothing — T0 IS +// the unverified tier, so counting such a pass as verification would mint +// StatusVerified with an effective tier that means "nothing checked this". +// A probe that ran and FAILED refutes the claim; a probe that did not run +// says nothing. +func (e Evidence) Verified() bool { return e.Ran && e.Passed && e.Tier > T0Unverified } // Refuted reports whether this evidence refutes its claim: the probe ran and // the claim did not hold. @@ -207,8 +210,9 @@ type Status string const ( // StatusVerified: at least one probe ran and passed. EffectiveTier > T0. StatusVerified Status = "verified" - // StatusRefuted: at least one probe ran and failed, and none passed at a - // higher tier. The merge gate should block on refuted claims. + // StatusRefuted: at least one probe ran and failed. Refutation dominates + // unconditionally — no volume or tier of passing probes outweighs a + // failure. The merge gate should block on refuted claims. StatusRefuted Status = "refuted" // StatusUnverified: nothing ran that could raise the claim. Remainder. StatusUnverified Status = "unverified"