From ec02247201b1c79989a28d631dd64c96187af73f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:39:45 +0000 Subject: [PATCH] D-GSO-6 follow-up: derive the selector's new_encounter from revision output new_encounter is read from the world (encounter roots minus horizon roots), and the Revision recipe writes GadamerRevision's delta.resulting. The cycle rests because revision absorbed the roots; dropping the write never rests. Pins that unresolved_tension cannot drive local_disagreement. The other three facts remain stand-ins. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01MHMvKHLhM6xwRswKY4s6vY --- .claude/board/STATUS_BOARD.md | 2 +- .../2026-10-06-selector-revision-wired.md | 39 +++ .claude/board/entries/README.md | 3 +- .../examples/recipe_selector_probe.rs | 277 +++++++++++++++++- 4 files changed, 318 insertions(+), 3 deletions(-) create mode 100644 .claude/board/entries/2026-10-06-selector-revision-wired.md diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 290573bb5..0341d221d 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 | 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 | +| **D-GSO-6** | P6: deterministic, versioned recipe selector | Shipped (#1358); `new_encounter` wired to revision output: 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 (encounter roots \ horizon roots) 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; 12 tests, 5 disable runs red | | **D-GSO-7** | P7: reasoning-band earning and downgrade | Queued | association cannot skip to causal permission; contradicting evidence lowers it; replayable | | **D-GSO-8** | P8: seal boundary | Queued | replay from the prior seal reproduces the next seal | diff --git a/.claude/board/entries/2026-10-06-selector-revision-wired.md b/.claude/board/entries/2026-10-06-selector-revision-wired.md new file mode 100644 index 000000000..122e8379b --- /dev/null +++ b/.claude/board/entries/2026-10-06-selector-revision-wired.md @@ -0,0 +1,39 @@ +# 2026-10-06 — D-GSO-6 follow-up: the selector's revision fact is wired + +## MEASURED + +`crates/cognitive-shader-driver/examples/recipe_selector_probe.rs`, now 12 +tests (7 new), 5 disable runs red. Builds on D-GSO-6 (#1358) and the Round 6 +cycle (#1359). + +- `new_encounter` is no longer supplied. It is read from the world: the + encounter's independent roots minus the horizon's. That equals + `GadamerRevision`'s `new_independent_roots` with the horizon as ancestry. +- The `Revision` recipe runs `GadamerRevision::revise`; only + `delta.resulting` reaches the next state. +- After one real revision the cycle rests (roots `0b101 → 0b111`). With the + write dropped it selects `Revision` every step and never rests. +- An echo (no new root) selects nothing. A held claim gaining its first + root (`IndependentConfirmation`) selects `Revision` once. +- Every (policy, stand-in state) start replays to the same path and the + same final horizon. + +Disable runs, each red: derived fact forced true; derived fact ignored in +favour of the supplied flag; revise result discarded; roots compared +against projected claims instead of roots; the bypass flag ignored. The +roots-vs-claims disable was green until the held-claim case was added. + +## FINDING + +- **The selector's rest after revision now comes from revision's output.** + It is no longer a stand-in. +- **`local_disagreement` cannot be read off the horizon.** + `unresolved_tension` survives every revision, so a selector reading it + would interrogate forever. + +## OPEN + +- `observations_pending`, `frontier_bounded` and `local_disagreement` are + still stand-ins. The last needs a source that clears: tension new since + the last interrogation, not tension held. +- The policy version still covers selection only (§13 digest). diff --git a/.claude/board/entries/README.md b/.claude/board/entries/README.md index 43f7a28cf..717cc7cad 100644 --- a/.claude/board/entries/README.md +++ b/.claude/board/entries/README.md @@ -25,12 +25,13 @@ 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. -226 entries, 2026-08-06 .. 2026-10-06. +227 entries, 2026-08-06 .. 2026-10-06. | date | entry id | finding | file | |---|---|---|---| | 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) | | 2026-10-06 | `spofc-do-learn-as-compile-round6` | | [2026-10-06-spofc-do-learn-as-compile-round6.md](2026-10-06-spofc-do-learn-as-compile-round6.md) | +| 2026-10-06 | `D-GSO-6` | | [2026-10-06-selector-revision-wired.md](2026-10-06-selector-revision-wired.md) | | 2026-10-06 | `quack-r2il-one-evaluator-round4` | | [2026-10-06-quack-r2il-one-evaluator-round4.md](2026-10-06-quack-r2il-one-evaluator-round4.md) | | 2026-10-06 | `D-MORTON-0` | | [2026-10-06-morton8x8-checked-address.md](2026-10-06-morton8x8-checked-address.md) | | 2026-10-06 | `diff-plateau-as-expression-round5` | | [2026-10-06-diff-plateau-as-expression-round5.md](2026-10-06-diff-plateau-as-expression-round5.md) | diff --git a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs index 418bb9d96..4bfd8c16f 100644 --- a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs +++ b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs @@ -37,7 +37,25 @@ //! digest) is not done here. //! - **The state transitions in the replay test are a stand-in.** Each step //! clears the condition its recipe answers; real recipes would produce that -//! change from their outputs. +//! change from their outputs. `new_encounter` is no longer one: see +//! "Revision, wired" below. The other three still are. +//! +//! # Revision, wired +//! +//! `new_encounter` is read from the world, not supplied: it holds while the +//! pending encounter carries an independent root the horizon does not yet +//! have. That is `GadamerRevision`'s own `has_new_root` with the horizon as +//! ancestry, one mask difference (one fused `Any` fold, Round 6). The +//! `Revision` recipe runs `GadamerRevision::revise` and its only output that +//! reaches the next state is `delta.resulting`. So the cycle rests after +//! revision because revision absorbed the roots, not because a stand-in +//! cleared a flag. Bypass the write and the selector picks `Revision` again, +//! forever. +//! +//! `local_disagreement` cannot be read off the horizon the same way: +//! `unresolved_tension` is preserved by revision and never cleared, so a +//! selector reading it would interrogate forever. Pinned as a test; the +//! source for that fact stays open. //! //! Run: `cargo run -p cognitive-shader-driver --example recipe_selector_probe` //! Tests: `cargo test -p cognitive-shader-driver --example recipe_selector_probe` @@ -187,6 +205,123 @@ fn cycle( (trace, MAX_STEPS + 1) } +// ── revision, wired ───────────────────────────────────────────────────── + +use lance_graph_contract::revision::{ + BasisView, CodebookId, EncounterEvidence, EvidenceMask, GadamerRevision, GrammarId, HorizonId, + InterpretiveHorizon, LanguageId, LensId, QuestionId, RevisionPolicy, +}; + +type Horizon = InterpretiveHorizon<(), u64>; + +/// The part of the world the wired fact reads: the current horizon and the +/// encounter waiting at it. The encounter is supplied by observation; the +/// selector never creates one. +#[derive(Debug, Clone, PartialEq, Eq)] +struct World { + horizon: Horizon, + encounter: EncounterEvidence, +} + +/// The horizon as ancestry, as in the Round 6 cycle. +fn ancestry(h: &Horizon) -> BasisView { + BasisView { + ancestry_independent_roots: h.independent_roots, + ancestry_derived_roots: h.inherited_roots, + ancestor_claims: h.projected_claims, + closes_cycle: false, + } +} + +impl World { + /// `new_encounter`, derived: the encounter carries a root the horizon + /// does not have. Equal to `revise(...).new_independent_roots != 0`. + fn new_encounter(&self) -> bool { + !self + .encounter + .independent_roots + .difference(&self.horizon.independent_roots) + .is_empty() + } + + /// The declared state: three stand-in facts plus the derived one. + fn state(&self, rest: EpistemicState) -> EpistemicState { + EpistemicState { + new_encounter: self.new_encounter(), + ..rest + } + } + + /// The `Revision` recipe: revise once; `delta.resulting` is the only + /// output carried forward. + fn revise(&mut self) { + let delta = + GadamerRevision.revise(&self.horizon, &self.encounter, &ancestry(&self.horizon)); + self.horizon = delta.resulting; + } +} + +/// Run a cycle whose `new_encounter` is derived from `world` at every step. +/// The other three facts use the stand-in transition. `write` is false only +/// in the bypass falsifier: the revision runs but its result is dropped. +fn wired_cycle( + policy: SelectorPolicy, + rest: EpistemicState, + world: &mut World, + write: bool, +) -> ([Option; MAX_STEPS + 1], usize) { + let mut trace = [None; MAX_STEPS + 1]; + let mut rest = rest; + for (step, slot) in trace.iter_mut().enumerate() { + match policy.select(world.state(rest)) { + Some(ProbeRecipe::Revision) => { + *slot = Some(ProbeRecipe::Revision); + if write { + world.revise(); + } else { + // Revise a copy and drop it: the work runs, nothing lands. + world.clone().revise(); + } + } + Some(recipe) => { + *slot = Some(recipe); + rest = after(recipe, rest); + } + None => return (trace, step), + } + } + (trace, MAX_STEPS + 1) +} + +/// A small world: the horizon holds claims {0, 2}; the encounter proposes +/// {1, 2}, rooted in {1, 2}, and contradicts claim 0. +fn fusion_world() -> World { + World { + horizon: InterpretiveHorizon { + id: HorizonId(0), + awareness: (), + question: QuestionId(6), + language: LanguageId(0), + grammar: GrammarId(0), + codebook: CodebookId(0), + lens: LensId(0), + projected_claims: 0b101, + independent_roots: 0b101, + inherited_roots: 0, + unresolved_tension: 0, + revision_index: 0, + }, + encounter: EncounterEvidence { + proposed_claims: 0b110, + independent_roots: 0b110, + inherited_roots: 0, + resistance: 0b001, + contradictions: 0b001, + affected_parts: 0b111, + }, + } +} + fn main() { let start = EpistemicState { observations_pending: true, @@ -208,11 +343,19 @@ fn main() { "{replayed} selections recorded under {:?} and replayed", SelectorPolicy::CURRENT ); + let mut world = fusion_world(); + let (trace, n) = wired_cycle(SelectorPolicy::CURRENT, start, &mut world, true); + println!( + "wired: {:?}, horizon roots {:#b}", + &trace[..n], + world.horizon.independent_roots + ); } #[cfg(test)] mod tests { use super::*; + use lance_graph_contract::revision::RevisionKind; use ProbeRecipe::*; fn state(o: bool, f: bool, l: bool, n: bool) -> EpistemicState { @@ -344,6 +487,138 @@ mod tests { ); } + /// The settled stand-in facts, so only the wired fact is open. + fn settled() -> EpistemicState { + state(false, true, false, false) + } + + /// FAILS IF: the derived fact disagrees with revision's own new-root test. + #[test] + fn the_derived_fact_is_revisions_new_root_test() { + let w = fusion_world(); + let delta = GadamerRevision.revise(&w.horizon, &w.encounter, &ancestry(&w.horizon)); + assert!(w.new_encounter()); + assert_ne!(delta.new_independent_roots, 0); + let after = World { + horizon: delta.resulting, + ..w + }; + assert!( + !after.new_encounter(), + "the resulting horizon has the roots" + ); + } + + /// FAILS IF: the cycle does not rest after one real revision, or rests + /// without one. The rest comes from `delta.resulting`, not a stand-in. + #[test] + fn a_real_revision_makes_the_cycle_rest() { + let mut world = fusion_world(); + let before = world.horizon.clone(); + let (trace, n) = wired_cycle(SelectorPolicy::V1, settled(), &mut world, true); + assert_eq!(&trace[..n], &[Some(Revision)]); + assert_eq!(world.horizon.independent_roots, 0b111); + assert_eq!(world.horizon.revision_index, before.revision_index + 1); + + // The full cycle under V1, with the wired fact last. + let mut world = fusion_world(); + let (trace, n) = wired_cycle( + SelectorPolicy::V1, + state(true, false, true, false), + &mut world, + true, + ); + assert_eq!( + &trace[..n], + &[ + Some(ObserveFold), + Some(ProductInterrogation), + Some(MooreInterrogation), + Some(Revision) + ] + ); + } + + /// FAILS IF: dropping revision's write still lets the cycle rest, i.e. + /// the rest does not depend on revision's output. + #[test] + fn dropping_the_revision_write_never_rests() { + let mut world = fusion_world(); + let (trace, n) = wired_cycle(SelectorPolicy::V1, settled(), &mut world, false); + assert_eq!(n, MAX_STEPS + 1, "did not rest"); + assert!(trace.iter().all(|r| *r == Some(Revision))); + assert_eq!(world, fusion_world(), "nothing was written"); + } + + /// FAILS IF: the derived fact reads claims instead of roots. Claim 1 is + /// already projected but has no independent root; the encounter brings + /// one. Revision calls that an independent confirmation, so the selector + /// must pick it. + #[test] + fn a_new_root_for_a_held_claim_selects_revision() { + let mut world = fusion_world(); + world.horizon.projected_claims = 0b011; + world.horizon.independent_roots = 0b001; + world.encounter = EncounterEvidence { + proposed_claims: 0b011, + independent_roots: 0b010, + inherited_roots: 0, + resistance: 0, + contradictions: 0, + affected_parts: 0b011, + }; + let delta = + GadamerRevision.revise(&world.horizon, &world.encounter, &ancestry(&world.horizon)); + assert_eq!(delta.kind, RevisionKind::IndependentConfirmation); + assert!(world.new_encounter()); + let (trace, n) = wired_cycle(SelectorPolicy::V1, settled(), &mut world, true); + assert_eq!(&trace[..n], &[Some(Revision)]); + assert_eq!(world.horizon.independent_roots, 0b011); + } + + /// FAILS IF: an echo (every root already held) still selects revision. + #[test] + fn an_echo_selects_nothing() { + let mut world = fusion_world(); + world.encounter.independent_roots = 0b001; + assert!(!world.new_encounter()); + let (_, n) = wired_cycle(SelectorPolicy::V1, settled(), &mut world, true); + assert_eq!(n, 0); + assert_eq!(world, { + let mut w = fusion_world(); + w.encounter.independent_roots = 0b001; + w + }); + } + + /// FAILS IF: the same starting world replays to a different path or a + /// different final horizon. + #[test] + fn the_wired_cycle_replays() { + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + for rest in EpistemicState::all() { + let (mut a, mut b) = (fusion_world(), fusion_world()); + let ra = wired_cycle(policy, rest, &mut a, true); + let rb = wired_cycle(policy, rest, &mut b, true); + assert_eq!(ra, rb, "{policy:?} {rest:?}"); + assert_eq!(a, b, "{policy:?} {rest:?}"); + assert!(ra.1 <= MAX_STEPS, "{policy:?} {rest:?} did not rest"); + } + } + } + + /// FAILS IF: revision starts clearing tension. Then `local_disagreement` + /// could be read off the horizon; while it holds, it cannot, because the + /// tension survives every revision and a reader of it would never rest. + #[test] + fn preserved_tension_cannot_drive_the_selector() { + let mut world = fusion_world(); + world.revise(); + assert_eq!(world.horizon.unresolved_tension, 0b001); + world.revise(); + assert_eq!(world.horizon.unresolved_tension, 0b001, "still held"); + } + /// FAILS IF: the selector fires on a settled state or stays silent on an /// open one. Only the fully settled state rests. #[test]