Skip to content

ontology_warrant: grade the quorum, never the fact - #904

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/ontology-warrant
Aug 7, 2026
Merged

ontology_warrant: grade the quorum, never the fact#904
AdaWorldAPI merged 1 commit into
mainfrom
claude/ontology-warrant

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Completes the direction OGAR #253 opened: OGAR produces facts, lance-graph grades them.

The two rungs, kept apart by the type system

A factfinder (ogar-elk and siblings) answers exactly: this subsumption is entailed, or it is not. That is rung 1 — retrieval — and it must stay exact.

What genuinely is graded is a different question: how well warranted a claim is, given how many independent sources speak to it. That is computed OVER the facts, never in place of them.

The separation is structural, not conventional: Quorum carries counts, warrant() returns a NarsTruth, and there is deliberately no method turning a NarsTruth back into an entailment.

The load-bearing rule: silence is abstention, not dissent

When two independently authored ontologies are compared, a claim is corroborated, silent, or conflicting — and conflating the second with the third inverts the result.

A source with no path between two classes has not denied the relation. It has said nothing. Counting silence as dissent turns "the other ontology is sparser than this one" into "the other ontology disagrees" — the opposite finding from identical data.

Measured, not preferred. On a real cross-ontology comparison:

reading frequency
sources that both spoke: 1,730 agree : 3 conflict 99.8 %
silence (1,693) folded into dissent ~51 % — a coin flip

The regression test computes both numbers, so the difference is visible in the suite rather than asserted in a comment.

So warrant() excludes silence from frequency entirely — never inflating agreement, never manufacturing disagreement — and lets it leave the claim less attested, which is what a lower confidence already means. has_evidence() exists because expectation() alone cannot separate "nobody spoke" from "opinion split evenly": both sit at 0.5.

Zero-dep and factfinder-agnostic

The module names no ontology, no vocabulary and no producer crate — it takes three counts. Any factfinder that can bucket its comparisons can feed it, and the contract crate stays dependency-free, which importing ogar-elk would have broken.

Tests — 6, each carrying the input that would falsify it

  • silence proven not to move frequency across 500 abstentions — paired with its twin proving a conflict does (a rule ignoring everything would pass the first alone)
  • confidence proven to rise with speaker count, and never to reach certainty
  • the prior proven distinguishable from an even split only via has_evidence
  • observe proven to route each verdict to its own bucket
  • the measured case as a regression, with the inverted reading computed alongside

cargo test -p lance-graph-contract: 1155 passed. clippy -D warnings clean, fmt clean.

Board: LATEST_STATE.md Contract Inventory updated in the same commit.


Generated by Claude Code

Summary by CodeRabbit

  • New Features
    • Added quorum-based warranting for evaluating corroborating, silent, and conflicting source verdicts.
    • Added graded confidence results based on speaking-source evidence.
    • Added support for prior confidence when no source provides evidence.
    • Added clear handling of abstention as silence rather than disagreement.

OGAR's factfinders (`ogar-elk` and siblings) answer exactly: this subsumption is
entailed, or it is not. That is rung 1 — retrieval — and it must stay exact.
What genuinely IS graded is a different question: how well warranted a claim is,
given how many independent sources speak to it. This computes that OVER the
facts rather than in place of them.

The two rungs are kept apart structurally rather than by convention. `Quorum`
carries counts, `warrant()` returns a `NarsTruth`, and there is deliberately NO
method turning a `NarsTruth` back into an entailment. A caller needing the exact
answer asks the factfinder; a caller needing the warrant asks here; neither
surface can be mistaken for the other.

THE LOAD-BEARING RULE: SILENCE IS ABSTENTION, NOT DISSENT.

When two independently authored ontologies are compared a claim is corroborated,
silent, or conflicting — and conflating the second with the third INVERTS the
result. A source with no path between two classes has not denied the relation;
it has said nothing. Counting silence as dissent turns "the other ontology is
sparser than this one" into "the other ontology disagrees", which is the
opposite finding from identical data.

This is measured, not preferred. On a real cross-ontology comparison the sources
that both spoke agreed 1,730 : 3 — 99.8 % — while 1,693 were silent. Folding
silence into the dissent bucket reports ~51 %, a coin flip. The regression test
computes BOTH numbers so the difference is visible in the suite rather than
asserted in a comment.

