Skip to content
42 changes: 42 additions & 0 deletions .claude/board/entries/2026-10-28-d-epi-cot-c3-warrant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# D-EPI-COT-C3 (first slice): tactics run only on grounded, Epi5-eligible markers

**Status:** TEST-PINNED. Code:
- `crates/lance-graph-planner/src/nars/warrant.rs`
- `epistemic_state5::RecipeRule`
- `certification::CertificationModel::measured_only`

Plan: `docs/research/epistemic-cot/c3-plan.md` §8.

## Decision

- **SCOPE:** a planner tactic (RCR 4, TR 6, CAS 8) runs only on `Marker`s. A marker is three things together:
- the ledger's pooled truth and roots;
- bits 59..63 projected through `Epi5Declarations`;
- a thought slot and generation.

Each premise must satisfy the recipe's `RecipeRule`. The rule's requires/forbids are policy pins: Associated for 4/6/7/8, and no intermediate-known for RCR.
- **Judgments** carry the tactic's truth, the proof's roots, the thought, and `Unknown × Open`. A term-logic step earns no certification. The only promotion is `pearl::reason` + `revise` over executed trial arms (pinned in the same test).
- **`measured_only`:** an unmeasured outcome is excluded, not read as negative. That includes trial arms, which `causes` reads without the universe.

## Evidence

- 9 new tests. 9 disables, each red.
- One disable (trial arms unrestricted) was green on the first fixture: every control unit was measured. An unmeasured-control fixture was added before recording.
- Planner lib 485 pass; contract lib 1561 pass.
- Clippy `--lib --tests` reports the same 8 sites as main `694b243bd`.

## Open

- Effect size, sample size, uncertainty and multiple comparisons are still unreported by `CertificationModel`.
- Missing-at-random is assumed.
- ASC and CR are not run through `fire`.
- The lab affordance law still has its own rule type.
- C3b and C3c wait on the bits 50..52 order decision and the TFLite location.

## Addendum, same day: ledger → `pearl::hydrate`

- `nars::mediator::hydrate_from_ledger` connects the ledger's grounded deductions of `A → Y` through `B` to the unchanged `pearl::hydrate`. It returns derivations, roots, the `Hydration` result, the edge and the mediated-path sensitivity.
- The ledger never creates a chain, and a path the chain does not witness leaves the edge unchanged. A chain-only path has no ledger receipt and is refused.
- `Law::Deduction` admits `Impl` in an explicit step: never closure, and `Rel` never composes.
- 5 tests; 5 disables red. The first grounding disable did not bind (no proofs exist without evidence); it was replaced before recording.
- Maturation order for new declared readings is `7 = Staunen … 0 = Wisdom`. The TFLite kernels are in ndarray.
3 changes: 2 additions & 1 deletion .claude/board/entries/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@ index row, (3) no duplicate entry id. Checks 1 and 2 are deliberately
opposite directions; the stranding this convention prevents shows up in
exactly one of them, never both.

497 entries, 2026-08-06 .. 2026-10-28.
498 entries, 2026-08-06 .. 2026-10-28.

| date | entry id | finding | file |
|---|---|---|---|
| 2026-10-28 | `thought-layers-nan-skip` | | [2026-10-28-thought-layers-nan-skip.md](2026-10-28-thought-layers-nan-skip.md) |
| 2026-10-28 | `lane-layout-from-classview` | | [2026-10-28-lane-layout-from-classview.md](2026-10-28-lane-layout-from-classview.md) |
| 2026-10-28 | `D-SAG-6-1` | | [2026-10-28-d-sag-6-1-audit-reference-ledger.md](2026-10-28-d-sag-6-1-audit-reference-ledger.md) |
| 2026-10-28 | `D-EPI-COT-C3` | | [2026-10-28-d-epi-cot-c3-warrant.md](2026-10-28-d-epi-cot-c3-warrant.md) |
| 2026-10-28 | `D-EPI-COT` | | [2026-10-28-d-epi-cot-c0-c2.md](2026-10-28-d-epi-cot-c0-c2.md) |
| 2026-10-28 | `D-DT64-6` | | [2026-10-28-d-dt64-6-routing-bindstore-behavior.md](2026-10-28-d-dt64-6-routing-bindstore-behavior.md) |
| 2026-10-28 | `D-DT64-6-1a` | | [2026-10-28-d-dt64-6-ogit-predicates-impl.md](2026-10-28-d-dt64-6-ogit-predicates-impl.md) |
Expand Down
81 changes: 81 additions & 0 deletions crates/lance-graph-contract/src/certification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,35 @@ impl<M: PopulationMask> CertificationModel<M> {
)
}

