ontology_warrant: grade the quorum, never the fact - #904
Conversation
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
Bugbot couldn't run - usage limit reachedBugbot 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) |
📝 WalkthroughWalkthroughAdded the public ChangesOntology warrant contract
Estimated code review effort: 3 (Moderate) | ~20 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
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. Comment |
There was a problem hiding this comment.
💡 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
crates/lance-graph-contract/src/ontology_warrant.rs (2)
111-121: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueTest the zero case on the integer, not the float.
speakingis converted tof32before the zero check. The comparison is exact for small integers, so behavior is correct today. Checkinghas_evidence()first states the intent directly and avoids a float equality comparison that lints flag underclippy::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 winAdd 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 oncen / (n + 1)passes the0.99clamp insideNarsTruth::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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (3)
.claude/board/LATEST_STATE.mdcrates/lance-graph-contract/src/lib.rscrates/lance-graph-contract/src/ontology_warrant.rs
| 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. |
There was a problem hiding this comment.
📐 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`.
| /// 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 | ||
| } |
There was a problem hiding this comment.
🎯 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.
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
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-elkand 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:
Quorumcarries counts,warrant()returns aNarsTruth, and there is deliberately no method turning aNarsTruthback 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:
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 becauseexpectation()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-elkwould have broken.Tests — 6, each carrying the input that would falsify it
has_evidenceobserveproven to route each verdict to its own bucketcargo test -p lance-graph-contract: 1155 passed.clippy -D warningsclean,fmtclean.Board:
LATEST_STATE.mdContract Inventory updated in the same commit.Generated by Claude Code
Summary by CodeRabbit