Degrade a partial window to possibly_present instead of refusing it - #397
Conversation
`BookWindow::observed` refused a `WindowRead::Partial` read outright (`PresenceError::WindowIncomplete`). That placement was right about the hazard and wrong about its blast radius: only `Absent` ever needed a complete window, but a construction-time refusal withheld `Present` and `PossiblyPresent` too. Since the adapter has no source-side control total for a nonempty window, every nonempty window is `Partial` — so the tool answered nothing at all over any range that held vouchers. Move the gate to where `Absent` is produced. `BookWindow` now retains its `WindowRead` and exposes `read()`; both states construct. `decide` checks it at the sole `PresenceStatus::Absent` site, last among the withholdings, and degrades a `Partial` window's would-be `Absent` to `PossiblyPresent(UndecidedReason::WindowNotProvenComplete)`. `Present` and `PossiblyPresent` are produced from a `Partial` window exactly as before. `agent_presence.rs` continues past a nonempty window instead of refusing early, and reports the window's real `read` state rather than a hardcoded "complete". `agent_catalog.rs`'s `voucher_presence` description states the new contract. That description names the reason a caller will actually read, which is not the one `safe_reason_code` returns. `UndecidedReason` serialises through `rename_all = "snake_case"`, so an item's `reason` field carries `window_not_proven_complete`; `presence_window_not_proven_complete` is the error-path spelling and appears nowhere in a successful response. The description advertises the serde form, and the schema test now asserts the anchored string and rejects the prefixed one, so the two cannot drift apart again without failing. Tests pin the invariant the type system used to buy: a `Partial` window withholds `absent` when nothing resembles a proposal, and the identical contents read `Complete` still issue it — the pair, so the first cannot pass because `Absent` broke generally. ADR 0017 is updated to match: the completeness gate is documented at `Absent`'s production rather than at construction, and the three Consequences passages asserting construction-time refusal now describe verdict-time degradation. Its measured analysis is unchanged — the source-side control total is still the only thing that would close nonempty qualification, and the widened re-read stays rejected. The ADR is not a pinned compatibility-surface file; the three touched Rust files are, and the surface and matrix are resealed here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Evidence behind "no cardinality mechanism was found"Recording the measurements so that claim in the description rests on data rather than assertion. All from today; the offline parts are reproducible from the captures under
|
replay_the_twenty_invoice_engagement still asserted that a nonempty window fails closed with presence_window_incomplete and evidence state partial. #397 deleted that error variant and made a partial window answer, withholding only Absent, so the assertion named a string the tool can no longer produce. It compiled because the code is a string literal rather than the enum, and it never failed because the test is #[ignore]d for the lab. An ignored test cannot fail, so it rots without telling anyone -- the same shape as the tool description that advertised the prefixed reason code, caught in review on #397, and the second instance of it in the same change. The assertions now mirror the offline equivalent: the window answers, its read is partial, totals.absent is zero, and no item may come back absent, because absence is the only verdict that needs to have seen the whole range. Nothing in the tree references presence_window_incomplete any more. Not run against the lab: the replay needs BRIDGE_TALLY_LIVE_* set, the gateway free, and owner authorization. The assertions are corrected but unexercised. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#397 moved the window-completeness gate from `BookWindow::observed` to `Absent`'s production site, and #294 then synced to master. Both landed in this branch's base, conflicting in five files. Resolutions, none of which drop either side's work: - `book_presence.rs` (5 hunks). Kept this branch's `ObservedWindow` constructor, `ColumnEvidence` and `narration_evidence`; kept the base's `read` field and `read()` accessor; deleted the `WindowIncomplete` refusal the base removed. The withholding order in `decide` is preserved: `skipped_evidence`, `ManualNumberNotSupplied`, the party outcomes, then the window read, then `Absent`. - `agent_catalog.rs`. Both sides rewrote the `voucher_presence` description. Replayed this branch's two narration-marker sentences onto the base's rewritten contract text at anchors present verbatim in both, so neither description silently replaced the other. - `book_presence_tests.rs` (2 hunks). Took the base's renamed and inverted partial-window test through this branch's constructor, and set `codes.len()` by counting the `PresenceError` variants the test lists rather than by arithmetic on the two sides. - Both compatibility manifests. Took this branch's 217-pin set, which is a strict superset of the base's 216, then resealed with `scripts/reseal.sh` (36 hashes changed). Verified directly that no path is missing from either parent, since `--verify` passes over a dropped pin. Two fixes the merge itself required: `a_partial_window_withholds_absent_even_when_nothing_resembles_the_proposal` auto-merged cleanly against the pre-`ObservedWindow` positional constructor and would not have compiled. Ported. `a_party_difference_echoes_the_source_spelling_not_its_catalog_binding` passed on this branch and failed after the merge, deterministically. Cause is master, not either PR: "Rectify unqualified folded ledger binding" (#331) changed a folded name match from resolving a binding to only suggesting candidates, so the proposal's `alpha traders` no longer reached `PartyOutcome::Bound` and the party-difference block is guarded on `Bound`. An exact spelling would have made the test a duplicate of `a_party_difference_compares_the_observed_party_field_not_every_ledger` under a name that no longer described it, so the test moves to the basis that still decides without byte equality: an identifier embedded in the master name. Driven to failure by removing that identifier, to prove it is what carries the test. ADR 0017 named the type `RemoteIdEvidence`, which this branch renamed to `ColumnEvidence`, in three places. Corrected. The `RemoteIdEvidenceUnavailable` reason variant is unchanged. Verified under the pinned 1.96.0 toolchain: fmt clean; src-tauri workspace 1537 passed, 0 failed, 1 ignored (the manual live replay); doctests 2 passed; tools workspace 55 passed; clippy `-D warnings` clean on both workspaces with no warning or error lines; `reseal.sh --verify` current. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#308's open thread held that the nonempty-window short-circuit made `PresenceBasis::NarrationMarker` unreachable: the adapter refused before any verdict was built, so the basis was contract-complete and operationally dead. #397 was expected to fix that by letting a `Partial` window answer. Nothing proved it. The adapter's ADR 0018 tests cover reader/writer agreement, legacy labels, ambiguous markers, bounds and refusals, but no end-to-end marker verdict, and `window_xml` carries no `NARRATION` at all -- so the existing nonempty-window test's two `present` verdicts are both by voucher number. The reachability also depended on a step below where the reasoning stopped: the adapter must declare `narration_evidence: ColumnEvidence::Observed`, or the marker path stays dead with the short-circuit gone. It does. This drives it. The same nonempty (therefore `Partial`) window carries a marker in JV-1's narration, under `automatic` numbering so that Tally's discarding of a supplied number leaves the marker as the only basis that can produce `present`. It comes back `present` with `basis: narration_marker`. Proven by mutation rather than by passing: - Neutralise the marker in the narration and the verdict drops to `possibly_present`, so the marker is what decides. - Reinstate the pre-#397 short-circuit and the identical request returns `isError: true` with `presence_window_incomplete` and no verdicts at all, so the short-circuit is exactly what made the basis unreachable. Both restored byte-identical afterwards. `agent_presence_tests.rs` is pinned, so the surface is resealed: one hash changed, pin set still 217, no path missing from either parent. Verified under the pinned 1.96.0 toolchain: fmt clean; src-tauri workspace 1538 passed, 0 failed, 1 ignored (the manual live replay); doctests 2 passed; tools workspace 55 passed; clippy `-D warnings` clean on both workspaces with no warning or error lines; `reseal.sh --verify` exit 0, read directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`ci.yml` filtered `pull_request` to `branches: [master]`, so a pull request stacked on another branch triggered no run at all. The consequence was not a missing badge: such a PR reports `mergeStateStatus: CLEAN`, because the only check that does run is GitGuardian, and "clean" means "no required check failed" rather than "the suite ran". A stack accumulated unchecked changes until its root merged, at which point the first real run met several pull requests' worth of change at once. This was reached, not hypothesised. #397 merged into `claude/voucher-presence` with one check on it; #308 sat on that branch through a base sync, a conflict resolution and a test rewrite, showing CLEAN throughout. A manual `workflow_dispatch` on #308's branch was the first Bridge CI run it ever had, and it passed on macOS and Windows -- so the gap was coverage, not a latent failure. It could as easily have been the other way. The `push` trigger stays master-only: a branch worth checking has a pull request, and that is what triggers the run, so filtering the push side avoids a second run per branch rather than losing coverage. `ci.yml` is in the compatibility surface, so the surface and matrix are resealed: one hash changed, pin set unchanged at 212, `reseal.sh --verify` exit 0 read directly. Verified: `check-ci-workflow-consistency.mjs` passes; the compatibility gate passes; tools workspace 11 binaries, 53 passed, 0 failed. The workflow parses and keeps its 9 jobs, with `pull_request` now carrying only `types`. Not changed, and worth a separate decision: `dependency-security.yml` carries the same `branches: [master]` filter, and `docs/proposed-merge-gate-ci.md` proposes a merge-gate workflow with it too. Both inherit this gap. Co-authored-by: t <dev@example.invalid> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Stacked on #294 (base
claude/voucher-presence), like #308. It is one commit.The problem
BookWindow::observedrefused construction unlessread == WindowRead::Complete, so a window that could not be proven complete produced no verdict at all. The adapter has no source-side control total for a nonempty window, so every nonempty window isPartial— which meant thevoucher_presencetool refused every real window that held vouchers, returningpresence_window_incompleteinstead of answering.That placement was right about the hazard and wrong about its blast radius. Only one of the three verdicts ever needed a complete window:
PresentPossiblyPresentAbsentThe change
The gate moves from window construction to the sole
PresenceStatus::Absentproduction site.BookWindowretains itsWindowReadand exposesread(); both states construct.decidechecks it last among the existing withholdings — afterRemoteIdEvidenceUnavailable,ManualNumberNotSuppliedand the party outcomes — and degrades aPartialwindow's would-beAbsenttoPossiblyPresent(UndecidedReason::WindowNotProvenComplete).PresentandPossiblyPresentare produced from aPartialwindow exactly as before.PresenceError::WindowIncompleteis deleted rather than deprecated; nothing produced or consumed it afterwards.This is the fourth instance of an idiom already in that function, not a new mechanism.
What this does not claim
Absent→ "safe to post" path, andAbsentis still only reachable from a window proven complete. This narrows what is withheld, not what is guaranteed.Complete, and it remains the only shape from whichabsentis reachable.CMPINFO/VOUCHERis a company-level counter, not window cardinality.A bug found while documenting it
The
voucher_presencedescription advertised the reason aspresence_window_not_proven_complete. That issafe_reason_code's error-path spelling, and this flow no longer errors.UndecidedReasonserialises throughrename_all = "snake_case", so an item'sreasonfield carrieswindow_not_proven_complete— an agent matching the documented string would have matched nothing, silently, with no error anywhere.The test meant to catch it was
description.contains("presence_window_not_proven_complete"), which verified the documentation against itself and could not fail for the right reason. It is now anchored toreason `window_not_proven_complete`with a negative assertion on the prefixed form, so the two surfaces cannot drift back together silently. Driven to failure and restored byte-identical to confirm it discriminates.ADR 0017
Updated to match: §2 now states the completeness gate at
Absent's production keyed offread()rather than at construction, §4'sAbsentrow requires a window proven read whole, and the three Consequences passages asserting construction-time refusal describe verdict-time degradation. The measured analysis is untouched — the source-side control total remains the only route to nonempty qualification, and the widened re-read stays rejected at a measured 1.95x for a detector that licenses nothing.docs/adr/0017is not a pinned compatibility-surface file. The three pinned Rust files that changed are resealed here.Verification
Run locally under the pinned 1.96.0 toolchain, in CI's order:
cargo fmt --all --check(src-tauri)cargo test --workspace(src-tauri)cargo test --workspace --doc(src-tauri)cargo test --workspace(tools)cargo clippy --workspace --all-targets -D warnings(src-tauri)cargo clippy --workspace --all-targets -D warnings(tools)bridge-tally-compatibility gatecompatibility_gate_passed:unknown_claims=11:evidenced_claims=0Zero
warning:/error:lines across the whole log. The one ignored test isreplay_the_twenty_invoice_engagement, a manual owner-authorized live read that needs the lab reachable.The invariant is proven by a mirror pair — identical window contents and proposal,
PartialwithholdsAbsent,Completeyields it. Without the second, the first could pass because absence had broken generally. Two pre-existing tests encoded the old contract directly and were renamed and inverted rather than deleted.Not run: frontend/JS tests and the feature-gated protocol and live-read variants CI runs separately, which this change does not touch.
🤖 Generated with Claude Code