/// The model restricted to the units whose outcome was measured.
///
/// `outcome` is a positive mask: a unit outside it reads as a known
/// negative. A unit whose outcome is UNKNOWN must not read that way, so
/// before certifying, every mask (population, strata, ordering,
/// robustness, and the trial arms, which `causes` reads without the
/// universe) is intersected with `measured`. An arm left with no measured
/// unit refuses as [`NotGrounded::EmptyArm`] rather than deciding.
#[must_use]
pub fn measured_only(&self, measured: &M) -> Self {
let keep = |m: &M| m.intersection(measured);
CertificationModel {
universe: keep(&self.universe),
exposed: keep(&self.exposed),
outcome: keep(&self.outcome),
strata: [
keep(&self.strata[0]),
keep(&self.strata[1]),
keep(&self.strata[2]),
keep(&self.strata[3]),
],
ordered: keep(&self.ordered),
clean: keep(&self.clean),
trial: keep(&self.trial),
assigned: keep(&self.assigned),
..self.clone()
}
}

/// The randomized arms read as a population of their own.
pub fn trial_view(&self) -> CertificationModel<M> {
CertificationModel {
Expand Down Expand Up @@ -458,6 +487,58 @@ impl<M: PopulationMask> ModelBuilder<M> {
mod tests {
use super::*;

/// Unmeasured outcomes are not negatives. Treated arm: 4 units with the
/// outcome, 4 whose outcome was never measured. Control arm: 3 of 8.
/// Reading the unmeasured units as negatives gives 4/8 against 3/8, so
/// `causes` holds; restricted to measured units it is 4/4 against 3/8.
/// The second fixture moves the gap the other way: 2 of 8 treated
/// measured positive, 6 unmeasured; as negatives 2/8 < 3/8 refutes, as
/// unknown 2/2 > 3/8 holds.
#[test]
fn unmeasured_outcomes_are_not_read_as_negatives() {
let build = |treated_hits: u32, treated_unknown: u32| {
let mut b = ModelBuilder::new();
b.sources(SupportBasis::InterventionBacked, &[1, 2]);
let (t_known, _) = b.arm(true, treated_hits, treated_hits);
let (t_unknown, _) = b.arm(true, treated_unknown, 0);
let (c, _) = b.arm(false, 8, 3);
let measured = t_known.union(&c);
assert!(!t_unknown.is_empty() && !t_unknown.intersects(&measured));
(b.build(), measured)
};

let (m, measured) = build(2, 6);
assert_eq!(m.causes(), Ok(false), "unknowns read as negatives refute");
assert_eq!(m.measured_only(&measured).causes(), Ok(true));

let (m, measured) = build(4, 4);
assert_eq!(m.causes(), Ok(true));
assert_eq!(m.measured_only(&measured).causes(), Ok(true));

// Unknowns in the control arm: treated 1 of 4 measured, control 3 of
// 8 measured plus 8 unmeasured. Read as negatives the control rate
// is 3/16 and treated (1/4) looks higher; restricted it is 3/8 and
// treated is lower.
let mut b = ModelBuilder::new();
b.sources(SupportBasis::InterventionBacked, &[1, 2]);
let (t, _) = b.arm(true, 4, 1);
let (c_known, _) = b.arm(false, 8, 3);
let (c_unknown, _) = b.arm(false, 8, 0);
let m = b.build();
let measured = t.union(&c_known);
assert!(!c_unknown.intersects(&measured));
assert_eq!(m.causes(), Ok(true), "unknown controls read as negatives");
assert_eq!(m.measured_only(&measured).causes(), Ok(false));

// An arm with no measured unit refuses instead of deciding.
let (m, _) = build(2, 6);
let control_only = m.trial.difference(&m.assigned);
assert_eq!(
m.measured_only(&control_only).causes(),
Err(NotGrounded::EmptyArm)
);
}

/// Every fold of a model, so two models can be compared fold by fold.
fn folds<M: PopulationMask>(m: &CertificationModel<M>) -> [Sat; 6] {
[
Expand Down
86 changes: 86 additions & 0 deletions crates/lance-graph-contract/src/epistemic_state5.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,59 @@ pub const fn facts_population(required: Facts) -> Population {
population
}

/// A recipe's epistemic obligations: the facts a state must assert
/// (`requires`) and must not assert (`forbids`) for the recipe to be eligible
/// on an edge in that state.
///
/// This is the affordance law's rule, moved from the D-GSO-AFF-0 lab
/// (`cognitive-shader-driver/examples/shared/affordance_law.rs`) so a
/// production consumer can read it. Eligibility is a read-only derivation from
/// declared facts: it does not authorize execution, does not say a
/// conclusion is true, and never writes an edge.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct RecipeRule {
/// Facts every eligible state asserts.
pub requires: Facts,
/// Facts no eligible state asserts.
pub forbids: Facts,
}

impl RecipeRule {
/// A rule with these obligations.
#[must_use]
pub const fn new(requires: Facts, forbids: Facts) -> Self {
Self { requires, forbids }
}

/// Whether a state asserting `facts` satisfies the rule.
#[must_use]
pub const fn holds(self, facts: Facts) -> bool {
facts & self.requires == self.requires && facts & self.forbids == 0
}

/// Whether `state` satisfies the rule.
#[must_use]
pub const fn admits(self, state: EpistemicState5) -> bool {
self.holds(state.facts())
}

/// The V1 codes on which the rule holds. Reserved codes never appear.
#[must_use]
pub const fn population(self) -> Population {
let mut population = 0;
let mut raw = 0;
while raw < CODES {
if let Some(facts) = COMPILED_FACTS_V1[raw] {
if self.holds(facts) {
population |= 1 << raw;
}
}
raw += 1;
}
population
}
}

/// A reading generation. V1 is the 2 × 3 product above.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum Epi5Gen {
Expand Down Expand Up @@ -540,6 +593,39 @@ mod tests {
EpistemicState5::new(Epi5Gen::V1, t, c).raw()
}

/// A rule's population is exactly the meaningful codes it admits, and
/// `forbids` removes codes `requires` alone would keep.
#[test]
fn a_recipe_rule_population_matches_its_per_state_check() {
let rule = RecipeRule::new(ASSOCIATED, INTERMEDIATE_KNOWN);
let mut expected = 0;
for raw in 0u8..32 {
if let Ok(s) = EpistemicState5::decode(Epi5Gen::V1, raw) {
if rule.admits(s) {
expected |= 1 << raw;
}
}
}
assert_eq!(rule.population(), expected);
assert_eq!(
RecipeRule::new(ASSOCIATED, 0).population() & !rule.population(),
facts_population(ASSOCIATED | INTERMEDIATE_KNOWN),
"forbids removes exactly the states that assert the forbidden fact"
);
assert!(!rule.admits(EpistemicState5::new(
Epi5Gen::V1,
T::IndirectKnown,
C::Causes
)));
assert!(rule.admits(EpistemicState5::new(
Epi5Gen::V1,
T::IndirectUnknown,
C::Causes
)));
assert!(!rule.admits(EpistemicState5::new(Epi5Gen::V1, T::Direct, C::Open)));
assert_eq!(rule.population() >> 24, 0, "reserved codes never admitted");
}

/// The layout is the product: `raw5 = t | c << 2`, 24 meaningful, 8
/// reserved, and decode inverts it on every code.
#[test]
Expand Down
Loading
Loading