diff --git a/.claude/board/LATEST_STATE.md b/.claude/board/LATEST_STATE.md index 6348f0425..733f0d849 100644 --- a/.claude/board/LATEST_STATE.md +++ b/.claude/board/LATEST_STATE.md @@ -14,6 +14,92 @@ 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 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 + +| 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`** — 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). + > **⊘ 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 + 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 diff --git a/crates/lance-graph-contract/src/fusion.rs b/crates/lance-graph-contract/src/fusion.rs new file mode 100644 index 000000000..4f7a21424 --- /dev/null +++ b/crates/lance-graph-contract/src/fusion.rs @@ -0,0 +1,424 @@ +//! 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. +//! +//! # ⚠ 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`** 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 +//! `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. +//! +//! > **⊘ 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 +//! +//! 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/kanban.rs b/crates/lance-graph-contract/src/kanban.rs index 1b4bb2ccf..436f32030 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, RevisionVerdict}; /// The four Rubicon phases (+ two terminal exits), Libet-anchored. /// @@ -209,6 +210,67 @@ 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) **+ 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 | + /// + /// `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, 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, + } + } } /// The Libet readiness window, in µs — the `-550 ms` anchor a thinking cycle @@ -359,6 +421,178 @@ 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 + /// `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)); + } + + /// **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(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!( + 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(RevisionVerdict::unadjudicated(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] diff --git a/crates/lance-graph-contract/src/lib.rs b/crates/lance-graph-contract/src/lib.rs index 11a808456..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; @@ -156,6 +157,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..525012633 --- /dev/null +++ b/crates/lance-graph-contract/src/revision.rs @@ -0,0 +1,665 @@ +//! 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, +} + +/// 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 { + 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]); + } +}