From 66e8ed852f707b69eac08ca28915409a8b60a013 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 07:08:00 +0000 Subject: [PATCH 1/6] contract: land revision.rs -- the court of appeal #1057 already depends on PR #1057 (merged) names revision as "the only write-back" and "the court of appeal" at the ACCEPT step of its DETECT->BOUND->PROPOSE->FILTER->GATE->TEST loop (D-ECG-3, D-ECG-6). counterfactual.rs shipped twice; revision.rs never landed, and zero of its symbols existed anywhere in crates/. A merged plan's ACCEPT step -- the one place a hypothesis becomes a fact -- was governed by a module that did not exist. This closes that. Zero-dep (std::array + std::fmt only), placed beside counterfactual.rs / temporal_pov.rs / settlement.rs. No production write capability: the output types stop before actual-world mutation by design. ONE DEFECT FIXED FROM THE DRAFT. The evidential_effect match guarded three kinds with `if has_new_root`, then listed the same three variants again falling through to NoIncrease. But IndependentConfirmation, HorizonExpansion and HorizonFusion are EACH derived above under a has_new_root precondition, so the guard can never be false and the second listing is unreachable -- a guard that cannot fail is the vacuous-guard defect the falsifiability rule names, and the dead arms read as policy for a state that cannot occur. Replaced with an exhaustive unguarded match plus a debug_assert! documenting the coupling, so relaxing a kind condition trips in debug rather than silently minting evidential weight for a rootless synthesis. THE HOLY-GRAIL ASYMMETRY, now documented on RevisionKind. HorizonFusion is Horizontverschmelzung as thesis x antithesis x synthesis: thesis = prior.projected_claims, antithesis = encounter.contradictions, synthesis = introduced union preserved. It is Gadamer, not Hegel -- unresolved_tension accumulates by union and is never cleared, so a fusion carries its contradiction forward as durable structure; a synthesis that RESOLVED its antithesis would be the false synthesis the policy exists to refuse. ContradictionPreserved and HorizonFusion see the SAME contradiction; the only thing separating them is has_new_root. Without it: Suspend, tension held, no weight minted. That asymmetry is the anti-laundering invariant in executable form -- no amount of re-reading inherited material can produce a synthesis, because re-reading yields no new independent root. Three falsifiers added, all disable-verified. Removing has_new_root from HorizonFusion's condition fails exactly two of them, at the new debug_assert (line 328), while the other seven stay green -- surgical: - contradiction_without_a_new_root_suspends_instead_of_synthesising: the identical encounter as the fusion test EXCEPT the root, must land ContradictionPreserved + Suspend. Two-sided against its twin; neither half can pass for the other's reason. Anti-vacuity asserts the discriminating quantity really is absent. - fusion_accumulates_tension_and_never_clears_prior_tension: Gadamer not Hegel, with tension carried in from before. - no_increase_eligible_outcome_is_reachable_without_a_new_root: sweeps the rootless encounter space across claims x resistance x contradiction. Gates: 9/9 revision tests, 1236 contract lib tests, clippy -D warnings clean, fmt clean. (cherry picked from commit 5eb74b1e732a48a69cb129e2ad3b9aa3a010fba9) --- crates/lance-graph-contract/src/lib.rs | 1 + crates/lance-graph-contract/src/revision.rs | 600 ++++++++++++++++++++ 2 files changed, 601 insertions(+) create mode 100644 crates/lance-graph-contract/src/revision.rs diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 11a808456..1ac5be813 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -156,6 +156,7 @@ pub mod recipes; /// class-agnostic. pub mod recoder_adapter; pub mod repository; +pub mod revision; /// D-ACR-8 — reading the Heckhausen crossing from the focus of attention. pub mod rubicon_witness; pub mod savants; diff --git a/crates/lance-graph-contract/src/revision.rs b/crates/lance-graph-contract/src/revision.rs new file mode 100644 index 000000000..350a459b1 --- /dev/null +++ b/crates/lance-graph-contract/src/revision.rs @@ -0,0 +1,600 @@ +//! Revision policy: explicit horizon change under textual / grammatical resistance. +//! +//! # Status +//! +//! LANDED from the session draft (2026-08-29). This module models revision, not +//! truth persistence, and still has **no production write capability** — the +//! output types stop before actual-world mutation by design. +//! +//! `entropy-closure-causal-ground-v1` (PR #1057, merged) names revision as +//! *"the only write-back"* and *"the court of appeal"* in its +//! DETECT→BOUND→PROPOSE→FILTER→GATE→TEST→ACCEPT loop, while `counterfactual.rs` +//! shipped and this module did not. That gap is what this file closes. The intended placement is beside `temporal.rs` and +//! `counterfactual.rs`: +//! +//! - temporal remembers the awareness horizon and durable arrival; +//! - counterfactual explores sealed hypothetical timelines; +//! - revision records whether an encounter changed the interpretive horizon or +//! merely returned inherited assumptions. +//! +//! `GadamerRevision` is a textual-hermeneutic policy, not a universal truth +//! algorithm. Echoes and closed cycles remain observable history but receive zero +//! additional evidential weight. + +use std::array; +use std::fmt; + +/// Minimal operations required from fixed-width masks. +/// +/// The canonical mask type can implement this trait at integration time. The draft +/// includes implementations for `u64` and fixed arrays of `u64` words. +pub trait EvidenceMask: Clone + PartialEq + Eq + fmt::Debug { + fn empty() -> Self; + fn is_empty(&self) -> bool; + fn union(&self, other: &Self) -> Self; + fn intersection(&self, other: &Self) -> Self; + fn difference(&self, other: &Self) -> Self; + fn is_subset_of(&self, other: &Self) -> bool; + + fn intersects(&self, other: &Self) -> bool { + !self.intersection(other).is_empty() + } +} + +impl EvidenceMask for u64 { + fn empty() -> Self { + 0 + } + + fn is_empty(&self) -> bool { + *self == 0 + } + + fn union(&self, other: &Self) -> Self { + *self | *other + } + + fn intersection(&self, other: &Self) -> Self { + *self & *other + } + + fn difference(&self, other: &Self) -> Self { + *self & !*other + } + + fn is_subset_of(&self, other: &Self) -> bool { + (*self & !*other) == 0 + } +} + +impl EvidenceMask for [u64; N] { + fn empty() -> Self { + [0; N] + } + + fn is_empty(&self) -> bool { + self.iter().all(|word| *word == 0) + } + + fn union(&self, other: &Self) -> Self { + array::from_fn(|idx| self[idx] | other[idx]) + } + + fn intersection(&self, other: &Self) -> Self { + array::from_fn(|idx| self[idx] & other[idx]) + } + + fn difference(&self, other: &Self) -> Self { + array::from_fn(|idx| self[idx] & !other[idx]) + } + + fn is_subset_of(&self, other: &Self) -> bool { + self.iter() + .zip(other.iter()) + .all(|(left, right)| (*left & !*right) == 0) + } +} + +/// Opaque identifiers. Replace with canonical planner / OGAR IDs when wired. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HorizonId(pub u64); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct QuestionId(pub u64); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LanguageId(pub u16); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GrammarId(pub u32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct CodebookId(pub u32); +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct LensId(pub u32); + +/// The reader's explicit interpretive horizon before or after an encounter. +/// +/// `A` is intended to become the canonical `temporal::AwarenessRef`. Keeping it +/// generic prevents `revision.rs` from creating its own clock or temporal store. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InterpretiveHorizon { + pub id: HorizonId, + pub awareness: A, + pub question: QuestionId, + pub language: LanguageId, + pub grammar: GrammarId, + pub codebook: CodebookId, + pub lens: LensId, + /// Claims currently projected as the working whole. + pub projected_claims: M, + /// Independent roots accumulated across genuine encounters. + pub independent_roots: M, + /// Earlier interpretations consumed as interpretations, never as fresh evidence. + pub inherited_roots: M, + /// Tension deliberately preserved instead of forced into a false synthesis. + pub unresolved_tension: M, + pub revision_index: u16, +} + +/// What the latest textual / grammatical / philosophical encounter produced. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct EncounterEvidence { + /// Proposed working whole after reading this encounter. + pub proposed_claims: M, + /// Actual source / grammar / observation roots contacted independently here. + pub independent_roots: M, + /// Derived thoughts, summaries, or inherited interpretations consumed here. + pub inherited_roots: M, + /// Parts that resisted the prior projection. + pub resistance: M, + /// Contradictions that remain live after the encounter. + pub contradictions: M, + /// Parts whose reading changed because the projected whole changed. + pub affected_parts: M, +} + +/// Bounded ancestry summary supplied by temporal / counterfactual provenance. +/// +/// No generic graph walk is required inside this module. The caller supplies the +/// fixed-width ancestry projection appropriate for the current SoA trajectory. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BasisView { + pub ancestry_independent_roots: M, + pub ancestry_derived_roots: M, + pub ancestor_claims: M, + /// Set by the bounded provenance projection when the candidate depends on itself. + pub closes_cycle: bool, +} + +/// What an encounter did to the interpretive horizon. +/// +/// # `HorizonFusion` — *Horizontverschmelzung* as thesis × antithesis × synthesis +/// +/// This variant is the one the whole policy exists to make earnable rather +/// than assumable. Its three derivation conditions ARE the dialectical triad, +/// and none of them is decorative: +/// +/// ```text +/// thesis prior.projected_claims the working whole brought in +/// antithesis encounter.contradictions what refused to be absorbed +/// synthesis introduced ∪ preserved what stands after the collision +/// ``` +/// +/// **It is Gadamer, not Hegel: the tension is PRESERVED, never dissolved.** +/// `unresolved_tension` accumulates by union and is never cleared, so a fusion +/// carries its contradiction forward as durable structure. A synthesis that +/// *resolved* its antithesis would be exactly the false synthesis this module +/// is built to refuse. +/// +/// **Synthesis must be EARNED.** [`RevisionKind::ContradictionPreserved`] and +/// `HorizonFusion` see the same contradiction; the single thing separating +/// them is `has_new_root` — a genuinely new independent root, not present in +/// ancestry. Without it the outcome is `ContradictionPreserved` → +/// [`EvidentialEffect::Suspend`]: the tension is held open, and no evidential +/// weight is minted. With it, `HorizonFusion` → +/// [`EvidentialEffect::IncreaseEligible`]. +/// +/// That asymmetry is the anti-laundering invariant in executable form: no +/// amount of re-reading inherited material can produce a synthesis, because +/// re-reading yields no new independent root. Fusion of two horizons requires +/// that at least one of them actually touched the world. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum RevisionKind { + IndependentConfirmation, + Reinterpretation, + HorizonExpansion, + HorizonFusion, + AssumptionExposed, + ContradictionPreserved, + Suspended, + Echo, + ClosedCycle, +} + +/// Evidential consequence is deliberately coarser than numerical confidence. +/// Downstream belief machinery may translate it, but echo/cycle can never inflate it. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum EvidentialEffect { + /// At least one genuinely new independent root was introduced. + IncreaseEligible, + /// Semantic movement occurred, but no new independent support was gained. + NoIncrease, + /// The interpretation should remain unresolved pending grounding. + Suspend, +} + +/// Explicit delta between the prior and resulting horizons. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RevisionDelta { + pub kind: RevisionKind, + pub evidential_effect: EvidentialEffect, + pub prior: InterpretiveHorizon, + pub resulting: InterpretiveHorizon, + pub preserved_claims: M, + pub introduced_claims: M, + pub withdrawn_claims: M, + pub revised_claims: M, + pub new_independent_roots: M, + pub inherited_roots: M, + pub resistance: M, + pub contradictions: M, + pub affected_parts: M, +} + +/// Textual revision policy seam. +pub trait RevisionPolicy { + fn revise( + &self, + prior: &InterpretiveHorizon, + encounter: &EncounterEvidence, + ancestry: &BasisView, + ) -> RevisionDelta + where + A: Clone; +} + +/// Gadamer-shaped revision policy. +/// +/// The policy makes the prior projection explicit, requires resistance or new +/// grounding for productive movement, preserves unresolved contradiction, and +/// prevents inherited derivations from counting as independent confirmation. +#[derive(Debug, Clone, Copy, Default)] +pub struct GadamerRevision; + +impl RevisionPolicy for GadamerRevision { + fn revise( + &self, + prior: &InterpretiveHorizon, + encounter: &EncounterEvidence, + ancestry: &BasisView, + ) -> RevisionDelta + where + A: Clone, + { + let new_independent_roots = encounter + .independent_roots + .difference(&ancestry.ancestry_independent_roots); + let preserved_claims = prior + .projected_claims + .intersection(&encounter.proposed_claims); + let introduced_claims = encounter + .proposed_claims + .difference(&prior.projected_claims); + let withdrawn_claims = prior + .projected_claims + .difference(&encounter.proposed_claims); + let revised_claims = introduced_claims.union(&withdrawn_claims); + + let has_new_root = !new_independent_roots.is_empty(); + let has_resistance = !encounter.resistance.is_empty(); + let has_contradiction = !encounter.contradictions.is_empty(); + let same_projection = introduced_claims.is_empty() && withdrawn_claims.is_empty(); + let recycles_ancestor_claims = encounter + .proposed_claims + .is_subset_of(&ancestry.ancestor_claims); + + let kind = if ancestry.closes_cycle && !has_new_root && !has_resistance { + RevisionKind::ClosedCycle + } else if !has_new_root && !has_resistance && (same_projection || recycles_ancestor_claims) + { + RevisionKind::Echo + } else if has_contradiction + && (!introduced_claims.is_empty() || !preserved_claims.is_empty()) + && has_new_root + { + RevisionKind::HorizonFusion + } else if has_contradiction { + RevisionKind::ContradictionPreserved + } else if has_resistance && !withdrawn_claims.is_empty() { + RevisionKind::AssumptionExposed + } else if has_new_root && same_projection { + RevisionKind::IndependentConfirmation + } else if has_new_root { + RevisionKind::HorizonExpansion + } else if has_resistance || !revised_claims.is_empty() { + RevisionKind::Reinterpretation + } else { + RevisionKind::Suspended + }; + + // The three `IncreaseEligible` kinds are each derived ABOVE under a + // `has_new_root` precondition, so a rootless confirmation / expansion / + // fusion is unreachable BY CONSTRUCTION. The draft expressed this as a + // match guard `if has_new_root` followed by a second listing of the same + // three variants falling through to `NoIncrease`. That guard can never + // be false, and the arms it shadowed are unreachable — a guard that + // cannot fail is the vacuous-guard defect (`CLAUDE.md` falsifiability + // rule), and the dead arms read as policy for a state that cannot occur. + // + // Asserted instead of re-guarded, so that relaxing a `kind` condition + // above trips here in debug rather than silently minting evidential + // weight for a rootless synthesis. + debug_assert!( + !matches!( + kind, + RevisionKind::IndependentConfirmation + | RevisionKind::HorizonExpansion + | RevisionKind::HorizonFusion + ) || has_new_root, + "an IncreaseEligible kind was derived without a new independent root" + ); + + let evidential_effect = match kind { + RevisionKind::IndependentConfirmation + | RevisionKind::HorizonExpansion + | RevisionKind::HorizonFusion => EvidentialEffect::IncreaseEligible, + RevisionKind::ContradictionPreserved | RevisionKind::Suspended => { + EvidentialEffect::Suspend + } + RevisionKind::Reinterpretation + | RevisionKind::AssumptionExposed + | RevisionKind::Echo + | RevisionKind::ClosedCycle => EvidentialEffect::NoIncrease, + }; + + let resulting = InterpretiveHorizon { + id: HorizonId(prior.id.0.wrapping_add(1)), + awareness: prior.awareness.clone(), + question: prior.question, + language: prior.language, + grammar: prior.grammar, + codebook: prior.codebook, + lens: prior.lens, + projected_claims: encounter.proposed_claims.clone(), + independent_roots: prior.independent_roots.union(&new_independent_roots), + inherited_roots: prior.inherited_roots.union(&encounter.inherited_roots), + unresolved_tension: prior.unresolved_tension.union(&encounter.contradictions), + revision_index: prior.revision_index.wrapping_add(1), + }; + + RevisionDelta { + kind, + evidential_effect, + prior: prior.clone(), + resulting, + preserved_claims, + introduced_claims, + withdrawn_claims, + revised_claims, + new_independent_roots, + inherited_roots: encounter.inherited_roots.clone(), + resistance: encounter.resistance.clone(), + contradictions: encounter.contradictions.clone(), + affected_parts: encounter.affected_parts.clone(), + } + } +} + +/// Output types deliberately stop before actual-world mutation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RevisionOutcome { + BranchUpdate(RevisionDelta), + HypothesisReport(HypothesisReport), + GroundingRequest(GroundingRequest), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct HypothesisReport { + pub claims: M, + pub contradictions: M, + pub kind: RevisionKind, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct GroundingRequest { + pub claims_to_test: M, + pub missing_independent_roots: M, + pub resistant_parts: M, +} + +#[cfg(test)] +mod tests { + use super::*; + + fn horizon(claims: u64, roots: u64) -> InterpretiveHorizon { + InterpretiveHorizon { + id: HorizonId(1), + awareness: 100, + question: QuestionId(1), + language: LanguageId(1), + grammar: GrammarId(1), + codebook: CodebookId(1), + lens: LensId(1), + projected_claims: claims, + independent_roots: roots, + inherited_roots: 0, + unresolved_tension: 0, + revision_index: 0, + } + } + + fn ancestry(claims: u64, roots: u64, cycle: bool) -> BasisView { + BasisView { + ancestry_independent_roots: roots, + ancestry_derived_roots: claims, + ancestor_claims: claims, + closes_cycle: cycle, + } + } + + fn encounter( + claims: u64, + independent: u64, + inherited: u64, + resistance: u64, + contradictions: u64, + ) -> EncounterEvidence { + EncounterEvidence { + proposed_claims: claims, + independent_roots: independent, + inherited_roots: inherited, + resistance, + contradictions, + affected_parts: resistance | contradictions, + } + } + + #[test] + fn repeated_derived_claim_is_an_echo_and_adds_no_weight() { + let prior = horizon(0b001, 0b001); + let result = GadamerRevision.revise( + &prior, + &encounter(0b001, 0b001, 0b100, 0, 0), + &ancestry(0b001, 0b001, false), + ); + assert_eq!(result.kind, RevisionKind::Echo); + assert_eq!(result.evidential_effect, EvidentialEffect::NoIncrease); + assert!(result.new_independent_roots.is_empty()); + } + + #[test] + fn self_supporting_ancestry_is_a_closed_cycle() { + let prior = horizon(0b001, 0b001); + let result = GadamerRevision.revise( + &prior, + &encounter(0b001, 0b001, 0b001, 0, 0), + &ancestry(0b001, 0b001, true), + ); + assert_eq!(result.kind, RevisionKind::ClosedCycle); + assert_eq!(result.evidential_effect, EvidentialEffect::NoIncrease); + } + + #[test] + fn same_claim_from_a_new_root_is_independent_confirmation() { + let prior = horizon(0b001, 0b001); + let result = GadamerRevision.revise( + &prior, + &encounter(0b001, 0b011, 0, 0, 0), + &ancestry(0b001, 0b001, false), + ); + assert_eq!(result.kind, RevisionKind::IndependentConfirmation); + assert_eq!(result.evidential_effect, EvidentialEffect::IncreaseEligible); + assert_eq!(result.new_independent_roots, 0b010); + } + + #[test] + fn resistance_that_withdraws_a_claim_exposes_an_assumption() { + let prior = horizon(0b011, 0b001); + let result = GadamerRevision.revise( + &prior, + &encounter(0b001, 0b001, 0, 0b010, 0), + &ancestry(0b011, 0b001, false), + ); + assert_eq!(result.kind, RevisionKind::AssumptionExposed); + assert_eq!(result.withdrawn_claims, 0b010); + assert_eq!(result.evidential_effect, EvidentialEffect::NoIncrease); + } + + #[test] + fn independent_horizons_can_fuse_without_erasing_tension() { + let prior = horizon(0b001, 0b001); + let result = GadamerRevision.revise( + &prior, + &encounter(0b011, 0b011, 0, 0b010, 0b100), + &ancestry(0b001, 0b001, false), + ); + assert_eq!(result.kind, RevisionKind::HorizonFusion); + assert_eq!(result.evidential_effect, EvidentialEffect::IncreaseEligible); + assert_eq!(result.resulting.unresolved_tension, 0b100); + } + + /// THE holy-grail falsifier: fusion and mere preservation see the SAME + /// contradiction, and differ only by whether a new independent root exists. + /// Without one, synthesis must NOT happen and no weight may be minted. + /// + /// Paired with `independent_horizons_can_fuse_without_erasing_tension`, + /// which is the identical encounter WITH a new root. Two-sided by + /// construction: neither half can pass for the other's reason. + #[test] + fn contradiction_without_a_new_root_suspends_instead_of_synthesising() { + let prior = horizon(0b001, 0b001); + // Identical to the fusion case EXCEPT independent_roots ⊆ ancestry, + // so `new_independent_roots` is empty. + let result = GadamerRevision.revise( + &prior, + &encounter(0b011, 0b001, 0, 0b010, 0b100), + &ancestry(0b001, 0b001, false), + ); + assert_eq!( + result.kind, + RevisionKind::ContradictionPreserved, + "no new independent root ⇒ the tension is held, not synthesised" + ); + assert_eq!(result.evidential_effect, EvidentialEffect::Suspend); + assert!( + result.new_independent_roots.is_empty(), + "anti-vacuity: the discriminating quantity really is absent here" + ); + // And the contradiction is still carried forward as durable structure. + assert_eq!(result.resulting.unresolved_tension, 0b100); + } + + /// Gadamer, not Hegel: fusion never CLEARS prior tension, it unions onto it. + #[test] + fn fusion_accumulates_tension_and_never_clears_prior_tension() { + let mut prior = horizon(0b001, 0b001); + prior.unresolved_tension = 0b1000; // tension carried in from before + let result = GadamerRevision.revise( + &prior, + &encounter(0b011, 0b011, 0, 0b010, 0b100), + &ancestry(0b001, 0b001, false), + ); + assert_eq!(result.kind, RevisionKind::HorizonFusion); + assert_eq!( + result.resulting.unresolved_tension, 0b1100, + "prior tension survives the synthesis; a fusion that resolved it \ + would be the false synthesis this policy refuses" + ); + } + + /// Every `IncreaseEligible` kind is reachable ONLY with a new independent + /// root — the invariant the removed match guard used to shadow. + #[test] + fn no_increase_eligible_outcome_is_reachable_without_a_new_root() { + let prior = horizon(0b011, 0b001); + // Sweep the rootless encounter space: no `independent_roots` beyond + // ancestry, across every combination of resistance/contradiction/shape. + for claims in [0b001_u64, 0b011, 0b111] { + for resistance in [0_u64, 0b010] { + for contradictions in [0_u64, 0b100] { + let result = GadamerRevision.revise( + &prior, + &encounter(claims, 0b001, 0, resistance, contradictions), + &ancestry(0b011, 0b001, false), + ); + assert_ne!( + result.evidential_effect, + EvidentialEffect::IncreaseEligible, + "rootless encounter minted weight: claims={claims:b} \ + resistance={resistance:b} contradictions={contradictions:b}" + ); + } + } + } + } + + #[test] + fn fixed_word_arrays_are_supported_without_heap_masks() { + let left = [0b001_u64, 0b100]; + let right = [0b010_u64, 0b100]; + assert_eq!(left.union(&right), [0b011, 0b100]); + assert_eq!(left.intersection(&right), [0, 0b100]); + } +} From 7ccfabe5891551b4fa5f42e2c1e80f45277ee30d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 07:10:41 +0000 Subject: [PATCH 2/6] contract: fusion.rs -- Horizontverschmelzung as candidate construction Completes the triptych the operator ruled must stay violently separate: fusion.rs GENERATES S what interpretation makes the relation between these positions intelligible? counterfactual.rs ATTACKS S remove S -- does structure collapse? revision.rs LICENSES S what mutation is warranted? Three different questions. fusion answers only the first and its output is a CANDIDATE; nothing here writes back. PR #1057 ratifies revision as the only write-back. REFUSES THE CHEAP OUTCOMES. Not "thesis wins", "antithesis wins", or a 50/50 compromise. The question is: what assumption must change so BOTH horizons become explicable without laundering either one's evidence? FusionOutcome therefore carries Synthesis / ThesisSurvives / AntithesisSurvives / Complementary / IrreducibleTension / Suspended / AskForMeans. AN ASSUMPTION IS AN INHERITED ROOT -- the mechanism that makes refusal checkable rather than stylistic. InterpretiveHorizon already separates independent_roots (contact with the world) from inherited_roots (interpretations consumed as interpretations), so an assumption held ONLY as inherited is revisable (withdrawing it discards no evidence) while one that is also independently grounded is not. Synthesis requires a revisable assumption; with none, the contradiction is irreducible and the engine says so. A fake intelligence always synthesises. THE ANTI-ALCHEMY LAW: understanding may increase without evidence increasing. Two horizons that look like independent witnesses but trace to the same root are ONE witness. Eligibility reads DISJOINT roots, never the union, so shared ancestry can never mint weight however intelligible the synthesis becomes. FusionReceipt::shared_roots records the collapse. THE RECEIPT PRESERVES BOTH ORIGINALS. A synthesis never overwrites thesis or antithesis -- tomorrow evidence may falsify it, and the system must then be able to ask why it looked compelling, which support was independent, which assumptions were fused, which contradiction was hidden. Without that, revision is history rewriting. Deliberately absent: any synthesis confidence scalar. Scalar confidence stays in NARS (f,c); CE64 61-63 stays a permission band (#1057). Six falsifiers, two disable-verified and each surgical: - eligibility reading the UNION instead of disjoint roots fails ONLY two_witnesses_sharing_one_root_never_become_evidentially_eligible; - making every assumption revisable fails ONLY irreducible_tension_is_reachable_when_no_assumption_is_revisable. Both anti-alchemy tests are two-sided: the shared-root refusal is paired with disjoint_roots_plus_a_revisable_assumption_earn_a_synthesis, so neither half can pass for the other's reason. Gates: 6/6 fusion, 1242 contract lib tests, clippy -D warnings clean, fmt. (cherry picked from commit 28cc1df25dc83bc2f3f29ff5a23e04ada709a665) --- crates/lance-graph-contract/src/fusion.rs | 361 ++++++++++++++++++++++ crates/lance-graph-contract/src/lib.rs | 1 + 2 files changed, 362 insertions(+) create mode 100644 crates/lance-graph-contract/src/fusion.rs diff --git a/crates/lance-graph-contract/src/fusion.rs b/crates/lance-graph-contract/src/fusion.rs new file mode 100644 index 000000000..d9ead0f61 --- /dev/null +++ b/crates/lance-graph-contract/src/fusion.rs @@ -0,0 +1,361 @@ +//! Horizontverschmelzung: candidate-synthesis construction over two horizons. +//! +//! # The triptych — kept violently separate (operator, 2026-08-29) +//! +//! ```text +//! fusion.rs GENERATES S "what interpretation makes the relation +//! between these positions intelligible?" +//! counterfactual.rs ATTACKS S "remove S — does explanatory structure +//! collapse?" (explanatory necessity) +//! revision.rs LICENSES S "given provenance, independence, +//! contradiction and the counterfactual +//! result, what mutation is warranted?" +//! ``` +//! +//! Three different questions. This module answers only the first, and its +//! output is a CANDIDATE — never a fact. Nothing here writes back; the write +//! path is [`crate::revision`], which PR #1057 ratifies as "the only +//! write-back" and "the court of appeal". +//! +//! # What this refuses to do +//! +//! The cheap outcomes are `thesis wins`, `antithesis wins`, and a 50/50 +//! compromise. None is a synthesis. The question asked instead is: +//! +//! > **What assumption must change so that BOTH horizons become explicable, +//! > without laundering either one's evidence?** +//! +//! **A fake intelligence always synthesises.** A serious one can conclude *"I +//! understand both horizons better now, and they still disagree"* — +//! [`FusionOutcome::IrreducibleTension`]. That variant is load-bearing, and +//! `irreducible_tension_is_reachable_when_no_assumption_is_revisable` proves +//! it can actually occur. +//! +//! # An assumption IS an inherited root +//! +//! This is the mechanism that makes the refusal checkable rather than +//! stylistic. [`crate::revision::InterpretiveHorizon`] already separates +//! `independent_roots` (contact with the world) from `inherited_roots` +//! (interpretations consumed as interpretations). So: +//! +//! - an assumption held ONLY as an inherited root is **revisable** — dropping +//! it withdraws no independent support; +//! - an assumption that is ALSO independently grounded is **not** freely +//! revisable — dropping it would discard evidence. +//! +//! Synthesis requires a revisable assumption. With none, the contradiction is +//! irreducible and the honest answer is to say so. +//! +//! # The anti-alchemy law +//! +//! > **Understanding may increase without evidence increasing.** +//! +//! Two horizons that look like independent witnesses but trace to the same +//! root are ONE witness. [`FusionReceipt::shared_roots`] records that +//! collapse, and such a fusion is never [`EvidentialEffect::IncreaseEligible`] +//! however intelligible the synthesis becomes. Coherence is allowed to rise; +//! evidential weight is not. +//! +//! # No confidence scalar +//! +//! Deliberately absent from [`FusionReceipt`]: any `synthesis_confidence: +//! f64`. Scalar confidence stays in NARS `(f, c)`; the band in CE64 bits +//! 61-63 stays a permission level (PR #1057). A receipt carries structure and +//! provenance, never a number that invites averaging. + +use crate::revision::{EvidenceMask, EvidentialEffect, HorizonId, InterpretiveHorizon}; + +/// What a fusion attempt concluded. Ordered coarse-to-fine by how much the +/// collision actually produced. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FusionOutcome { + /// A revisable assumption was found whose withdrawal makes both horizons + /// explicable. The originals are NOT overwritten — see [`FusionReceipt`]. + Synthesis(SynthesizedClaim), + /// The antithesis had no independent grounding of its own. + ThesisSurvives, + /// The thesis had no independent grounding of its own. + AntithesisSurvives, + /// The horizons never actually contradicted — different questions, not + /// rival answers. + Complementary, + /// Both horizons are independently grounded, they genuinely conflict, and + /// NO assumption is revisable without discarding evidence. The honest + /// terminal state, and the one a fake intelligence never reaches. + IrreducibleTension, + /// Neither side is independently grounded; the collision cannot be judged. + Suspended, + /// As `Suspended`, but the missing grounding is nameable. + AskForMeans { missing_independent_roots: M }, +} + +/// The candidate produced by a successful fusion. A CANDIDATE — counterfactual +/// attacks it next, revision licenses it after that. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SynthesizedClaim { + /// What both horizons still assert together. + pub preserved: M, + /// Assumptions withdrawn to make the collision explicable. Revisable by + /// construction: inherited, never independently grounded. + pub revised_assumptions: M, + /// Tension the synthesis does NOT dissolve. Gadamer, not Hegel. + pub surviving_tension: M, +} + +/// The audit record of a fusion attempt. +/// +/// **The synthesis never overwrites thesis or antithesis.** Both horizons stay +/// recoverable, because tomorrow some evidence may falsify the synthesis and +/// the system must then be able to ask why it looked compelling, which support +/// was independent, which assumptions were fused, and which contradiction was +/// merely hidden. Without that, "revision" is history rewriting. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FusionReceipt { + pub thesis: HorizonId, + pub antithesis: HorizonId, + pub thesis_claims: M, + pub antithesis_claims: M, + /// Roots reached independently by BOTH sides — the common-ancestry + /// collapse. Non-empty here means the two "witnesses" overlap. + pub shared_roots: M, + /// Roots held by exactly one side. This, not the union, is what makes a + /// fusion evidentially eligible. + pub disjoint_roots: M, + pub inherited_roots: M, + pub revisable_assumptions: M, + pub surviving_tension: M, + pub outcome: FusionOutcome, + pub evidential_effect: EvidentialEffect, +} + +/// Fuse two interpretive horizons into a candidate synthesis. +/// +/// `contradiction` is the mask of claims the caller has established as +/// genuinely conflicting between the two horizons — fusion does not infer +/// conflict, it is told where conflict is. +pub fn fuse( + thesis: &InterpretiveHorizon, + antithesis: &InterpretiveHorizon, + contradiction: &M, +) -> FusionReceipt { + let shared_roots = thesis + .independent_roots + .intersection(&antithesis.independent_roots); + let thesis_only = thesis + .independent_roots + .difference(&antithesis.independent_roots); + let antithesis_only = antithesis + .independent_roots + .difference(&thesis.independent_roots); + let disjoint_roots = thesis_only.union(&antithesis_only); + + let inherited_roots = thesis.inherited_roots.union(&antithesis.inherited_roots); + let all_independent = thesis + .independent_roots + .union(&antithesis.independent_roots); + // An assumption is revisable iff it is inherited and NOT independently + // grounded: withdrawing it discards interpretation, never evidence. + let revisable_assumptions = inherited_roots.difference(&all_independent); + + let preserved = thesis + .projected_claims + .intersection(&antithesis.projected_claims); + let surviving_tension = thesis + .unresolved_tension + .union(&antithesis.unresolved_tension) + .union(contradiction); + + let thesis_grounded = !thesis_only.is_empty(); + let antithesis_grounded = !antithesis_only.is_empty(); + let has_contradiction = !contradiction.is_empty(); + + let outcome = if !has_contradiction { + FusionOutcome::Complementary + } else if !thesis_grounded && !antithesis_grounded { + // Includes the two-witnesses-one-source case: identical roots leave + // both `*_only` masks empty however large the shared root set is. + if revisable_assumptions.is_empty() { + FusionOutcome::Suspended + } else { + FusionOutcome::AskForMeans { + missing_independent_roots: revisable_assumptions.clone(), + } + } + } else if thesis_grounded && !antithesis_grounded { + FusionOutcome::ThesisSurvives + } else if antithesis_grounded && !thesis_grounded { + FusionOutcome::AntithesisSurvives + } else if revisable_assumptions.is_empty() { + // Both independently grounded, genuinely conflicting, and nothing may + // be withdrawn without discarding evidence. + FusionOutcome::IrreducibleTension + } else { + FusionOutcome::Synthesis(SynthesizedClaim { + preserved: preserved.clone(), + revised_assumptions: revisable_assumptions.clone(), + surviving_tension: surviving_tension.clone(), + }) + }; + + // The anti-alchemy law. Only a genuinely two-witness fusion is eligible; + // shared ancestry, however intelligible the result, is not. + let evidential_effect = match &outcome { + FusionOutcome::Synthesis(_) if thesis_grounded && antithesis_grounded => { + EvidentialEffect::IncreaseEligible + } + FusionOutcome::Suspended + | FusionOutcome::AskForMeans { .. } + | FusionOutcome::IrreducibleTension => EvidentialEffect::Suspend, + _ => EvidentialEffect::NoIncrease, + }; + + FusionReceipt { + thesis: thesis.id, + antithesis: antithesis.id, + thesis_claims: thesis.projected_claims.clone(), + antithesis_claims: antithesis.projected_claims.clone(), + shared_roots, + disjoint_roots, + inherited_roots, + revisable_assumptions, + surviving_tension, + outcome, + evidential_effect, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::revision::{CodebookId, GrammarId, LanguageId, LensId, QuestionId}; + + fn horizon( + id: u64, + claims: u64, + independent: u64, + inherited: u64, + ) -> InterpretiveHorizon { + InterpretiveHorizon { + id: HorizonId(id), + awareness: 100, + question: QuestionId(1), + language: LanguageId(1), + grammar: GrammarId(1), + codebook: CodebookId(1), + lens: LensId(1), + projected_claims: claims, + independent_roots: independent, + inherited_roots: inherited, + unresolved_tension: 0, + revision_index: 0, + } + } + + /// THE anti-alchemy falsifier. Two horizons that look like independent + /// witnesses but both trace to source X (`0b001`) are ONE witness. However + /// intelligible the collision becomes, it may not mint evidential weight. + #[test] + fn two_witnesses_sharing_one_root_never_become_evidentially_eligible() { + let t = horizon(1, 0b0001, 0b001, 0b0110); // T <- X, via A,B + let a = horizon(2, 0b0010, 0b001, 0b1000); // A <- X, via D + let r = fuse(&t, &a, &0b0100); + + assert_eq!(r.shared_roots, 0b001, "the common ancestor is recorded"); + assert!( + r.disjoint_roots.is_empty(), + "anti-vacuity: neither side has grounding the other lacks" + ); + assert_ne!( + r.evidential_effect, + EvidentialEffect::IncreaseEligible, + "shared ancestry must never mint weight" + ); + assert!(matches!( + r.outcome, + FusionOutcome::Suspended | FusionOutcome::AskForMeans { .. } + )); + } + + /// The paired half: genuinely disjoint roots, and a revisable assumption, + /// DO earn a synthesis. Without this the test above could pass by the + /// engine simply never synthesising. + #[test] + fn disjoint_roots_plus_a_revisable_assumption_earn_a_synthesis() { + let t = horizon(1, 0b0001, 0b0001, 0b0100); // T <- X + let a = horizon(2, 0b0010, 0b0010, 0b0100); // A <- Y, X ⟂ Y + let r = fuse(&t, &a, &0b1000); + + assert!(r.shared_roots.is_empty(), "genuinely two witnesses"); + assert_eq!(r.disjoint_roots, 0b0011); + assert_eq!(r.revisable_assumptions, 0b0100, "inherited, not grounded"); + assert!(matches!(r.outcome, FusionOutcome::Synthesis(_))); + assert_eq!(r.evidential_effect, EvidentialEffect::IncreaseEligible); + } + + /// **A fake intelligence always synthesises.** Both horizons independently + /// grounded, genuinely conflicting, and every assumption is ALSO + /// independently grounded — so nothing may be withdrawn without discarding + /// evidence. The honest answer is that they still disagree. + #[test] + fn irreducible_tension_is_reachable_when_no_assumption_is_revisable() { + // inherited ⊆ independent ⇒ revisable_assumptions is empty. + let t = horizon(1, 0b0001, 0b0101, 0b0100); + let a = horizon(2, 0b0010, 0b1010, 0b1000); + let r = fuse(&t, &a, &0b0001); + + assert!( + r.revisable_assumptions.is_empty(), + "anti-vacuity: the discriminating quantity really is absent" + ); + assert_eq!(r.outcome, FusionOutcome::IrreducibleTension); + assert_eq!(r.evidential_effect, EvidentialEffect::Suspend); + assert!( + !r.surviving_tension.is_empty(), + "the disagreement is carried, not dissolved" + ); + } + + /// No contradiction ⇒ the horizons were answering different questions. + #[test] + fn absent_contradiction_is_complementary_not_synthesis() { + let t = horizon(1, 0b0001, 0b0001, 0b0100); + let a = horizon(2, 0b0010, 0b0010, 0b1000); + let r = fuse(&t, &a, &0); + assert_eq!(r.outcome, FusionOutcome::Complementary); + assert_eq!(r.evidential_effect, EvidentialEffect::NoIncrease); + } + + /// An ungrounded antithesis does not survive contact with a grounded one. + #[test] + fn an_ungrounded_side_does_not_win() { + let t = horizon(1, 0b0001, 0b0011, 0); + let a = horizon(2, 0b0010, 0b0001, 0); // roots ⊂ thesis's + let r = fuse(&t, &a, &0b1000); + assert_eq!(r.outcome, FusionOutcome::ThesisSurvives); + assert_eq!(r.evidential_effect, EvidentialEffect::NoIncrease); + } + + /// The receipt must keep BOTH originals recoverable — otherwise revision + /// is history rewriting. + #[test] + fn the_receipt_preserves_both_originals_and_carries_no_confidence_scalar() { + let t = horizon(7, 0b0001, 0b0001, 0b0100); + let a = horizon(9, 0b0010, 0b0010, 0b0100); + let r = fuse(&t, &a, &0b1000); + + assert_eq!(r.thesis, HorizonId(7)); + assert_eq!(r.antithesis, HorizonId(9)); + assert_eq!(r.thesis_claims, 0b0001, "thesis recoverable after fusion"); + assert_eq!( + r.antithesis_claims, 0b0010, + "antithesis recoverable after fusion" + ); + // The synthesis did NOT overwrite either horizon's claims. + if let FusionOutcome::Synthesis(s) = &r.outcome { + assert_ne!(s.preserved, r.thesis_claims); + assert_ne!(s.preserved, r.antithesis_claims); + } else { + panic!("expected a synthesis for this fixture"); + } + } +} diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 1ac5be813..6a92339dc 100644 --- a/crates/lance-graph-contract/src/lib.rs +++ b/crates/lance-graph-contract/src/lib.rs @@ -142,6 +142,7 @@ pub use qualia::{ axis_index, axis_label, qualia_to_state, QualiaI4_16D, QualiaVector, AXIS_LABELS, MIDPOINT, QUALIA_DIMS, QUALIA_I4_DIMS, QUALIA_I4_LABELS, ZERO, }; +pub mod fusion; pub mod materialize; pub mod reasoning; pub mod recipe_dispatch; From f44b9784aed3ce39a384770a449761365dfaed75 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 07:17:30 +0000 Subject: [PATCH 3/6] contract: route the revision exit -- Plan was declared and unreachable Closes ISS-KANBAN-PLAN-EXIT-HAS-NO-NAMED-ROUTE, filed earlier this session. Evaluation's successors are [Commit, Plan, Prune]. advance() takes the first non-Prune and therefore ALWAYS returns Commit; veto() takes Prune. NO named primitive produced Plan, so the documented revision exit -- "re-enter Planning carrying the witness" -- had legal-edge status (a persist_sink test asserts the edge is legal) and no route to it. The only (Evaluation, Plan) occurrence in the whole workspace was that test. KanbanColumn::revise() is the third primitive, symmetric with advance()/veto(): the successor equal to Plan, or None. It COMPLETES the Rubicon DAG rather than routing around it -- no edge added, no next_phases change, and every column it returns is one try_advance_phase already accepts. advance_on_revision(EvidentialEffect) is the TEST->ACCEPT step of #1057's loop, where revision is "the only write-back" and "the court of appeal". The three verdicts map 1:1 onto three routes: IncreaseEligible -> advance() -> Commit a new independent root was contacted; calcify NoIncrease -> revise() -> Plan understanding rose, evidence did not -- re-deliberate CARRYING THE WITNESS, which is the semantic movement itself Suspend -> None tension stays open pending grounding NoIncrease -> Plan is load-bearing: Echo and ClosedCycle both reduce to it, so a cycle that merely re-read inherited material returns to deliberation with what it learned and can NEVER reach Commit. That is the closed-path guarantee (F-MEP-0d) expressed as motion instead of prose. Revision never prunes -- Prune is the MUL gate's Block (Libet free-won't), a different act on a different arm; the absence is deliberate. Four falsifiers, disable-verified: making NoIncrease fall back to advance() fails exactly the two tests that assert the Plan route and the closed-path guarantee, and nothing else. PRIOR-ART CORRECTION, same commit. fusion.rs now names what it restates instead of implying novelty: BeliefArena::revise_at already implements BOTH the synthesis primitive and the anti-alchemy law -- its guard is b.stamp.disjoint(stamp), and since Stamp carries evidential ancestry, disjointness IS the independence test, so FusionReceipt::shared_roots is a re-derivation of it over a different carrier. And nars::stance::stance_panel already reads a contradiction set through four philosophical stances, its own doc stating the Hegel mapping exactly: the three meanings of aufheben ARE revise_at's three fields (cancelled = pooled truth, preserved = contradiction, lifted = rung), with Nietzsche/Kant/Wittgenstein as the other three, all late-bound and non-destructive. Hegelian synthesis was already in the substrate. fusion.rs adds only a zero-dep contract-layer seam for consumers that cannot depend on the planner; where the two disagree, revise_at is canonical. Gates: 1246 contract lib tests, clippy -D warnings clean, fmt clean. (cherry picked from commit cb923d5f1e2fbd3c9fa934d4a3c29da90f4dcc40) --- crates/lance-graph-contract/src/fusion.rs | 28 +++++ crates/lance-graph-contract/src/kanban.rs | 147 ++++++++++++++++++++++ 2 files changed, 175 insertions(+) diff --git a/crates/lance-graph-contract/src/fusion.rs b/crates/lance-graph-contract/src/fusion.rs index d9ead0f61..c4cb551e2 100644 --- a/crates/lance-graph-contract/src/fusion.rs +++ b/crates/lance-graph-contract/src/fusion.rs @@ -56,6 +56,34 @@ //! however intelligible the synthesis becomes. Coherence is allowed to rise; //! evidential weight is not. //! +//! # ⚠ PRIOR ART — this is a contract-layer restatement, NOT a new idea +//! +//! Found AFTER this module was written (2026-08-29), and recorded here so the +//! next session does not re-derive it a fourth time: +//! +//! - **`lance_graph_planner::nars::belief::BeliefArena::revise_at`** already +//! implements BOTH the synthesis primitive and the anti-alchemy law. Its +//! guard is `b.stamp.disjoint(stamp)` — `Stamp` carries evidential +//! ancestry, so disjointness IS the independence test, and +//! [`FusionReceipt::shared_roots`] is a re-derivation of it over a +//! different carrier (`EvidenceMask` instead of `Stamp`). +//! - **`lance_graph_planner::nars::stance::stance_panel`** already reads a +//! contradiction set through four philosophical stances, and its own doc +//! states the Hegel mapping exactly: *"The three meanings of aufheben ARE +//! `revise_at`'s three fields: cancelled = pooled truth, preserved = the +//! `contradiction` field, lifted = the rung."* Nietzsche (genealogy by flip +//! direction), Kant (ablate modal grading; the delta is the reader's +//! a-priori contribution), and Wittgenstein (meaning as distinct +//! language-games) are the other three. Stances are **late-bound and +//! non-destructive** — nothing collapses. +//! +//! So Hegelian synthesis is already in the substrate, and the "new horizon +//! from which the disagreement becomes intelligible" is already four horizons. +//! What this module adds is only a **zero-dep contract-layer seam**: the same +//! discipline expressed over `InterpretiveHorizon` without a `BeliefArena`, +//! for consumers that cannot depend on the planner. Where the two disagree, +//! **`revise_at` is canonical.** +//! //! # No confidence scalar //! //! Deliberately absent from [`FusionReceipt`]: any `synthesis_confidence: diff --git a/crates/lance-graph-contract/src/kanban.rs b/crates/lance-graph-contract/src/kanban.rs index 1b4bb2ccf..415223ce7 100644 --- a/crates/lance-graph-contract/src/kanban.rs +++ b/crates/lance-graph-contract/src/kanban.rs @@ -24,6 +24,7 @@ use crate::collapse_gate::MailboxId; use crate::mul::GateDecision; +use crate::revision::EvidentialEffect; /// The four Rubicon phases (+ two terminal exits), Libet-anchored. /// @@ -209,6 +210,61 @@ impl KanbanColumn { GateDecision::Hold { .. } => None, } } + + /// The REVISION exit — the third routing primitive, symmetric with + /// [`advance`](KanbanColumn::advance) and [`veto`](KanbanColumn::veto): + /// the first legal successor equal to [`KanbanColumn::Plan`], or `None`. + /// + /// # Why this exists — `Plan` was declared and structurally unreachable + /// + /// `Evaluation`'s successors are `[Commit, Plan, Prune]`. `advance()` + /// takes the first non-`Prune` and therefore ALWAYS returns `Commit`; + /// `veto()` takes `Prune`. **No named primitive produced `Plan`**, so the + /// documented revision exit — "re-enter Planning carrying the witness" — + /// had legal-edge status (asserted by a `persist_sink` test) and no route + /// to it. Logged as `ISS-KANBAN-PLAN-EXIT-HAS-NO-NAMED-ROUTE`; this is + /// the route. It COMPLETES the Rubicon model rather than routing around + /// it: no edge is added, no `next_phases` entry changes, and the returned + /// column is always one `try_advance_phase` already accepts. + #[inline] + #[must_use] + pub fn revise(self) -> Option { + self.next_phases() + .iter() + .copied() + .find(|c| *c == KanbanColumn::Plan) + } + + /// Route a cycle on a REVISION verdict — the `TEST → ACCEPT` step of the + /// `entropy-closure-causal-ground-v1` loop (PR #1057), where revision is + /// "the only write-back" and "the court of appeal". + /// + /// The three [`EvidentialEffect`] variants map 1:1 onto three routes, and + /// the mapping is the anti-laundering invariant expressed as motion: + /// + /// | verdict | route | why | + /// |---|---|---| + /// | [`IncreaseEligible`](EvidentialEffect::IncreaseEligible) | [`advance`](KanbanColumn::advance) → `Commit` | a genuinely new independent root was contacted; the synthesis earned calcification | + /// | [`NoIncrease`](EvidentialEffect::NoIncrease) | [`revise`](KanbanColumn::revise) → `Plan` | **understanding rose, evidence did not** — re-enter Planning *carrying the witness*, which IS the semantic movement | + /// | [`Suspend`](EvidentialEffect::Suspend) | `None` | the tension stays open pending grounding; the cycle holds rather than resolving | + /// + /// `NoIncrease → Plan` is the load-bearing row. `Echo` and `ClosedCycle` + /// both reduce to it, so a cycle that merely re-read inherited material + /// goes back to deliberation with what it learned — it can never reach + /// `Commit`, which is precisely the closed-path guarantee. + /// + /// **Revision never prunes.** `Prune` is the MUL gate's `Block` (Libet + /// free-won't), a different act on a different arm; nothing here can + /// produce it. Stated because the absence is deliberate, not an omission. + #[inline] + #[must_use] + pub fn advance_on_revision(self, effect: EvidentialEffect) -> Option { + match effect { + EvidentialEffect::IncreaseEligible => self.advance(), + EvidentialEffect::NoIncrease => self.revise(), + EvidentialEffect::Suspend => None, + } + } } /// The Libet readiness window, in µs — the `-550 ms` anchor a thinking cycle @@ -359,6 +415,97 @@ const _: () = assert!(core::mem::size_of::() <= 16); #[cfg(test)] mod tests { + /// `Plan` was DECLARED with semantics and structurally unreachable: from + /// `Evaluation`, `advance()` takes the first non-`Prune` (always `Commit`) + /// and `veto()` takes `Prune`. This is the route that closes + /// `ISS-KANBAN-PLAN-EXIT-HAS-NO-NAMED-ROUTE`. + #[test] + fn revise_reaches_the_plan_exit_that_advance_and_veto_cannot() { + let e = KanbanColumn::Evaluation; + assert_eq!(e.revise(), Some(KanbanColumn::Plan)); + // Anti-vacuity: the two pre-existing primitives really cannot get there. + assert_ne!(e.advance(), Some(KanbanColumn::Plan)); + assert_ne!(e.veto(), Some(KanbanColumn::Plan)); + assert_eq!(e.advance(), Some(KanbanColumn::Commit)); + } + + /// The three `EvidentialEffect` verdicts must route to three DIFFERENT + /// places — if any two coincided the mapping would carry no information. + #[test] + fn the_three_revision_verdicts_route_three_different_ways() { + let e = KanbanColumn::Evaluation; + let inc = e.advance_on_revision(EvidentialEffect::IncreaseEligible); + let noi = e.advance_on_revision(EvidentialEffect::NoIncrease); + let sus = e.advance_on_revision(EvidentialEffect::Suspend); + + assert_eq!(inc, Some(KanbanColumn::Commit), "earned root ⇒ calcify"); + assert_eq!( + noi, + Some(KanbanColumn::Plan), + "understanding rose, evidence did not ⇒ re-deliberate carrying the witness" + ); + assert_eq!(sus, None, "tension stays open pending grounding"); + assert_ne!(inc, noi); + assert_ne!(noi, sus); + assert_ne!(inc, sus); + } + + /// The closed-path guarantee: no revision verdict that failed to contact a + /// new independent root may reach `Commit`. + #[test] + fn a_rootless_verdict_can_never_reach_commit() { + for phase in [ + KanbanColumn::Planning, + KanbanColumn::CognitiveWork, + KanbanColumn::Evaluation, + KanbanColumn::Plan, + ] { + for effect in [EvidentialEffect::NoIncrease, EvidentialEffect::Suspend] { + assert_ne!( + phase.advance_on_revision(effect), + Some(KanbanColumn::Commit), + "{phase:?} + {effect:?} reached Commit without a new root" + ); + } + } + } + + /// Whatever `revise` proposes must be an edge `try_advance_phase` accepts — + /// it completes the Rubicon DAG, it does not route around it. And a column + /// with no `Plan` successor must stay silent. + #[test] + fn revise_only_ever_proposes_a_legal_edge_and_is_silent_elsewhere() { + for phase in [ + KanbanColumn::Planning, + KanbanColumn::CognitiveWork, + KanbanColumn::Evaluation, + KanbanColumn::Plan, + KanbanColumn::Commit, + KanbanColumn::Prune, + ] { + match phase.revise() { + Some(to) => assert!(phase.can_transition_to(to), "{phase:?} -> {to:?} illegal"), + None => assert!( + !phase.next_phases().contains(&KanbanColumn::Plan), + "{phase:?} has a Plan successor but revise() was silent" + ), + } + } + // Non-vacuous silence: exactly ONE column can revise. + let n = [ + KanbanColumn::Planning, + KanbanColumn::CognitiveWork, + KanbanColumn::Evaluation, + KanbanColumn::Plan, + KanbanColumn::Commit, + KanbanColumn::Prune, + ] + .iter() + .filter(|p| p.revise().is_some()) + .count(); + assert_eq!(n, 1, "only Evaluation carries the revision exit"); + } + use super::*; #[test] From b87b1e7733178ff3339ede65bf328a26dc5dd964 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 07:27:28 +0000 Subject: [PATCH 4/6] board: LATEST_STATE contract inventory for revision/fusion/revise + the five-loci finding Board-hygiene rule discharged for 5eb74b1e / 28cc1df2 / cb923d5f, which added contract types without the same-commit LATEST_STATE update the rule requires -- a rule this session cited repeatedly and then broke. Records the net delta (three modules, one primitive), why revision.rs exists (PR #1057 depends on it as 'the only write-back'), that ISS-KANBAN-PLAN-EXIT-HAS-NO-NAMED-ROUTE is closed, and a scope note stating against myself that 1138 lines of contract code now sit on a PR whose status line says PLAN/BOARD ONLY -- with the split recommended but NOT done, since rewriting a pushed branch is the operator's call. Also records the prior art this session re-derived thinner versions of: revise_at (stamp disjointness IS the independence test; canonical), stance_panel (Hegel/Nietzsche/Kant/Wittgenstein, aufheben == revise_at's three fields), and the epistemic-quadrant probe (only-the-elimination-returns). The architecture lives in five uncited loci; scattered, not missing. (cherry picked from commit 25e53d39ae100821b5bf9e9bd9d8f250cdcab976) --- .claude/board/LATEST_STATE.md | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 6348f0425..766cecf16 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -14,6 +14,70 @@ untouched until that carve actually lands (it re-bases to 17 then). `TokenId` vs `WordId` control) → D-TVT-2 (the carve, A-or-B decided by D-TVT-1) → D-TVT-3 (lens write onto SPO-stream rows) → D-TVT-4 (BUY / NO-BUY). +## 2026-08-29 — the epistemic triptych lands in the contract (BRANCH, not yet merged) + +> **⚠ SCOPE NOTE, stated against myself.** These three modules were committed +> onto `claude/happy-hamilton-0azlw4`, the branch of PR #1074 — whose own +> status line reads *"PLAN/BOARD ONLY. Measure-before-carve. **No contract +> change, no wiring**, until W1's numbers land."* 1138 lines of contract code +> now sit on a plan-only PR. The plan's STOP rule targets the EWA/Σ carrier +> (K1 `TrustSigma` on `TrustQualia`) specifically, and **none of this is that +> carrier** — so the rule's intent is intact — but its letter is not, and a +> reviewer of a plan now faces Rust. **Recommended: split these three commits +> (`5eb74b1e`, `28cc1df2`, `cb923d5f`) onto their own PR** and restore #1074 +> to plan-only. Not done unilaterally: it needs a force-push to a pushed +> branch, which is the operator's call. + +### Current Contract Inventory — net delta: THREE modules, one primitive + +| added | where | what it is | +|---|---|---| +| `revision` (module) | `contract/src/revision.rs` | `EvidenceMask`, `InterpretiveHorizon`, `EncounterEvidence`, `BasisView`, `RevisionKind` (9), `EvidentialEffect` (3), `RevisionDelta`, `RevisionPolicy`, `GadamerRevision`, `RevisionOutcome`, `HypothesisReport`, `GroundingRequest` | +| `fusion` (module) | `contract/src/fusion.rs` | `FusionOutcome` (7), `SynthesizedClaim`, `FusionReceipt`, `fuse()` | +| `KanbanColumn::revise` | `contract/src/kanban.rs` | third routing primitive beside `advance`/`veto`; reaches `Plan` | +| `KanbanColumn::advance_on_revision` | `contract/src/kanban.rs` | `EvidentialEffect` → route, the `TEST→ACCEPT` step of #1057 | + +**Why revision.rs at all:** PR #1057 (MERGED) names revision *"the only +write-back"* and *"the court of appeal"* at its ACCEPT step, while +`counterfactual.rs` had shipped twice and `revision.rs` never landed. A merged +plan's ACCEPT step was governed by a module that did not exist. + +**`ISS-KANBAN-PLAN-EXIT-HAS-NO-NAMED-ROUTE` is CLOSED.** `Evaluation`'s +successors are `[Commit, Plan, Prune]`; `advance()` takes the first non-`Prune` +(always `Commit`), `veto()` takes `Prune`, and nothing produced `Plan`. The +documented revision exit had legal-edge status and no route. `revise()` is the +route; no edge added, no `next_phases` change. + +### ⚠ PRIOR ART — three loci that already held this, uncited + +Recorded because this session re-derived thinner versions of all three before +finding them, twice AFTER diagnosing the pattern: + +- **`planner::nars::belief::BeliefArena::revise_at`** — already implements the + synthesis primitive AND the anti-alchemy law. Its guard is + `b.stamp.disjoint(stamp)`; `Stamp` carries evidential ancestry, so + **disjointness IS the independence test**. `FusionReceipt::shared_roots` is a + re-derivation of it over a different carrier. **`revise_at` is canonical.** +- **`planner::nars::stance::stance_panel`** — four philosophical late-bound, + non-destructive reads over one contradiction set. Its own doc: *"The three + meanings of aufheben ARE `revise_at`'s three fields: cancelled = pooled + truth, preserved = the `contradiction` field, lifted = the rung."* Plus + Nietzsche (genealogy by flip direction), Kant (ablate modal grading; the + delta is the reader's a-priori contribution, doubling as an inertness test), + Wittgenstein (distinct language-games). Hegelian synthesis was already in the + substrate. +- **`.claude/plans/epistemic-quadrant-materialization-v1.md`** + its 1859-line + `probe_sudoku_teacher.rs` — G3 is the membrane: *"bifurcation clones the slab + as a counterfactual world, propagates to contradiction, and **ONLY the + elimination returns**"*. G4 measures the cost of refusing to fork. + +**The finding that outranks any single defect: this architecture lives in FIVE +loci that do not cross-reference** — `revise_at`, `stance_panel`, PR #1057, +`epistemic-quadrant-materialization-v1`, and now the contract layer. Nothing is +missing; it is scattered, and each session re-derives a thinner version of a +neighbour it never read. A cross-reference pass is worth more than more code. + + ## 2026-08-27 — the D-MCAL arc, #1065–#1070 ALL MERGED (six of six deliverables) ### Current Contract Inventory — net delta of the arc From fab75b484b85de8bc2d9796493d950c828fe4d0f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 08:24:23 +0000 Subject: [PATCH 5/6] contract: three operator corrections -- close the eligibility->Commit shortcut Operator sent back three corrections rather than more code. All three were verified against source before landing; two of them correct claims I made. 1. Stamp::disjoint is NOT the canonical independence test (I was wrong). I recorded "disjointness IS the independence test; revise_at is canonical". PR #854 had already ruled otherwise: event identity != evidential-base membership != source dependence (LATEST_STATE:1567). Stamp models source MEMBERSHIP and is lossy -- Stamp::source(id) = 1 << (id % 64), so ids 0 and 64 alias -- and causal_audit.rs:346 leaves independent_strength None because there is NO DEPENDENCE MODEL. So disjoint is SOUND BUT NOT COMPLETE: aliasing yields only false overlap, never false disjointness, so revision under-pools rather than double-counts. It rejects KNOWN overlap; it does not establish independence. The remedy (EvidenceEventId, EvidentialBase, TRI-STATE OverlapKnowledge/Independence) is designed and unbuilt -- tri-state being the tell, since disjoint returns a bool and collapses Unknown. Corrected register: revise_at canonical for S4 pooling under known overlap; the #854 ruling canonical for independence NOT being established. FusionReceipt::shared_roots is therefore CONJECTURE expressing a contract the substrate cannot yet attest -- not duplication. 2. stance_panel is NOT H3 (I overcorrected). I concluded "Hegelian synthesis was already in the substrate" and the new horizon "is already four horizons". Those are four READINGS of an existing state -- blend modes over the same pixels, non-destructive by design. The Grail question is what new explanatory structure makes H1 and H2 intelligible as partial horizons of a larger whole. revise_at pools the truth of the SAME CStmt under the S4 guard, which is an Aufhebung metaphor, not the inference of a latent mediator, revised ontology or perspective transform. THE GRAIL SURVIVES, and is unbuilt. 3. IncreaseEligible -> Commit was a legal shortcut past the architecture. IncreaseEligible means exactly "a genuinely new independent root was introduced". It does NOT mean the counterfactual passed, the band admitted the operation, provenance is authoritative, or the candidate survived falsification. GadamerRevision::revise emits it from an EncounterEvidence ALONE -- no FusionReceipt, no attack -- so routing it to Commit bypassed Fusion -> Counterfactual -> Revision entirely. Fixed by typing the docket: CounterfactualVerdict {Necessary, Dispensable, NotRun} + RevisionVerdict {effect, counterfactual} with ::unadjudicated() as the honest default. advance_on_revision now takes the VERDICT, not the bare effect: Commit requires effect==IncreaseEligible AND counterfactual==Necessary; eligible-but-unattacked routes to Plan (understanding rose, docket incomplete, re-deliberate carrying the witness). NoIncrease -> Plan and Suspend -> None are unchanged. revision_verdict_alone_cannot_reach_commit PROVES the shortcut rather than asserting it: it walks the real path (EncounterEvidence -> GadamerRevision -> IncreaseEligible, anti-vacuity asserted), then shows unadjudicated routes to Plan and never Commit, that Necessary does open the gate, and that Dispensable does not. Disable-verified: restoring the old unconditional arm fails exactly that test and nothing else. Also records in fusion.rs that fuse() establishes synthesis ADMISSIBILITY and provenance, never the new relation X -- SynthesizedClaim carries no inferred structure, and that boundary is deliberate so this module cannot become the thing that both proposes and licenses. Gates: 1247 contract lib tests, clippy -D warnings clean, fmt clean. (cherry picked from commit 0dc6d92501727988eeff05f4cb029775cc6d0918) --- .claude/board/LATEST_STATE.md | 33 ++++-- crates/lance-graph-contract/src/fusion.rs | 59 ++++++++--- crates/lance-graph-contract/src/kanban.rs | 105 ++++++++++++++++++-- crates/lance-graph-contract/src/revision.rs | 65 ++++++++++++ 4 files changed, 234 insertions(+), 28 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 766cecf16..bbd4388ae 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -53,19 +53,38 @@ route; no edge added, no `next_phases` change. Recorded because this session re-derived thinner versions of all three before finding them, twice AFTER diagnosing the pattern: -- **`planner::nars::belief::BeliefArena::revise_at`** — already implements the - synthesis primitive AND the anti-alchemy law. Its guard is - `b.stamp.disjoint(stamp)`; `Stamp` carries evidential ancestry, so - **disjointness IS the independence test**. `FusionReceipt::shared_roots` is a - re-derivation of it over a different carrier. **`revise_at` is canonical.** +- **`planner::nars::belief::BeliefArena::revise_at`** — canonical for **S4 + pooling / known-overlap rejection**, via `b.stamp.disjoint(stamp)`. + > **⊘ CORRECTED same-day (operator).** This entry first said disjointness + > "IS the independence test" and that `revise_at` is canonical for + > independence. **Wrong** — PR #854 already ruled it: + > `event identity ≠ evidential-base membership ≠ source dependence` + > (line 1567 of this file). `Stamp` models source MEMBERSHIP and is lossy + > (`1 << (id % 64)`; ids 0 and 64 alias); `causal_audit.rs:346` leaves + > `independent_strength` `None` because there is **no dependence model**. + > `disjoint` is SOUND but NOT COMPLETE — aliasing yields only false overlap, + > so revision under-pools rather than double-counts. + > **Canonical register, corrected:** `revise_at` for S4 pooling under + > known overlap; **the #854 ruling** for the fact that true evidential + > independence is NOT established, its remedy (`EvidenceEventId`, + > `EvidentialBase`, tri-state `Independence`) designed and unbuilt. + > `FusionReceipt::shared_roots` is therefore CONJECTURE expressing a + > contract the substrate cannot yet attest — not duplication. - **`planner::nars::stance::stance_panel`** — four philosophical late-bound, non-destructive reads over one contradiction set. Its own doc: *"The three meanings of aufheben ARE `revise_at`'s three fields: cancelled = pooled truth, preserved = the `contradiction` field, lifted = the rung."* Plus Nietzsche (genealogy by flip direction), Kant (ablate modal grading; the delta is the reader's a-priori contribution, doubling as an inertness test), - Wittgenstein (distinct language-games). Hegelian synthesis was already in the - substrate. + Wittgenstein (distinct language-games). + > **⊘ CORRECTED same-day (operator): `stance_panel` is NOT H₃.** This entry + > first concluded "Hegelian synthesis was already in the substrate" and that + > the new horizon "is already four horizons". **Overcorrected.** Those are + > four READINGS of an existing state — blend modes over the same pixels. + > The Grail question is what new explanatory structure makes H₁ and H₂ + > intelligible as partial horizons of a larger whole; `revise_at` pools the + > truth of the SAME `CStmt`, which is an Aufhebung metaphor, not the + > inference of a latent mediator. **The Grail survives, and is unbuilt.** - **`.claude/plans/epistemic-quadrant-materialization-v1.md`** + its 1859-line `probe_sudoku_teacher.rs` — G3 is the membrane: *"bifurcation clones the slab as a counterfactual world, propagates to contradiction, and **ONLY the diff --git a/crates/lance-graph-contract/src/fusion.rs b/crates/lance-graph-contract/src/fusion.rs index c4cb551e2..4f7a21424 100644 --- a/crates/lance-graph-contract/src/fusion.rs +++ b/crates/lance-graph-contract/src/fusion.rs @@ -61,12 +61,30 @@ //! Found AFTER this module was written (2026-08-29), and recorded here so the //! next session does not re-derive it a fourth time: //! -//! - **`lance_graph_planner::nars::belief::BeliefArena::revise_at`** already -//! implements BOTH the synthesis primitive and the anti-alchemy law. Its -//! guard is `b.stamp.disjoint(stamp)` — `Stamp` carries evidential -//! ancestry, so disjointness IS the independence test, and -//! [`FusionReceipt::shared_roots`] is a re-derivation of it over a -//! different carrier (`EvidenceMask` instead of `Stamp`). +//! - **`lance_graph_planner::nars::belief::BeliefArena::revise_at`** carries +//! the S4 pooling guard: `b.stamp.disjoint(stamp)`. +//! +//! > **⊘ CORRECTION (operator, 2026-08-29).** An earlier revision of this +//! > block said disjointness "IS the independence test" and that +//! > [`FusionReceipt::shared_roots`] merely re-derives it. **That was +//! > wrong**, and PR #854 already ruled why: +//! > `event identity ≠ evidential-base membership ≠ source dependence` +//! > (`LATEST_STATE.md:1567`). `Stamp` models **source MEMBERSHIP**, and it +//! > is lossy — `Stamp::source(id) = 1 << (id % 64)`, so ids `0` and `64` +//! > alias. `causal_audit.rs:346`: *"`independent_strength` is left `None` +//! > throughout: **no dependence model**."* +//! > +//! > So `disjoint` is **sound but not complete**: aliasing manufactures only +//! > false OVERLAP, never false disjointness, so revision under-pools rather +//! > than double-counts. It safely rejects KNOWN overlap; it does not +//! > establish independence. The named remedy — `EvidenceEventId` + +//! > `EvidentialBase` + **tri-state** `OverlapKnowledge`/`Independence` — +//! > is designed and unbuilt, and tri-state is the tell: `disjoint` returns +//! > a bool and so collapses `Unknown` into one of the two answers. +//! > +//! > [`FusionReceipt::shared_roots`] therefore expresses a contract the +//! > substrate **cannot yet attest** — CONJECTURE, not duplication. Read it +//! > as aspirational until an evidence-event identity exists. //! - **`lance_graph_planner::nars::stance::stance_panel`** already reads a //! contradiction set through four philosophical stances, and its own doc //! states the Hegel mapping exactly: *"The three meanings of aufheben ARE @@ -77,12 +95,29 @@ //! language-games) are the other three. Stances are **late-bound and //! non-destructive** — nothing collapses. //! -//! So Hegelian synthesis is already in the substrate, and the "new horizon -//! from which the disagreement becomes intelligible" is already four horizons. -//! What this module adds is only a **zero-dep contract-layer seam**: the same -//! discipline expressed over `InterpretiveHorizon` without a `BeliefArena`, -//! for consumers that cannot depend on the planner. Where the two disagree, -//! **`revise_at` is canonical.** +//! > **⊘ SECOND CORRECTION — `stance_panel` is not H₃.** An earlier +//! > revision claimed the "new horizon from which the disagreement becomes +//! > intelligible" was "already four horizons". **Overcorrected.** Those are +//! > four *readings of an existing state* — how Hegel ranks this +//! > contradiction, how Nietzsche reads its genealogy, what Kant's a-priori +//! > reader contributed, which language-games Wittgenstein sees. They are +//! > blend modes over the same pixels, non-destructive by design. +//! > +//! > The Grail question is different: **what new explanatory structure makes +//! > H₁ and H₂ intelligible as partial horizons of a larger whole?** Neither +//! > `stance_panel` nor `revise_at` constructs that — `revise_at` pools the +//! > truth of the SAME `CStmt` under the S4 guard, which is a computational +//! > Aufhebung metaphor, not the inference of a latent mediator, a revised +//! > ontology, or a perspective transform. **The Grail survives.** +//! +//! # What this module does and does not do +//! +//! `fuse()` establishes **synthesis ADMISSIBILITY and provenance** — when a +//! synthesis is allowed, and on whose evidence. It does NOT infer the new +//! relation: [`SynthesizedClaim`] carries `preserved` / `revised_assumptions` +//! / `surviving_tension` and no inferred structure `X`. That boundary is +//! deliberate, not an omission: the generative act stays outside, so this +//! module cannot become the thing that both proposes and licenses. //! //! # No confidence scalar //! diff --git a/crates/lance-graph-contract/src/kanban.rs b/crates/lance-graph-contract/src/kanban.rs index 415223ce7..436f32030 100644 --- a/crates/lance-graph-contract/src/kanban.rs +++ b/crates/lance-graph-contract/src/kanban.rs @@ -24,7 +24,7 @@ use crate::collapse_gate::MailboxId; use crate::mul::GateDecision; -use crate::revision::EvidentialEffect; +use crate::revision::{EvidentialEffect, RevisionVerdict}; /// The four Rubicon phases (+ two terminal exits), Libet-anchored. /// @@ -244,7 +244,8 @@ impl KanbanColumn { /// /// | verdict | route | why | /// |---|---|---| - /// | [`IncreaseEligible`](EvidentialEffect::IncreaseEligible) | [`advance`](KanbanColumn::advance) → `Commit` | a genuinely new independent root was contacted; the synthesis earned calcification | + /// | [`IncreaseEligible`](EvidentialEffect::IncreaseEligible) **+ counterfactual `Necessary`** | [`advance`](KanbanColumn::advance) → `Commit` | a new independent root was contacted AND the candidate survived attack — the docket completed | + /// | `IncreaseEligible` with the docket **incomplete** | [`revise`](KanbanColumn::revise) → `Plan` | eligible ≠ accepted; re-deliberate rather than calcify on an unattacked candidate | /// | [`NoIncrease`](EvidentialEffect::NoIncrease) | [`revise`](KanbanColumn::revise) → `Plan` | **understanding rose, evidence did not** — re-enter Planning *carrying the witness*, which IS the semantic movement | /// | [`Suspend`](EvidentialEffect::Suspend) | `None` | the tension stays open pending grounding; the cycle holds rather than resolving | /// @@ -258,9 +259,14 @@ impl KanbanColumn { /// produce it. Stated because the absence is deliberate, not an omission. #[inline] #[must_use] - pub fn advance_on_revision(self, effect: EvidentialEffect) -> Option { - match effect { - EvidentialEffect::IncreaseEligible => self.advance(), + pub fn advance_on_revision(self, verdict: RevisionVerdict) -> Option { + match verdict.effect { + // Eligibility is NOT acceptance. A new independent root makes a + // synthesis eligible; only surviving the counterfactual attack + // makes it acceptable. Without that, the docket is incomplete and + // the cycle re-deliberates rather than calcifying. + EvidentialEffect::IncreaseEligible if verdict.is_acceptable() => self.advance(), + EvidentialEffect::IncreaseEligible => self.revise(), EvidentialEffect::NoIncrease => self.revise(), EvidentialEffect::Suspend => None, } @@ -415,6 +421,7 @@ const _: () = assert!(core::mem::size_of::() <= 16); #[cfg(test)] mod tests { + use crate::revision::CounterfactualVerdict; /// `Plan` was DECLARED with semantics and structurally unreachable: from /// `Evaluation`, `advance()` takes the first non-`Prune` (always `Commit`) /// and `veto()` takes `Prune`. This is the route that closes @@ -429,14 +436,94 @@ mod tests { assert_eq!(e.advance(), Some(KanbanColumn::Commit)); } + /// **Proof the shortcut existed, and is closed.** `GadamerRevision::revise` + /// can emit `IncreaseEligible` from an `EncounterEvidence` alone — no + /// `FusionReceipt`, no counterfactual attack. Routing that to `Commit` + /// bypassed Fusion → Counterfactual → Revision entirely. + #[test] + fn revision_verdict_alone_cannot_reach_commit() { + use crate::revision::{ + BasisView, EncounterEvidence, GadamerRevision, HorizonId, InterpretiveHorizon, + RevisionPolicy, + }; + use crate::revision::{CodebookId, GrammarId, LanguageId, LensId, QuestionId}; + + // The real shortcut path: raw encounter -> policy -> eligible. + let prior = InterpretiveHorizon:: { + id: HorizonId(1), + awareness: 0, + question: QuestionId(1), + language: LanguageId(1), + grammar: GrammarId(1), + codebook: CodebookId(1), + lens: LensId(1), + projected_claims: 0b001, + independent_roots: 0b001, + inherited_roots: 0, + unresolved_tension: 0, + revision_index: 0, + }; + let delta = GadamerRevision.revise( + &prior, + &EncounterEvidence { + proposed_claims: 0b001, + independent_roots: 0b011, + inherited_roots: 0, + resistance: 0, + contradictions: 0, + affected_parts: 0, + }, + &BasisView { + ancestry_independent_roots: 0b001, + ancestry_derived_roots: 0b001, + ancestor_claims: 0b001, + closes_cycle: false, + }, + ); + // Anti-vacuity: the shortcut's precondition really is met. + assert_eq!(delta.evidential_effect, EvidentialEffect::IncreaseEligible); + + let e = KanbanColumn::Evaluation; + assert_eq!( + e.advance_on_revision(RevisionVerdict::unadjudicated(delta.evidential_effect)), + Some(KanbanColumn::Plan), + "eligible but unattacked must re-deliberate, never calcify" + ); + assert_ne!( + e.advance_on_revision(RevisionVerdict::unadjudicated(delta.evidential_effect)), + Some(KanbanColumn::Commit), + "THE shortcut: eligibility alone reaching Commit bypasses the docket" + ); + // ...and the docket, once walked, does open the gate. + assert_eq!( + e.advance_on_revision(RevisionVerdict { + effect: delta.evidential_effect, + counterfactual: CounterfactualVerdict::Necessary, + }), + Some(KanbanColumn::Commit), + ); + // A candidate that survived nothing is not accepted either. + assert_eq!( + e.advance_on_revision(RevisionVerdict { + effect: EvidentialEffect::IncreaseEligible, + counterfactual: CounterfactualVerdict::Dispensable, + }), + Some(KanbanColumn::Plan), + ); + } + /// The three `EvidentialEffect` verdicts must route to three DIFFERENT /// places — if any two coincided the mapping would carry no information. #[test] fn the_three_revision_verdicts_route_three_different_ways() { let e = KanbanColumn::Evaluation; - let inc = e.advance_on_revision(EvidentialEffect::IncreaseEligible); - let noi = e.advance_on_revision(EvidentialEffect::NoIncrease); - let sus = e.advance_on_revision(EvidentialEffect::Suspend); + let inc = e.advance_on_revision(RevisionVerdict { + effect: EvidentialEffect::IncreaseEligible, + counterfactual: CounterfactualVerdict::Necessary, + }); + let noi = + e.advance_on_revision(RevisionVerdict::unadjudicated(EvidentialEffect::NoIncrease)); + let sus = e.advance_on_revision(RevisionVerdict::unadjudicated(EvidentialEffect::Suspend)); assert_eq!(inc, Some(KanbanColumn::Commit), "earned root ⇒ calcify"); assert_eq!( @@ -462,7 +549,7 @@ mod tests { ] { for effect in [EvidentialEffect::NoIncrease, EvidentialEffect::Suspend] { assert_ne!( - phase.advance_on_revision(effect), + phase.advance_on_revision(RevisionVerdict::unadjudicated(effect)), Some(KanbanColumn::Commit), "{phase:?} + {effect:?} reached Commit without a new root" ); diff --git a/crates/lance-graph-contract/src/revision.rs b/crates/lance-graph-contract/src/revision.rs index 350a459b1..525012633 100644 --- a/crates/lance-graph-contract/src/revision.rs +++ b/crates/lance-graph-contract/src/revision.rs @@ -220,6 +220,71 @@ pub enum EvidentialEffect { Suspend, } +/// Did the counterfactual attack actually run, and what did it find? +/// +/// The middle leg of the Fusion → Counterfactual → Revision docket. Its +/// question is **explanatory necessity**: remove the candidate — does +/// structure collapse? +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CounterfactualVerdict { + /// Removal collapsed explanatory structure: the candidate is load-bearing. + Necessary, + /// Structure survived removal: the candidate is decorative. + Dispensable, + /// **Not attacked.** The docket is incomplete — the default, and the + /// reason this type exists. + NotRun, +} + +/// An ADJUDICATED revision — an [`EvidentialEffect`] plus proof the docket was +/// actually walked. +/// +/// # Why eligibility is not acceptance +/// +/// [`EvidentialEffect::IncreaseEligible`] means exactly one thing: *a +/// genuinely new independent root was introduced*. It does **not** mean the +/// counterfactual passed, the reasoning band admitted the operation, +/// provenance is authoritative, or the synthesis survived falsification. +/// +/// [`GadamerRevision::revise`] can emit `IncreaseEligible` from an +/// `EncounterEvidence` alone — no [`crate::fusion::FusionReceipt`], no +/// counterfactual attack. Routing that straight to `Commit` was a legal +/// shortcut PAST the architecture: +/// +/// ```text +/// EncounterEvidence → revise → IncreaseEligible → Commit (the shortcut) +/// Fusion → Counterfactual → Revision → ACCEPT (the docket) +/// ``` +/// +/// `revision_verdict_alone_cannot_reach_commit` proves the shortcut was +/// reachable and is now closed: eligibility WITHOUT `Necessary` routes to +/// `Plan` — understanding rose, the docket did not complete, so re-deliberate +/// carrying the witness. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct RevisionVerdict { + pub effect: EvidentialEffect, + pub counterfactual: CounterfactualVerdict, +} + +impl RevisionVerdict { + /// A verdict with the docket NOT walked — the honest default. + #[must_use] + pub fn unadjudicated(effect: EvidentialEffect) -> Self { + Self { + effect, + counterfactual: CounterfactualVerdict::NotRun, + } + } + + /// Only an eligible effect whose candidate survived counterfactual attack + /// may be accepted into reality. + #[must_use] + pub fn is_acceptable(self) -> bool { + self.effect == EvidentialEffect::IncreaseEligible + && self.counterfactual == CounterfactualVerdict::Necessary + } +} + /// Explicit delta between the prior and resulting horizons. #[derive(Debug, Clone, PartialEq, Eq)] pub struct RevisionDelta { From 15982f74d2481b160f8fff860dc636a47c2f15f7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 29 Aug 2026 12:38:50 +0000 Subject: [PATCH 6/6] board: record the split -- scope violation resolved, not merely flagged The five contract commits are cut onto claude/epistemic-triptych-contract from main, and #1074 is recut to EWA measurement / attention geometry only, per operator instruction. Supersession index regenerated against this branch's plan set. --- .claude/board/LATEST_STATE.md | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index bbd4388ae..733f0d849 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -16,17 +16,20 @@ D-TVT-1) → D-TVT-3 (lens write onto SPO-stream rows) → D-TVT-4 (BUY / NO-BUY ## 2026-08-29 — the epistemic triptych lands in the contract (BRANCH, not yet merged) -> **⚠ SCOPE NOTE, stated against myself.** These three modules were committed -> onto `claude/happy-hamilton-0azlw4`, the branch of PR #1074 — whose own -> status line reads *"PLAN/BOARD ONLY. Measure-before-carve. **No contract -> change, no wiring**, until W1's numbers land."* 1138 lines of contract code -> now sit on a plan-only PR. The plan's STOP rule targets the EWA/Σ carrier -> (K1 `TrustSigma` on `TrustQualia`) specifically, and **none of this is that -> carrier** — so the rule's intent is intact — but its letter is not, and a -> reviewer of a plan now faces Rust. **Recommended: split these three commits -> (`5eb74b1e`, `28cc1df2`, `cb923d5f`) onto their own PR** and restore #1074 -> to plan-only. Not done unilaterally: it needs a force-push to a pushed -> branch, which is the operator's call. +> **✅ SCOPE RESOLVED (operator-instructed, 2026-08-29).** These commits were +> first landed onto `claude/happy-hamilton-0azlw4` — the branch of PR #1074, +> whose status line reads *"PLAN/BOARD ONLY. Measure-before-carve. **No +> contract change, no wiring**, until W1's numbers land."* That was a scope +> violation I flagged against myself: 1138 lines of contract code on a +> plan-only PR. #1074's STOP rule targets the EWA/Σ carrier (K1 `TrustSigma` +> on `TrustQualia`) specifically, and **none of this is that carrier**, so the +> rule's intent was intact — but its letter was not, and a reviewer of a plan +> faced Rust. +> +> **Split executed on operator instruction:** the five commits now live on +> `claude/epistemic-triptych-contract`, cut from `main`, and #1074 is recut to +> EWA measurement / attention geometry only. Each PR can now be reviewed on the +> questions it actually raises. ### Current Contract Inventory — net delta: THREE modules, one primitive