Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .claude/board/EPIPHANIES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
## 2026-07-29 — E-A-GUARANTEE-WITH-A-RE-EXPORTED-BYPASS-IS-NOT-A-GUARANTEE-1 — CodeRabbit's #866 review landed AFTER the merge; three of its five findings were real and one of them broke my headline claim

**Status:** IN PR (#867). **Confidence:** High — all five findings were checked against the code; the two declined are demonstrably false positives, the three accepted are fixed with tests still green.

**Process finding first: a review that arrives after the merge still has to be worked.** CodeRabbit's #866 review (Run ID `16a75898`) was still processing when the operator merged, so the findings landed against code that was already on `main`. Nothing in the webhook stream says "you merged before the review finished" — the subscription simply ends. **When a PR merges with a review in flight, the review must be pulled explicitly**; otherwise findings against shipped code are silently dropped. Two of the three real ones here were rated Major.

**The finding that mattered: my central claim had a re-exported bypass.** The facade's headline is that stamp-collision is *structurally unreachable* — a consumer takes an `Axis`, never a `Stamp`, so independent evidence cannot silently stop pooling. But the same module **re-exported `Stamp` and `BeliefArena`**, so `BeliefArena::observe(.., Stamp::source(64))` reproduced the exact aliasing the type exists to prevent. The guarantee was true of the path I designed and false of the module I shipped. Fixed by dropping the `Stamp` re-export and making `Axis::stamp` crate-private (so the type cannot be named from outside without breaking BBB tiering), and — the part that matters more — **scoping the claim in the doc**: `PremiseBundle::arena()` still hands out a raw arena, and a consumer taking that escape hatch owns the stamp discipline. The guarantee is over the facade's path, not over every reachable call. Generalization: **an unreachability claim is a statement about the whole module surface, not about the API you meant people to use** — enumerate the escape hatches before claiming one.

**Second real finding: I violated the workspace's own litmus test.** `CLAUDE.md` states it outright — *"Does this add a free function on a carrier's state, or a method on the carrier? → Free function = reject. Method = accept."* I shipped `resolve(&bundle)` and `differential(&bundle, &throttle)`. Now `bundle.resolve()` / `bundle.differential(&throttle)`. The rule was in the file I read at session start; reading a doctrine and applying it are different acts, and a reviewer citing my own repo's rule back at me is the cheapest possible way to learn that.

**Third: domain vocabulary had leaked into a deliberately concept-blind public module** — "patient", "clinical", `DRUG`/`MARKER` test constants, and the operator's own fence-list ("no LOINC codes, disease tables, panel specifics") quoted verbatim *inside* the public crate. Naming the forbidden categories to forbid them still tells a public reader which domain this seam serves. Swept: neutral subject/trigger/observable terms, and the fence now reads "no domain catalogue of any kind ... not even in doc examples or tests". A rule against domain vocabulary cannot be written in domain vocabulary.

**Two declined, verified not assumed.** (a) "PR reference wrong — says #865, belongs to #866": line 17 reads `**Status:** IN PR.` with no number; CodeRabbit anchored on diff context from a neighbouring entry. (b) "Prepend TECH_DEBT instead of appending": `TD-LANCE-GRAPH-ALL-FEATURES-DELTA-BREAK` is at line 3, already the top entry. Both checked by reading the files rather than trusting either the tool or my memory of what I wrote.

## 2026-07-29 — E-A-COUNT-NEXT-TO-A-CLAIM-IS-READ-AS-BACKING-IT-1 — `Resolution.axes` reported the bundle's axes beside a single statement's verdict; the fix was already in the belief's own stamp

**Status:** IN PR (follow-up to merged #866). **Confidence:** High — the divergence is asserted by a test whose precondition pins that the bundle really does carry more axes than backed the winner.

**The near-miss.** `resolve()` returned `Resolution { stmt, truth, contradiction, axes }` where `axes` was `PremiseBundle::distinct_axes()` — the count of evidence axes **in the bundle**. The doc said exactly that, so nothing was false. But the field sits beside `stmt` and `truth`, and a caller reading `axes: 4` next to a resolution reads *"four axes support this"*. They diverge whenever an axis observed only OTHER statements — which for the consuming use case (a differential over rival hypotheses) is the normal case, not the corner case.

**Why "the doc is accurate" was not good enough.** A number's meaning is set by its neighbours as much as by its documentation. Placed inside a per-statement verdict struct, a bundle-wide count is *read* as per-statement support no matter what the prose says — and the direction of the error is always **overstatement**, which for a clinical consumer is the bad direction. This is the sibling of `E-COMPUTED-PRINTED-NOT-ASSERTED-1` (same day): there a claim was documented but unasserted; here a value was accurate but mis-scoped by adjacency.

**The honest number already existed.** `Belief.stamp` IS that belief's evidential base — the union of the sources that pooled into it — so `stamp.0.count_ones()` is exactly "how many axes backed THIS statement". No new bookkeeping, no second pass: the arena had been carrying the right answer all along, and the facade was computing a worse one beside it. Generalization worth keeping: **before deriving a summary statistic, check whether the substrate already records the exact thing** — a stamp, a mask, a provenance set. A recomputed approximation next to an exact record is a defect waiting for someone to trust it.

**Also pinned this round:** the tie-break. `max_by` keeps the LAST maximum, so `resolve` reverses the index comparison to prefer the earliest-observed belief on equal expectation. Nothing else in the suite has two equal-expectation beliefs, so an inverted comparator would have passed every other test; the new test asserts the winner from BOTH insertion orders of the same pair, and inverting `.then(ib.cmp(ia))` makes it fail (`s:3` where `s:1` is required). Falsifier proven, not argued.

## 2026-07-29 — E-MAKE-THE-TRAP-UNREACHABLE-NOT-DOCUMENTED-1 — the medcare reasoning seam: a facade whose main job is that a consumer cannot express the expensive mistake; plus protoc is Lance, not the lab

**Status:** IN PR. **Confidence:** High — the seam compiles and its six tests pass, including both halves of the pooling falsifier; the protoc finding is read off `cargo tree`.
Expand Down
2 changes: 1 addition & 1 deletion .claude/board/LATEST_STATE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## 2026-07-29 — branch `claude/x265-x266-plans-review-h9osnl` — `lance_graph::reasoning`, the concept-blind consumer seam

### Current Contract Inventory — new module (lance-graph core, `planner` feature)
- `lance_graph::reasoning` — the curated consumer reasoning facade, **concept-blind by construction** (`E-MAKE-THE-TRAP-UNREACHABLE-NOT-DOCUMENTED-1`). Re-exports the clinical entry points only: `TruthValue` (all five NAL operators), `BeliefArena`/`Belief`/`CStmt`/`Copula`/`Stamp`/`ReviseOutcome`, the five tactics + `Candidate`/`Frontier`/`ReasoningGap`/`GapKind`/`Throttle`, and `counterfactual::{substitute_binding, multi_substitute_binding, worlds_differ, …}`.
- `lance_graph::reasoning` — the curated consumer reasoning facade, **concept-blind by construction** (no domain vocabulary in the public crate, including doc-comments and tests) (`E-MAKE-THE-TRAP-UNREACHABLE-NOT-DOCUMENTED-1`). Re-exports the supported reasoning entry points only: `TruthValue` (all five NAL operators), `BeliefArena`/`Belief`/`CStmt`/`Copula`/`Stamp`/`ReviseOutcome`, the five tactics + `Candidate`/`Frontier`/`ReasoningGap`/`GapKind`/`Throttle`, and `counterfactual::{substitute_binding, multi_substitute_binding, worlds_differ, …}`.
- **New in the facade:** `Axis` (+ `MAX_AXES`) — one independent evidence source; takes an axis index, NOT a `Stamp`, so distinct axes yield disjoint evidence bits **by construction** and the silent stamp-collision failure (pooling degrades to CHOICE, confidence stops rising, nothing logs) cannot be expressed by a consumer. `Axis::new` refuses `index >= 64` rather than letting `Stamp::source`'s `% 64` alias axis 64 onto axis 0.
- `PremiseBundle` (owns stamp assignment) · `Resolution { stmt, truth, contradiction, axes }` · `resolve` · `differential` (returns `Frontier` so `ReasoningGap` — "what premise is MISSING to separate these" — is surfaced, not discarded).
- **`GuardRule`/`GuardViolation`/`detect_violations` — deliberately NOT inference.** No `TruthValue`, no `Belief`: a stored-value contradiction routed through the arena would become revisable and could be *softened* by later evidence. Asserted, not just documented.
Expand Down
Loading
Loading