So `warrant()` lets silence affect confidence only through what it is not: it is
excluded from frequency entirely (never inflating agreement, never manufacturing
disagreement) and simply leaves the claim less attested — which is what a lower
confidence already means. `has_evidence()` exists because `expectation()` alone
cannot separate "nobody spoke" from "opinion split evenly": both sit at 0.5.

ZERO-DEP AND FACTFINDER-AGNOSTIC. The module names no ontology, no vocabulary
and no producer crate — it takes three counts. Any factfinder that can bucket
its comparisons can feed it, and the contract crate stays dependency-free (which
importing `ogar-elk` would have broken).

Tests: 6, each carrying the input that would falsify it. Silence proven not to
move frequency across 500 abstentions, paired with its twin proving a conflict
DOES move it (a rule ignoring everything would pass the first alone).
Confidence proven to rise with speaker count and never to reach certainty. The
prior proven distinguishable from a split only via `has_evidence`. `observe`
proven to route each verdict to its own bucket. And the measured case as a
regression, with the inverted reading computed alongside it.

Board: LATEST_STATE Contract Inventory updated in the same commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
@cursor

cursor Bot commented Aug 7, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_fe4586a8-1107-42b8-9007-aeffa9e0c1b9)

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Added the public ontology_warrant module. It aggregates corroborating, silent, and conflicting source verdicts into graded NarsTruth warrants. Silence remains abstention. Tests cover aggregation, routing, priors, confidence, and cross-ontology behavior.

Changes

Ontology warrant contract

Layer / File(s) Summary
Quorum warrant aggregation
crates/lance-graph-contract/src/lib.rs, crates/lance-graph-contract/src/ontology_warrant.rs
The crate exposes ontology_warrant. Quorum and SourceVerdict support abstention-aware verdict aggregation and graded NarsTruth warrants.
Aggregation validation and contract documentation
crates/lance-graph-contract/src/ontology_warrant.rs, .claude/board/LATEST_STATE.md
Tests cover silence, conflict, confidence scaling, prior behavior, verdict routing, and cross-ontology agreement. The contract inventory documents the new module.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: claude

Poem

I’m a rabbit with warrants, neat and bright,
Silent sources stay out of the fight.
Counts join the quorum, truths take form,
Conflicts weigh in, while priors stay warm.
Hop, hop—the contract tests pass tonight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: grading quorum warrants while preserving exact fact retrieval.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7d3efc2b9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

