From e4fb83518b0cf1f74940df0f1078b719afe3b0de Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 11:28:29 +0000 Subject: [PATCH] D-GSO-6: deterministic, versioned recipe selector (P6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SelectorPolicy::select is a pure const fn over a declared four-fact EpistemicState (observations pending, frontier bounded, local disagreement, new encounter). V1 follows the plan §11 order (fold, bound, local, revise) and rests only when nothing is open; V2 swaps two steps to show that a policy change is a new version (they differ on 2 of 16 states). A recorded Selection (policy + state) replays to the same recipe from the record alone, even when the current default changes. The version covers selection only; tying recipe implementations to it (the plan §13 semantics digest) stays open. Recipe ordinals and the V1 order are a scaffold; ProbeRecipe is not a shipped recipes ID. 5 tests (V1 table oracle on all 16 states, purity across call order, recorded replay incl. a state where V1 and V2 differ, every cycle rests in one step per open condition and replays, only the settled state rests), 5 disable runs red. Board: D-GSO-6 In PR; D-GSO-5 Shipped (#1355). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DgxrCafsJuAahpBs7oC14R --- .claude/board/STATUS_BOARD.md | 4 +- crates/cognitive-shader-driver/Cargo.toml | 5 + .../examples/README.md | 14 + .../examples/recipe_selector_probe.rs | 362 ++++++++++++++++++ 4 files changed, 383 insertions(+), 2 deletions(-) create mode 100644 crates/cognitive-shader-driver/examples/recipe_selector_probe.rs diff --git a/.claude/board/STATUS_BOARD.md b/.claude/board/STATUS_BOARD.md index 20a61b242..290573bb5 100644 --- a/.claude/board/STATUS_BOARD.md +++ b/.claude/board/STATUS_BOARD.md @@ -31,8 +31,8 @@ Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md`. O | **D-GSO-2** | P2: one-hop Moore local-plasticity probe (neighbour mask → ordinal → 8:8 relation → immediate fold) | Shipped (#1346) | `cognitive-shader-driver/examples/moore_plasticity_probe.rs` over `Register128` (4 × 4) with the existing `PalettePerturbation::hop`; 6 tests (mask = geometry, 84 visits; fold = pair-list oracle; synchronous; gate; one hop; replay), 4 disable runs red | | **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 | In PR | `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 | Queued | same replay identity ⇒ same recipe | +| **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-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/crates/cognitive-shader-driver/Cargo.toml b/crates/cognitive-shader-driver/Cargo.toml index 6ce33fff0..7ad763629 100644 --- a/crates/cognitive-shader-driver/Cargo.toml +++ b/crates/cognitive-shader-driver/Cargo.toml @@ -68,6 +68,11 @@ test = true name = "ewa_render_probe" test = true +# D-GSO-6 recipe selector probe; tests run under `cargo test`. +[[example]] +name = "recipe_selector_probe" +test = true + [dependencies] lance-graph-contract = { path = "../lance-graph-contract" } # Read-only ontology registry (Phase 7, v4 plan). default-features = false so the diff --git a/crates/cognitive-shader-driver/examples/README.md b/crates/cognitive-shader-driver/examples/README.md index 54fa8905b..b4497640b 100644 --- a/crates/cognitive-shader-driver/examples/README.md +++ b/crates/cognitive-shader-driver/examples/README.md @@ -144,3 +144,17 @@ cargo run -p cognitive-shader-driver --example ewa_render_probe `support/virtual_surfel.rs` holds the D-CTX-1 surfel reading that the rendering rounds read from, so they use exactly what D-CTX-1 tested. + +## recipe_selector_probe.rs + +D-GSO-6 (plan §18 P6). Recipe selection as a pure function of a declared +policy version and a declared epistemic state (observations pending, frontier +bounded, local disagreement, new encounter). V1 follows the §11 loop order +(fold, bound, local, revise) and rests when nothing is open; V2 swaps two steps +to show that a policy change is a new version. A recorded `Selection` +(policy + state) replays to the same recipe from the record alone. The policy +version covers selection only, not the recipes' implementations (plan §13). + +```bash +cargo run -p cognitive-shader-driver --example recipe_selector_probe +``` diff --git a/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs new file mode 100644 index 000000000..418bb9d96 --- /dev/null +++ b/crates/cognitive-shader-driver/examples/recipe_selector_probe.rs @@ -0,0 +1,362 @@ +//! D-GSO-6 (P6): deterministic, versioned recipe selector. +//! +//! Plan: `.claude/plans/2026-10-06-global-sudoku-replayable-orchestration-v1.md` +//! §11 (the loop), §13 (replay contract) and §18 P6. +//! +//! Claim under test: recipe selection is a pure function of a declared policy +//! version and a declared epistemic state. The same `(policy, state)` always +//! chooses the same recipe, a recorded selection replays under its recorded +//! policy even after the default policy changes, and a policy change is visible +//! as a different version rather than a silent change of behaviour. +//! +//! # The declared state +//! +//! Four facts an earlier step has already measured, nothing the selector +//! computes itself: +//! +//! | field | set by | meaning | +//! |---|---|---| +//! | `observations_pending` | evidence arrival | verdicts not yet folded (D-GSO-3) | +//! | `frontier_bounded` | a product interrogation | the candidate frontier is known (D-GSO-5 R1) | +//! | `local_disagreement` | a local fold | neighbourhood tension is present (D-GSO-2/3) | +//! | `new_encounter` | evidence arrival | an encounter awaits revision (D-GSO-5 R3) | +//! +//! Policy V1 follows the §11 order DETECT → BOUND → PROPOSE/TEST → REVISE: +//! fold pending observations first, then bound the frontier, then interrogate +//! local disagreement, then revise. With nothing left it returns no recipe: +//! the cycle rests (a hole is never forced into a guess, §11). +//! +//! # What this probe does not decide +//! +//! - **The recipe ordinals and the V1 order are a scaffold, not canon.** They +//! reuse the P5 scaffold names; `ProbeRecipe` is again its own type and never +//! a shipped `recipes::Recipe` ID (plan §12). +//! - **The policy version covers selection only.** Plan §13 requires the +//! recipe-policy version to cover every executable operation a recipe can +//! select; tying the recipes' implementations to the version (a semantics +//! 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. +//! +//! Run: `cargo run -p cognitive-shader-driver --example recipe_selector_probe` +//! Tests: `cargo test -p cognitive-shader-driver --example recipe_selector_probe` + +/// A probe recipe ordinal, as in the P5 scaffold. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum ProbeRecipe { + ObserveFold, + ProductInterrogation, + MooreInterrogation, + Revision, +} + +/// The declared epistemic state the selector reads. Every field is supplied; +/// the selector derives nothing. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct EpistemicState { + observations_pending: bool, + frontier_bounded: bool, + local_disagreement: bool, + new_encounter: bool, +} + +impl EpistemicState { + /// All 16 states, in a fixed order. + fn all() -> impl Iterator { + (0..16u8).map(|b| Self { + observations_pending: b & 1 != 0, + frontier_bounded: b & 2 != 0, + local_disagreement: b & 4 != 0, + new_encounter: b & 8 != 0, + }) + } +} + +/// A selection policy. The version is part of every selection it makes. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +enum SelectorPolicy { + /// §11 order: fold, bound, local, revise. + V1, + /// A later policy that interrogates local disagreement before bounding + /// the frontier. It exists to show that a policy change is a new version. + V2, +} + +impl SelectorPolicy { + /// The policy a new cycle uses. Recorded selections never consult this. + const CURRENT: Self = Self::V1; + + /// Pure selection: no clock, no counter, no global, no randomness. `None` + /// means nothing is left to do and the cycle rests. + const fn select(self, s: EpistemicState) -> Option { + if s.observations_pending { + return Some(ProbeRecipe::ObserveFold); + } + match self { + Self::V1 => { + if !s.frontier_bounded { + return Some(ProbeRecipe::ProductInterrogation); + } + if s.local_disagreement { + return Some(ProbeRecipe::MooreInterrogation); + } + } + Self::V2 => { + if s.local_disagreement { + return Some(ProbeRecipe::MooreInterrogation); + } + if !s.frontier_bounded { + return Some(ProbeRecipe::ProductInterrogation); + } + } + } + if s.new_encounter { + return Some(ProbeRecipe::Revision); + } + None + } +} + +/// What a replay needs to choose again: the policy version and the state. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +struct Selection { + policy: SelectorPolicy, + state: EpistemicState, + recipe: Option, +} + +impl Selection { + /// Select under `policy` and record what was used. + const fn make(policy: SelectorPolicy, state: EpistemicState) -> Self { + Self { + policy, + state, + recipe: policy.select(state), + } + } + + /// Choose again from the record alone. + const fn replay(self) -> Option { + self.policy.select(self.state) + } +} + +/// Stand-in transition: the recipe clears the condition it answers. +const fn after(recipe: ProbeRecipe, s: EpistemicState) -> EpistemicState { + match recipe { + ProbeRecipe::ObserveFold => EpistemicState { + observations_pending: false, + ..s + }, + ProbeRecipe::ProductInterrogation => EpistemicState { + frontier_bounded: true, + ..s + }, + ProbeRecipe::MooreInterrogation => EpistemicState { + local_disagreement: false, + ..s + }, + ProbeRecipe::Revision => EpistemicState { + new_encounter: false, + ..s + }, + } +} + +/// The most steps any cycle can take: one per condition. +const MAX_STEPS: usize = 4; + +/// Run a cycle from `start` under `policy` until it rests. Returns the +/// recipes chosen, in order, and how many there were. Fixed-size, no heap. +fn cycle( + policy: SelectorPolicy, + start: EpistemicState, +) -> ([Option; MAX_STEPS + 1], usize) { + let mut trace = [None; MAX_STEPS + 1]; + let mut state = start; + for (step, slot) in trace.iter_mut().enumerate() { + match policy.select(state) { + Some(recipe) => { + *slot = Some(recipe); + state = after(recipe, state); + } + None => return (trace, step), + } + } + (trace, MAX_STEPS + 1) +} + +fn main() { + let start = EpistemicState { + observations_pending: true, + frontier_bounded: false, + local_disagreement: true, + new_encounter: true, + }; + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + let (trace, n) = cycle(policy, start); + println!("{policy:?}: {:?}", &trace[..n]); + } + let mut replayed = 0; + for state in EpistemicState::all() { + let recorded = Selection::make(SelectorPolicy::CURRENT, state); + assert_eq!(recorded.replay(), recorded.recipe); + replayed += 1; + } + println!( + "{replayed} selections recorded under {:?} and replayed", + SelectorPolicy::CURRENT + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use ProbeRecipe::*; + + fn state(o: bool, f: bool, l: bool, n: bool) -> EpistemicState { + EpistemicState { + observations_pending: o, + frontier_bounded: f, + local_disagreement: l, + new_encounter: n, + } + } + + /// FAILS IF: V1 departs from the §11 order on any of the 16 states. The + /// oracle is written out as a table, not derived from `select`. + #[test] + fn v1_follows_the_loop_order_on_every_state() { + let mut checked = 0; + for s in EpistemicState::all() { + let expected = if s.observations_pending { + Some(ObserveFold) + } else if !s.frontier_bounded { + Some(ProductInterrogation) + } else if s.local_disagreement { + Some(MooreInterrogation) + } else if s.new_encounter { + Some(Revision) + } else { + None + }; + assert_eq!(SelectorPolicy::V1.select(s), expected, "{s:?}"); + checked += 1; + } + assert_eq!(checked, 16); + // Spot checks that pin the order directly. + assert_eq!( + SelectorPolicy::V1.select(state(false, false, true, true)), + Some(ProductInterrogation) + ); + assert_eq!( + SelectorPolicy::V1.select(state(false, true, false, false)), + None + ); + } + + /// FAILS IF: the same (policy, state) ever chooses differently, whether + /// asked twice in a row or in a different order. + #[test] + fn selection_is_a_function_of_policy_and_state() { + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + let forward: Vec<_> = EpistemicState::all().map(|s| policy.select(s)).collect(); + let again: Vec<_> = EpistemicState::all().map(|s| policy.select(s)).collect(); + let mut backward: Vec<_> = EpistemicState::all() + .collect::>() + .into_iter() + .rev() + .map(|s| policy.select(s)) + .collect(); + backward.reverse(); + assert_eq!(forward, again, "{policy:?}"); + assert_eq!(forward, backward, "{policy:?}"); + } + } + + /// FAILS IF: a recorded selection does not replay to the same recipe from + /// the record alone, or a policy change is not visible as a version. + #[test] + fn a_recorded_selection_replays_under_its_own_policy() { + // V1 and V2 differ on some states, so the version carries meaning. + let differing: Vec<_> = EpistemicState::all() + .filter(|&s| SelectorPolicy::V1.select(s) != SelectorPolicy::V2.select(s)) + .collect(); + assert_eq!( + differing.len(), + 2, + "pinned: the two states with local tension and no frontier" + ); + let s = differing[0]; + + // A selection recorded under V1 replays to V1's answer even though V2 + // would answer differently today. + let recorded = Selection::make(SelectorPolicy::V1, s); + assert_eq!(recorded.replay(), recorded.recipe); + assert_ne!(recorded.replay(), SelectorPolicy::V2.select(s)); + + // Every record replays, under both policies, on every state. + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + for s in EpistemicState::all() { + let r = Selection::make(policy, s); + assert_eq!(r.replay(), r.recipe); + } + } + assert_eq!(SelectorPolicy::CURRENT, SelectorPolicy::V1); + } + + /// FAILS IF: a cycle does not rest, takes more than one step per + /// condition, or replays to a different path. + #[test] + fn every_cycle_rests_and_replays() { + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + for s in EpistemicState::all() { + let (trace, n) = cycle(policy, s); + assert!(n <= MAX_STEPS, "{policy:?} {s:?} did not rest"); + let conditions = usize::from(s.observations_pending) + + usize::from(!s.frontier_bounded) + + usize::from(s.local_disagreement) + + usize::from(s.new_encounter); + assert_eq!(n, conditions, "{policy:?} {s:?}"); + assert_eq!(cycle(policy, s), (trace, n), "{policy:?} {s:?}"); + } + } + // The full cycle, pinned per policy. + let full = state(true, false, true, true); + assert_eq!( + &cycle(SelectorPolicy::V1, full).0[..4], + &[ + Some(ObserveFold), + Some(ProductInterrogation), + Some(MooreInterrogation), + Some(Revision) + ] + ); + assert_eq!( + &cycle(SelectorPolicy::V2, full).0[..4], + &[ + Some(ObserveFold), + Some(MooreInterrogation), + Some(ProductInterrogation), + Some(Revision) + ] + ); + } + + /// FAILS IF: the selector fires on a settled state or stays silent on an + /// open one. Only the fully settled state rests. + #[test] + fn only_a_settled_state_rests() { + for policy in [SelectorPolicy::V1, SelectorPolicy::V2] { + let resting: Vec<_> = EpistemicState::all() + .filter(|&s| policy.select(s).is_none()) + .collect(); + assert_eq!( + resting, + vec![state(false, true, false, false)], + "{policy:?}" + ); + } + } +}