From df12fba262ceba3c4eb4f0b54a5d31a786ad3f84 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 09:42:49 +0000 Subject: [PATCH 1/2] selector probe: sequence-aware coverage for local_disagreement present(encounter, seq) refuses a seq that is not greater than the last (the D-GSO-8 rule) and stamps contradiction bits; MooreInterrogation records the covered seq per bit. A tension bit reopens when never covered or when newer evidence arrived, so new evidence on a covered bit reopens the interrogation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MHMvKHLhM6xwRswKY4s6vY --- .claude/board/STATUS_BOARD.md | 2 +- .../2026-10-07-selector-seq-aware-coverage.md | 34 ++++ .claude/board/entries/README.md | 3 +- .../examples/recipe_selector_probe.rs | 160 ++++++++++++++++-- 4 files changed, 186 insertions(+), 13 deletions(-) create mode 100644 .claude/board/entries/2026-10-07-selector-seq-aware-coverage.md diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 0bf2f7685..21498c6bc 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -32,7 +32,7 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-GSO-3** | P3: global ontology agreement/disagreement probe (mammal fixture) | Shipped (#1350) | `cognitive-shader-driver/examples/ontology_agreement_probe.rs`: `NiblePath` hierarchy + `ontology_warrant::Quorum`/`SourceVerdict` folded per observation into local (self + siblings), up (ancestors), down (descendants); binary-entropy tension; all four cells + unknown reached; 6 tests, 5 disable runs red | | **D-GSO-4** | P4: entropy × topology routing probe | Shipped (#1351) | `cognitive-shader-driver/examples/entropy_topology_probe.rs` = F-ECG-1 and F-ECG-2 at probe level (F-ECG-3 open: no real corpus carries topology bits yet): bits 59..60 read through `BandDeclarations::project_truth` (topology lens, asserted provenance), known-share census as competence, `SettlementSignals::cell`, §4b walker route; equal measured entropy (0.8167 bits) → Crystal vs Glass; 6 tests, 4 disable runs red | | **D-GSO-5** | P5: first recipe quartet over existing primitives | Shipped (#1355) | `cognitive-shader-driver/examples/recipe_quartet_probe.rs`: four `match`-arm recipes over `Quorum::observe`, `Quad8::fold_product`, `Morton8x8` + `PalettePerturbation::hop`, `GadamerRevision::revise` (verdict left `NotRun`: removing the new roots is tautological, so the counterfactual moves to P7); a thread-local counting allocator shows zero allocations per recipe (no instruction vector); `ProbeRecipe` is its own type, not a shipped `recipes` ID; ordinal meanings not canonized; no Pearl projection (P7) and no selector (P6); 7 tests, 6 disable runs red | -| **D-GSO-6** | P6: deterministic, versioned recipe selector | Shipped (#1358, #1362, #1365); `observations_pending` + `frontier_bounded` wired, `new_encounter` → `revision_pending`: In PR | `cognitive-shader-driver/examples/recipe_selector_probe.rs`: `SelectorPolicy::select(state)` is a pure `const fn` over a declared 4-fact `EpistemicState`; V1 = §11 order (fold, bound, local, revise), rests only on the settled state; V2 swaps two steps (differs on 2 of 16 states); a recorded `Selection` (policy + state) replays under its own policy; every cycle rests in one step per open condition and replays identically; the version covers selection only, not recipe implementations (§13 digest open); 5 tests, 5 disable runs red. Follow-up: `new_encounter` is derived from the world (revising would still change a horizon mask; equals `revise` changing it on a 2-bit universe; rootless revisions included) and the `Revision` recipe writes `GadamerRevision`'s `delta.resulting`; dropping that write never rests; `unresolved_tension` cannot drive `local_disagreement` (never cleared); the other three facts are still stand-ins; 14 tests, 10 disable runs red. Follow-up 2: `local_disagreement` = `unresolved_tension \ interrogated`, where `interrogated` is the coverage `MooreInterrogation` writes (tension itself never cleared); a revision adding a new contradiction reopens it; dropping the interrogation write never rests; `observations_pending` and `frontier_bounded` remain stand-ins; 17 tests, 15 disable runs red. Follow-up 3: `new_encounter` renamed `revision_pending` (revising would change the horizon); `observations_pending` = arrived verdicts > verdicts the `Quorum` counted (silence included; `speaking()` would never rest on a silent source); dropping the fold write never rests; coverage stays per tension bit (new evidence on a covered bit does not reopen); `frontier_bounded` = a recorded `Quad8::fold_product` frontier exists for the current candidate space (a changed space reopens it; an empty frontier is bounded); no stand-in fact remains in the wired cycle; 25 tests, 25 disable runs red | +| **D-GSO-6** | P6: deterministic, versioned recipe selector | Shipped (#1358, #1362, #1365, #1368); sequence-aware coverage: In PR | `cognitive-shader-driver/examples/recipe_selector_probe.rs`: `SelectorPolicy::select(state)` is a pure `const fn` over a declared 4-fact `EpistemicState`; V1 = §11 order (fold, bound, local, revise), rests only on the settled state; V2 swaps two steps (differs on 2 of 16 states); a recorded `Selection` (policy + state) replays under its own policy; every cycle rests in one step per open condition and replays identically; the version covers selection only, not recipe implementations (§13 digest open); 5 tests, 5 disable runs red. Follow-up: `new_encounter` is derived from the world (revising would still change a horizon mask; equals `revise` changing it on a 2-bit universe; rootless revisions included) and the `Revision` recipe writes `GadamerRevision`'s `delta.resulting`; dropping that write never rests; `unresolved_tension` cannot drive `local_disagreement` (never cleared); the other three facts are still stand-ins; 14 tests, 10 disable runs red. Follow-up 2: `local_disagreement` = `unresolved_tension \ interrogated`, where `interrogated` is the coverage `MooreInterrogation` writes (tension itself never cleared); a revision adding a new contradiction reopens it; dropping the interrogation write never rests; `observations_pending` and `frontier_bounded` remain stand-ins; 17 tests, 15 disable runs red. Follow-up 3: `new_encounter` renamed `revision_pending` (revising would change the horizon); `observations_pending` = arrived verdicts > verdicts the `Quorum` counted (silence included; `speaking()` would never rest on a silent source); dropping the fold write never rests; coverage stays per tension bit (new evidence on a covered bit does not reopen); `frontier_bounded` = a recorded `Quad8::fold_product` frontier exists for the current candidate space (a changed space reopens it; an empty frontier is bounded); no stand-in fact remains in the wired cycle; 25 tests, 25 disable runs red. Follow-up 4: coverage is sequence-aware (D-GSO-8 rule): `present(encounter, seq)` refuses a `seq` not greater than the last and stamps contradiction bits; `MooreInterrogation` records the covered `seq` per bit; a tension bit reopens when never covered or when newer evidence arrived; 28 tests, 6 new disable runs red | | **D-GSO-7** | P7: reasoning-band earning and downgrade | Shipped (#1360) | `cognitive-shader-driver/examples/reasoning_band_probe.rs`: `ReasoningBand` (bits 61..63) read only via `band_reading::project_band`; earned one rung per executed passing proof under the shipped Pearl `CausalMask` (SO majority → Association, PO intervention trial passed → Causal, SPO removal attack passed → Counterfactual), outcomes executed from trial data and never supplied, no skipping, `NotRun` never raises; failed test, contradicting `Quorum` (minority caps at Association, majority drops to Surface) and `simpsons_paradox_risk` lower it; replay gives the same edge bits; unreadable provenance / undeclared / band-absent refuse; ladder and thresholds are policy pins; 7 tests, 10 disable runs red | | **D-GSO-7a** | P7a: the reasoning band as a relational certification (supersedes the meaning of D-GSO-7's rungs; #1360 unchanged) | Shipped (#1369) | `cognitive-shader-driver/examples/relational_certification_probe.rs`: a reading declared per class over bits 61..63 (`0 Open, 1 Associated, 2 Related, 3 Contributes, 4 CausalCandidate, 5 Causes`, 6..7 refuse; a class declared under the historical reading refuses the same bits); contracts are integer folds over unit masks + `causal_audit::SupportLedger` (2 distinct sources; the robustness mask only refutes; `Causes` = 2 distinct `InterventionBacked` sources + executed randomized arms); `entails` is an explicit table equal to `code >=` on 0..=5 only; repetition, observational effect size, the inference mantissa and confounded observational data never raise or demote past their contract; exhaustive 2,500-model family: 0 chain violations with scoped association, 10 with marginal (Simpson); sibling specificity and removal (overdetermination) are shown not to be rungs; only receipts recorded at or before the model's seal count, so a later receipt cannot change an earlier seal's replay; 13 tests, 12 disable runs red | | **D-GSO-AFF-0** | Affordance measurement: `CausalEdge64 × RecipeLaw[0..63] → EligibleRecipes: u64` | Shipped (#1370) | `cognitive-shader-driver/examples/affordance_measurement_probe.rs`: bits 59..63 read as one EpistemicState5 code (`spare() << 2 \| truth_raw()`, 10 declared codes, non-monotone, 22 refuse) + Pearl3; 7 recipes as `requires`/`forbids` facts + Pearl planes, compiled at build time to `[u64; 32]` × `[u64; 8]`; measure = 2 lookups + AND, 0 allocations; F1 determinism and law-generation dependence, F2 relevant factor flips its bit, F3 S/P/O/F/C/43..45/mantissa/plasticity/witness swept with no change, F4/F5 tables agree bit-for-bit with an independent rule re-derivation that names every refusal, F6/F7 preference only clears, half-field writes to 59..60 yield refusing codes; v1 / unknown provenance refuses (a v1 row with temporal 128 would read as `Causes`); no scheduler/task/capability object; 9 tests, 8 disable runs red | diff --git a/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md b/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md new file mode 100644 index 000000000..7a532963a --- /dev/null +++ b/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md @@ -0,0 +1,34 @@ +# 2026-10-07 — Selector follow-up 4: sequence-aware coverage + +Deliverable line: the D-GSO-6 selector (follow-up to #1368). Uses the D-GSO-8 +sequence rule (#1366). + +## MEASURED + +`crates/cognitive-shader-driver/examples/recipe_selector_probe.rs`, 28 +tests, 6 new disable runs red. + +- `present(encounter, seq)` refuses any `seq` not greater than the last + accepted one and leaves the world unchanged. An accepted encounter stamps + each of its contradiction bits with `seq`. +- `MooreInterrogation` records, per bit, the evidence `seq` it covered. + `local_disagreement` holds for a tension bit that was never covered, or + whose newest evidence is newer than its coverage. +- Presenting the same contradiction again at a newer `seq`, with nothing + else in the horizon changing, reopens the interrogation once + (`revision_pending` stays false) and records the new `seq`. Re-presenting + that `seq` is refused. +- Evidence on one bit does not reopen another bit. +- An encounter written without `present` carries `seq` 0 and never reopens a + covered bit, so the earlier tests are unchanged. + +Disable runs, each red: reuse of a `seq` allowed; the stale-coverage clause +removed; coverage `seq` not recorded; evidence not stamped; `>=` instead of +`>`; the encounter written before the refusal check. The last was green until +the refusal test presented a different encounter than the current one. + +## OPEN + +- Only encounters carry a `seq`. Verdict arrivals and space changes do not, + so they are not on one durable event chronology yet. +- `evidence_seq` and `covered_seq` are probe-local, like `interrogated`. diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 6efeaaeca..9d6026f8c 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,11 +25,12 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately opposite directions; the stranding this convention prevents shows up in exactly one of them, never both. -240 entries, 2026-08-06 .. 2026-10-07. +241 entries, 2026-08-06 .. 2026-10-07. | date | entry id | finding | file | |---|---|---|---| | 2026-10-07 | `tinker-janus-fold-harvest` | TinkerPop bulk = K (GroupReduce), ONE_BULK = S, GValue pinning = bundle invalidation; JanusGraph slice = OrderedLaneWitness→Range (30–38×); no new V4 op | [2026-10-07-tinker-janus-fold-harvest.md](2026-10-07-tinker-janus-fold-harvest.md) | +| 2026-10-07 | `selector-seq-aware-coverage` | | [2026-10-07-selector-seq-aware-coverage.md](2026-10-07-selector-seq-aware-coverage.md) | | 2026-10-07 | `gated-gather-bit-schedule` | Production gather is 3.8× slow from a per-row branch; bit-gated visitation wins or ties at every density; ordinal vectors never win (R1 holds) | [2026-10-07-gated-gather-bit-schedule.md](2026-10-07-gated-gather-bit-schedule.md) | | 2026-10-07 | `bind-bundle-gated-gather` | One bound query, five routes, identical answers; the semijoin is never gated and dominates; a gated mask gather beats a selection vector (R1 survives) | [2026-10-07-bind-bundle-gated-gather.md](2026-10-07-bind-bundle-gated-gather.md) | | 2026-10-06 | `v3-v4-dual-reading-round3` | | [2026-10-06-v3-v4-dual-reading-round3.md](2026-10-06-v3-v4-dual-reading-round3.md) | diff --git a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs index cb53931b3..a3742d3fd 100644 --- a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs +++ b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs @@ -66,9 +66,16 @@ //! adds a new contradiction reopens the fact, so a cycle can run `Revision` //! and then interrogate the tension that revision introduced. //! -//! Coverage is per tension bit, not per piece of evidence: new evidence on a -//! bit that is already covered does not reopen the interrogation. A coverage -//! that tracks receipts or generations would; it is not built here. +//! Coverage is sequence-aware, with the D-GSO-8 rule for sequence numbers: +//! an encounter is presented with a `seq` that must be strictly greater than +//! the last accepted one, or it is refused. Presenting it stamps each of its +//! contradiction bits with that `seq`. The `MooreInterrogation` recipe +//! records, per bit, the evidence `seq` it covered. A tension bit is open +//! when it was never covered, or when newer contradiction evidence arrived +//! after its coverage. So new evidence on an already-covered bit reopens the +//! interrogation, while presenting the same encounter again is refused +//! before it can. An encounter written without `present` carries `seq` 0 and +//! never reopens a covered bit. //! //! What this does not decide: `interrogated` is a probe-local record. The //! horizon carries no such field, and the Moore recipe's actual fold (the @@ -271,6 +278,14 @@ struct World { /// Tension bits `MooreInterrogation` has examined. Only that recipe /// writes it, and only by union. interrogated: u64, + /// The last accepted encounter `seq`. Sequence numbers strictly increase. + last_seq: Option, + /// Per claim bit, the `seq` of the newest contradiction evidence on it. + /// Only `present` writes it. + evidence_seq: [u64; 64], + /// Per claim bit, the evidence `seq` the last interrogation covered. + /// Only `MooreInterrogation` writes it. + covered_seq: [u64; 64], /// Verdicts that have arrived, in arrival order. Only arrival writes here. verdicts: [SourceVerdict; VERDICTS], /// How many of `verdicts` have arrived. @@ -295,6 +310,21 @@ struct Frontier { first: Option, } +/// An encounter `seq` that is not greater than the last accepted one. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct StaleSeq(u64); + +/// The set bits of a mask, lowest first. +fn bits(mut m: u64) -> impl Iterator { + std::iter::from_fn(move || { + (m != 0).then(|| { + let b = m.trailing_zeros() as usize; + m &= m - 1; + b + }) + }) +} + /// How many verdicts a world can hold. const VERDICTS: usize = 8; @@ -325,13 +355,28 @@ impl World { .is_empty() } - /// `local_disagreement`, derived: tension not yet interrogated. + /// `local_disagreement`, derived: a tension bit never interrogated, or + /// one with contradiction evidence newer than its coverage. fn local_disagreement(&self) -> bool { - !self - .horizon - .unresolved_tension - .difference(&self.interrogated) - .is_empty() + let tension = self.horizon.unresolved_tension; + let never = tension & !self.interrogated; + let stale = + bits(tension & self.interrogated).any(|b| self.evidence_seq[b] > self.covered_seq[b]); + never != 0 || stale + } + + /// The "do": an encounter arrives with a durable `seq`. Refused, with the + /// world unchanged, unless `seq` is greater than the last accepted one. + fn present(&mut self, encounter: EncounterEvidence, seq: u64) -> Result<(), StaleSeq> { + if self.last_seq.is_some_and(|last| seq <= last) { + return Err(StaleSeq(seq)); + } + self.last_seq = Some(seq); + for b in bits(encounter.contradictions) { + self.evidence_seq[b] = seq; + } + self.encounter = encounter; + Ok(()) } /// The declared state, every fact read from the world. @@ -404,9 +449,14 @@ impl World { .fold(self.quorum, |q, &v| q.observe(v)); } - /// The `MooreInterrogation` recipe's write: the tension it examined. + /// The `MooreInterrogation` recipe's write: the tension it examined, and + /// the evidence `seq` it covered on each bit. fn interrogate(&mut self) { - self.interrogated |= self.horizon.unresolved_tension; + let tension = self.horizon.unresolved_tension; + for b in bits(tension) { + self.covered_seq[b] = self.evidence_seq[b]; + } + self.interrogated |= tension; } /// The `Revision` recipe: revise once; `delta.resulting` is the only @@ -488,6 +538,9 @@ fn unbounded_fusion_world() -> World { affected_parts: 0b111, }, interrogated: 0, + last_seq: None, + evidence_seq: [0; 64], + covered_seq: [0; 64], verdicts: [SourceVerdict::Silent; VERDICTS], arrived: 0, quorum: Quorum::default(), @@ -521,6 +574,8 @@ fn main() { SelectorPolicy::CURRENT ); let mut world = unbounded_fusion_world(); + let encounter = world.encounter.clone(); + world.present(encounter.clone(), 1).expect("first seq"); world.arrive(SourceVerdict::Corroborates); let (trace, n) = wired_cycle(SelectorPolicy::CURRENT, &mut world, None); println!( @@ -535,6 +590,20 @@ fn main() { &trace[..n], world.frontier.map(|f| f.count) ); + // The same contradiction again as new evidence (seq 2): coverage is + // stale, so the interrogation reopens. Re-presenting seq 2 is refused. + let again = EncounterEvidence { + proposed_claims: world.horizon.projected_claims, + independent_roots: world.horizon.independent_roots, + ..encounter + }; + world.present(again.clone(), 2).expect("newer seq"); + let (trace, n) = wired_cycle(SelectorPolicy::CURRENT, &mut world, None); + println!( + "newer evidence on a covered bit: {:?}; re-presenting seq 2: {:?}", + &trace[..n], + world.present(again, 2) + ); } #[cfg(test)] @@ -922,6 +991,75 @@ mod tests { assert_eq!(world.interrogated, 0b011); } + /// FAILS IF: new contradiction evidence on a bit that is already covered + /// does not reopen the interrogation, or the reopened interrogation does + /// not record the newer `seq`. + #[test] + fn newer_evidence_on_a_covered_bit_reopens() { + let mut world = fusion_world(); + let first = world.encounter.clone(); + world.present(first.clone(), 1).expect("first seq"); + let (trace, n) = wired_cycle(SelectorPolicy::V1, &mut world, None); + assert_eq!(&trace[..n], &[Some(Revision), Some(MooreInterrogation)]); + assert_eq!(world.covered_seq[0], 1); + + // The same contradiction again, as new evidence. Nothing else in the + // horizon would change, so revision is not pending; only coverage is + // stale. + let again = EncounterEvidence { + proposed_claims: world.horizon.projected_claims, + independent_roots: world.horizon.independent_roots, + ..first + }; + world.present(again, 2).expect("newer seq"); + assert!(!world.revision_pending(), "the horizon would not change"); + assert!(world.local_disagreement(), "newer evidence reopens"); + let (trace, n) = wired_cycle(SelectorPolicy::V1, &mut world, None); + assert_eq!(&trace[..n], &[Some(MooreInterrogation)]); + assert_eq!(world.covered_seq[0], 2); + assert!(!world.local_disagreement()); + } + + /// FAILS IF: a reused or older `seq` is accepted, or a refusal changes the + /// world. + #[test] + fn a_stale_seq_is_refused_and_changes_nothing() { + let mut world = fusion_world(); + let e = world.encounter.clone(); + world.present(e.clone(), 5).expect("first seq"); + let before = world.clone(); + // A different encounter, so a write that slipped past the refusal + // would be visible. + let other = EncounterEvidence { + contradictions: 0b100, + resistance: 0b100, + ..echo_world().encounter + }; + assert_ne!(other, e); + assert_eq!(world.present(other.clone(), 5), Err(StaleSeq(5))); + assert_eq!(world.present(other, 4), Err(StaleSeq(4))); + assert_eq!(world, before); + } + + /// FAILS IF: evidence on a different bit reopens a covered bit, or a + /// covered bit reopens with no new evidence at all. + #[test] + fn coverage_reopens_only_where_evidence_is_newer() { + let mut world = fusion_world(); + world.horizon.unresolved_tension = 0b011; + world.interrogated = 0b011; + let e = EncounterEvidence { + contradictions: 0b010, + resistance: 0b010, + ..echo_world().encounter + }; + world.present(e, 1).expect("first seq"); + assert!(world.local_disagreement()); + world.interrogate(); + assert_eq!((world.covered_seq[0], world.covered_seq[1]), (0, 1)); + assert!(!world.local_disagreement(), "nothing newer on either bit"); + } + /// An echo world (no revision pending, no tension) with `verdicts` arrived. fn arrivals(verdicts: &[SourceVerdict]) -> World { let mut w = echo_world(); From 413b21e60db7c0571ba31359441807f7b4d7294a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 10:02:36 +0000 Subject: [PATCH 2/2] selector probe: refuse a new encounter until the accepted one is revised Addresses the Codex review on #1380: present() overwrote the single encounter slot, so a second encounter accepted before wired_cycle ran replaced the first while last_seq moved past both. A newer encounter is now refused (Refused::Unprocessed, world unchanged) while the accepted one is still revision_pending. Fixture encounters written without present stay replaceable. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MHMvKHLhM6xwRswKY4s6vY --- .../2026-10-07-selector-seq-aware-coverage.md | 10 +++- .../examples/recipe_selector_probe.rs | 59 ++++++++++++++++--- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md b/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md index 7a532963a..070248130 100644 --- a/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md +++ b/.claude/board/entries/2026-10-07-selector-seq-aware-coverage.md @@ -5,8 +5,8 @@ sequence rule (#1366). ## MEASURED -`crates/cognitive-shader-driver/examples/recipe_selector_probe.rs`, 28 -tests, 6 new disable runs red. +`crates/cognitive-shader-driver/examples/recipe_selector_probe.rs`, 29 +tests, 7 new disable runs red. - `present(encounter, seq)` refuses any `seq` not greater than the last accepted one and leaves the world unchanged. An accepted encounter stamps @@ -19,12 +19,16 @@ tests, 6 new disable runs red. (`revision_pending` stays false) and records the new `seq`. Re-presenting that `seq` is refused. - Evidence on one bit does not reopen another bit. +- One encounter slot: once an encounter is accepted, a newer one is refused + (`Refused::Unprocessed`, world unchanged) until the cycle has revised with + it (`revision_pending` false). Codex review on #1380: replacing it would + advance `last_seq` past an encounter that never revised the horizon. - An encounter written without `present` carries `seq` 0 and never reopens a covered bit, so the earlier tests are unchanged. Disable runs, each red: reuse of a `seq` allowed; the stale-coverage clause removed; coverage `seq` not recorded; evidence not stamped; `>=` instead of -`>`; the encounter written before the refusal check. The last was green until +`>`; the unprocessed-encounter refusal removed; the encounter written before the refusal check. The last was green until the refusal test presented a different encounter than the current one. ## OPEN diff --git a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs index a3742d3fd..9a8f68a36 100644 --- a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs +++ b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs @@ -74,7 +74,9 @@ //! when it was never covered, or when newer contradiction evidence arrived //! after its coverage. So new evidence on an already-covered bit reopens the //! interrogation, while presenting the same encounter again is refused -//! before it can. An encounter written without `present` carries `seq` 0 and +//! before it can. There is one encounter slot: a new encounter is also +//! refused while the current one is unprocessed (`revision_pending`), so an +//! accepted encounter is never replaced before the cycle revises with it. An encounter written without `present` carries `seq` 0 and //! never reopens a covered bit. //! //! What this does not decide: `interrogated` is a probe-local record. The @@ -310,9 +312,15 @@ struct Frontier { first: Option, } -/// An encounter `seq` that is not greater than the last accepted one. +/// Why `present` refused an encounter. Either way the world is unchanged. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -struct StaleSeq(u64); +enum Refused { + /// The `seq` is not greater than the last accepted one. + StaleSeq(u64), + /// The previous encounter has not been absorbed yet: revising with it + /// would still change the horizon. Replacing it would lose it. + Unprocessed, +} /// The set bits of a mask, lowest first. fn bits(mut m: u64) -> impl Iterator { @@ -366,10 +374,18 @@ impl World { } /// The "do": an encounter arrives with a durable `seq`. Refused, with the - /// world unchanged, unless `seq` is greater than the last accepted one. - fn present(&mut self, encounter: EncounterEvidence, seq: u64) -> Result<(), StaleSeq> { + /// world unchanged, unless `seq` is greater than the last accepted one + /// and the previously accepted encounter has been absorbed + /// (`revision_pending` is false). There is one encounter slot, so an encounter is never + /// overwritten before it is processed. + fn present(&mut self, encounter: EncounterEvidence, seq: u64) -> Result<(), Refused> { if self.last_seq.is_some_and(|last| seq <= last) { - return Err(StaleSeq(seq)); + return Err(Refused::StaleSeq(seq)); + } + // Only an accepted encounter is protected; one written into the slot + // without `present` (a fixture) carries no `seq` and may be replaced. + if self.last_seq.is_some() && self.revision_pending() { + return Err(Refused::Unprocessed); } self.last_seq = Some(seq); for b in bits(encounter.contradictions) { @@ -1036,11 +1052,38 @@ mod tests { ..echo_world().encounter }; assert_ne!(other, e); - assert_eq!(world.present(other.clone(), 5), Err(StaleSeq(5))); - assert_eq!(world.present(other, 4), Err(StaleSeq(4))); + assert_eq!(world.present(other.clone(), 5), Err(Refused::StaleSeq(5))); + assert_eq!(world.present(other, 4), Err(Refused::StaleSeq(4))); assert_eq!(world, before); } + /// FAILS IF: a second encounter replaces one that has not been revised + /// with yet, or the refusal changes the world, or the second encounter + /// stays refused after the cycle absorbs the first. + #[test] + fn an_unprocessed_encounter_is_not_replaced() { + let mut world = fusion_world(); + let first = world.encounter.clone(); + world.present(first.clone(), 1).expect("first seq"); + assert!(world.revision_pending()); + let before = world.clone(); + let second = EncounterEvidence { + contradictions: 0b100, + resistance: 0b100, + ..echo_world().encounter + }; + assert_ne!(second, first); + assert_eq!(world.present(second.clone(), 2), Err(Refused::Unprocessed)); + assert_eq!(world, before); + wired_cycle(SelectorPolicy::V1, &mut world, None); + assert!(!world.revision_pending()); + world + .present(second.clone(), 2) + .expect("first one absorbed"); + assert_eq!(world.encounter, second); + assert_eq!(world.evidence_seq[2], 2); + } + /// FAILS IF: evidence on a different bit reopens a covered bit, or a /// covered bit reopens with no new evidence at all. #[test]