/// it is the denominator's job to count opinions, not participants.
#[must_use]
pub const fn speaking(self) -> u16 {
self.corroborating.saturating_add(self.conflicting)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve the full speaker total in the denominator

When the valid u16 bucket counts sum above 65,535, this saturating addition undercounts the denominator and can substantially inflate agreement. For example, Quorum::new(u16::MAX, 0, u16::MAX).warrant() reports frequency 1.0 instead of 0.5. Widen both operands before adding so every representable corroborating and conflicting count contributes to the ratio.

Useful? React with 👍 / 👎.

let frequency = f32::from(self.corroborating) / speaking;
let evidence = speaking * EVIDENCE_PER_SOURCE;
let confidence = evidence / (evidence + 1.0);
NarsTruth::new(frequency, confidence)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid flattening confidence above 99 speakers

When 100 or more sources speak, passing the computed value through NarsTruth::new clamps confidence to 0.99 (exploration.rs lines 97-101). Consequently, 99 speakers and every larger quorum receive identical confidence, contradicting this API's documented n / (n + 1) calculation and preventing larger bodies of evidence from being ranked as better attested.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
crates/lance-graph-contract/src/ontology_warrant.rs (2)

111-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Test the zero case on the integer, not the float.

speaking is converted to f32 before the zero check. The comparison is exact for small integers, so behavior is correct today. Checking has_evidence() first states the intent directly and avoids a float equality comparison that lints flag under clippy::float_cmp.

♻️ Proposed refactor
     pub fn warrant(self) -> NarsTruth {
-        let speaking = f32::from(self.speaking());
-        if speaking == 0.0 {
+        if !self.has_evidence() {
             return NarsTruth::prior();
         }
+        let speaking = f32::from(self.speaking());
         let frequency = f32::from(self.corroborating) / speaking;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-contract/src/ontology_warrant.rs` around lines 111 - 121,
Update warrant to check has_evidence() before converting speaking to f32,
returning NarsTruth::prior() when no evidence exists. Then perform the existing
speaking, frequency, evidence, and confidence calculations only for the nonzero
case, removing the float equality comparison.

160-267: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a boundary test for very large speaker counts.

The six tests cover the abstention rule, conflict, confidence growth, priors, routing, and the measured case. They do not cover the upper end of the counter range. A test at the saturation boundary would lock the speaking() behavior discussed at lines 86-98 and would also record what happens once n / (n + 1) passes the 0.99 clamp inside NarsTruth::new.

#[test]
fn frequency_stays_a_true_share_at_the_top_of_the_counter_range() {
    let q = Quorum::new(60_000, 0, 10_000);
    let f = q.warrant().frequency;
    assert!((f - 0.857).abs() < 1e-3, "frequency must remain corroborating/speaking, got {f}");
}

As per coding guidelines: "Add Rust unit tests alongside implementations via #[cfg(test)] modules; prefer focused scenarios over broad integration tests".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-contract/src/ontology_warrant.rs` around lines 160 - 267,
Add a focused boundary test in the existing tests module for Quorum::speaking
and warrant frequency using Quorum::new(60_000, 0, 10_000). Assert the resulting
frequency remains approximately 0.857 within 1e-3, covering large speaker counts
and the NarsTruth::new saturation behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.claude/board/LATEST_STATE.md:
- Around line 23-27: Update the disagreement percentage in the cross-ontology
comparison description to match the documented computed value of approximately
50%, while preserving the existing measured counts and traceability reference to
`the_measured_cross_ontology_case_reads_as_agreement`.

In `@crates/lance-graph-contract/src/ontology_warrant.rs`:
- Around line 86-98: Update Warrant::speaking to return a u32 and add
corroborating and conflicting without saturation, preserving the existing u16
field types and layout. In warrant(), convert the widened speaking() result to
f32 using an explicit cast rather than f32::from, so frequency calculations use
the exact speaker count.

---

Nitpick comments:
In `@crates/lance-graph-contract/src/ontology_warrant.rs`:
- Around line 111-121: Update warrant to check has_evidence() before converting
speaking to f32, returning NarsTruth::prior() when no evidence exists. Then
perform the existing speaking, frequency, evidence, and confidence calculations
only for the nonzero case, removing the float equality comparison.
- Around line 160-267: Add a focused boundary test in the existing tests module
for Quorum::speaking and warrant frequency using Quorum::new(60_000, 0, 10_000).
Assert the resulting frequency remains approximately 0.857 within 1e-3, covering
large speaker counts and the NarsTruth::new saturation behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c0c77b0d-0a6f-4d6f-8b44-f70054aad205

📥 Commits

Reviewing files that changed from the base of the PR and between 48f5591 and e7d3efc.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (3)
  • .claude/board/LATEST_STATE.md
  • crates/lance-graph-contract/src/lib.rs
  • crates/lance-graph-contract/src/ontology_warrant.rs

Comment on lines +23 to +27
data. Measured, not preferred: on a real cross-ontology comparison the
sources that both spoke agreed 1,730 : 3 (99.8 %) while 1,693 were silent;
folding silence into dissent reports ~51 %. Both numbers are computed in
`the_measured_cross_ontology_case_reads_as_agreement` so the difference is
visible rather than asserted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Align the disagreement percentage with the module documentation.

Line 25 states that folding silence into dissent reports "~51 %". The module documentation at crates/lance-graph-contract/src/ontology_warrant.rs line 42 states "~50 %" for the same computation. The computed value is 1730 / 3426 = 50.5 %. Use one figure in both documents so the measured claim stays traceable.

📝 Proposed fix
-    sources that both spoke agreed 1,730 : 3 (99.8 %) while 1,693 were silent;
-    folding silence into dissent reports ~51 %. Both numbers are computed in
+    sources that both spoke agreed 1,730 : 3 (99.8 %) while 1,693 were silent;
+    folding silence into dissent reports ~50.5 %. Both numbers are computed in
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.claude/board/LATEST_STATE.md around lines 23 - 27, Update the disagreement
percentage in the cross-ontology comparison description to match the documented
computed value of approximately 50%, while preserving the existing measured
counts and traceability reference to
`the_measured_cross_ontology_case_reads_as_agreement`.

Comment on lines +86 to +98
/// How many sources actually asserted something. **Silence is excluded** —
/// it is the denominator's job to count opinions, not participants.
#[must_use]
pub const fn speaking(self) -> u16 {
self.corroborating.saturating_add(self.conflicting)
}

/// Whether any source spoke at all. `false` means the warrant is a prior
/// and carries no evidence — a caller must not read it as agreement.
#[must_use]
pub const fn has_evidence(self) -> bool {
self.speaking() > 0
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Saturation in speaking() can distort frequency.

speaking() returns u16. Both corroborating and conflicting are public and each can reach u16::MAX. When the sum saturates, speaking() shrinks below the real number of speakers while corroborating stays exact, so warrant() computes a frequency that is too high. NarsTruth::new clamps the result at 1.0, so the distortion is silent rather than loud.

Widen the accumulator to u32. The counts stay u16, so no field layout changes.

🐛 Proposed fix to remove the saturation path
-    #[must_use]
-    pub const fn speaking(self) -> u16 {
-        self.corroborating.saturating_add(self.conflicting)
-    }
+    #[must_use]
+    pub const fn speaking(self) -> u32 {
+        self.corroborating as u32 + self.conflicting as u32
+    }

warrant() then needs speaking as f32 instead of f32::from(...), because f32: From<u32> is not implemented.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/lance-graph-contract/src/ontology_warrant.rs` around lines 86 - 98,
Update Warrant::speaking to return a u32 and add corroborating and conflicting
without saturation, preserving the existing u16 field types and layout. In
warrant(), convert the widened speaking() result to f32 using an explicit cast
rather than f32::from, so frequency calculations use the exact speaker count.

@AdaWorldAPI
AdaWorldAPI merged commit c94db4b into main Aug 7, 2026
7 checks passed
AdaWorldAPI added a commit that referenced this pull request Aug 7, 2026
Four findings from codex + coderabbit on #904. All four were real; the first is
the one that matters.

THE SATURATING DENOMINATOR (codex P2 / coderabbit). `speaking()` returned `u16`
and used `saturating_add`. Both counts are public and each can reach
`u16::MAX`, so the sum saturates while the numerator stays exact — making
frequency too HIGH, which `NarsTruth::new` then clamps to `1.0`.
`Quorum::new(u16::MAX, 0, u16::MAX)` reported perfect agreement for a
dead-even split.

That is the worst possible failure direction for this module. Its entire
purpose is to stop a counting mistake from inflating agreement, and it shipped
with a path that inflates agreement silently. Fixed by widening the accumulator
to `u32` — the fields stay `u16`, so no layout changes — with a regression test
asserting the even split reads 0.5 and `speaking()` returns the full 131,070.

THE CONFIDENCE CEILING (codex P2). `NarsTruth::new` clamps confidence to 0.99,
and `n / (n + 1)` reaches 0.99 at n = 99, so 99 speakers and 10,000 receive
identical confidence. The clamp is the carrier's invariant — NARS confidence is
strictly below certainty, evidence never finishes arriving — so it is not
something to work around here. But the `n / (n + 1)` formula in the doc reads
as if it discriminated forever, which it does not. Now stated explicitly, with
the pointer that `speaking()` stays exact and unclamped for callers that need
to rank past that point, plus a test pinning both halves.

THE PERCENTAGE (coderabbit). Module doc said "~50 %", LATEST_STATE said "~51 %";
the computed value is 1730/3426 = 50.5 %. Both now say 50.5 %, and the module
doc gained the counts it is derived from so the figure stays traceable.

THE FLOAT ZERO-CHECK (coderabbit nitpick). `warrant()` compared a converted
`f32` against 0.0 before checking the integer. Now checks `has_evidence()`
first — states the intent directly and drops a float comparison that
`clippy::float_cmp` would flag.

coderabbit also asked for a large-speaker boundary test; that is exactly what
would have caught finding 1, and it is now in the suite.

8 tests, 1157 passing crate-wide, clippy -D warnings clean, fmt clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCGhDYoQBXs3poaR7sFuqp
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant