From 708631e550815ed348c50d6830eb10772d28ffb7 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 24 Apr 2026 02:40:08 -0400 Subject: [PATCH 1/2] =?UTF-8?q?core:=20Veridicality.antiConsensusGate=20?= =?UTF-8?q?=E2=80=94=206th=20graduation=20(10th=20ferry=20+=20SD-9=20opera?= =?UTF-8?q?tionalized)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships the independence-gate primitive from Amara's 10th ferry verbatim, operationalising the drift-taxonomy pattern-5 rule + SD-9 soft default ("agreement is signal, not proof") at the code layer. Sixth graduation under the Otto-105 cadence; composes on Veridicality.Claim + Veridicality.Provenance from PR #309 (5th graduation). Built on top of the graduation-5 branch since #309 hasn't merged to main yet; will consolidate on rebase. Surface: - Veridicality.antiConsensusGate : Claim<'T> list -> Result list, string> Returns Ok claims when the set of distinct Prov.RootAuthority values across the input has cardinality >= 2; Error otherwise. Operational intent: if 50 claims all assert the same fact but trace back to one upstream source, the 50-way agreement is still one piece of evidence. The gate rejects pseudo-consensus. Input assumed to already be ABOUT the same assertion (callers group-by canonical claim key before invoking). Canonicalization is a separate future graduation (SemanticCanonicalization from Amara's 8th-ferry rainbow-table framework). Attribution: - Aaron = concept origin (bullshit-detector framing in bootstrap conversation; pattern-5 insight) - Amara = formalization (3rd ferry drift-taxonomy pattern-5 + 10th ferry oracle-rule spec "Independence gate" row) - Otto = implementation Tests (6 new, 16 total in module, all passing): - Empty list -> Error (vacuous agreement) - Single-claim list -> Error - 50 claims from one root -> Error (pseudo-consensus) - 2 claims from 2 distinct roots -> Ok - Many claims spanning multiple roots -> Ok - Return type: Ok(claims) preserves input unchanged on pass Build: 0 Warning / 0 Error. SPOF (per Otto-106): pure function; no external deps. Callers using antiConsensusGate as a hard trust gate should pair it with RobustStats.robustAggregate when combining the resulting weights (multi-root agreement still needs outlier-resistant combination). Next graduation queue (in priority order): - SemanticCanonicalization (canonical claim keys for "same assertion" detection — the precondition for antiConsensusGate to group correctly) - scoreVeridicality (Amara's V(c) / BS(c) composite; needs ADR on 5-feature vs 7-feature factorisation) Co-Authored-By: Claude Opus 4.7 --- src/Core/Veridicality.fs | 47 ++++++++++++++ .../Algebra/Veridicality.Tests.fs | 62 +++++++++++++++++++ 2 files changed, 109 insertions(+) diff --git a/src/Core/Veridicality.fs b/src/Core/Veridicality.fs index 3985100e..a781e1d3 100644 --- a/src/Core/Veridicality.fs +++ b/src/Core/Veridicality.fs @@ -195,3 +195,50 @@ module Veridicality = | None -> [] Map.add key (c :: existing) acc) Map.empty |> Map.map (fun _ xs -> List.rev xs) + + /// **Anti-consensus gate** — claims supporting the same + /// assertion must come from at least TWO independent + /// `RootAuthority` values before they're allowed to upgrade + /// trust. Returns `Ok claims` when the set of distinct root + /// authorities across the input has cardinality >= 2; + /// `Error msg` otherwise. + /// + /// Operationalises the drift-taxonomy pattern-5 rule + /// (*"agreement is signal, not proof"* — SD-9 soft default + /// from Amara's 3rd ferry) and the 10th-ferry oracle rule + /// *"agreement from one provenance root does not upgrade + /// truth"*. + /// + /// Operational intent: if 50 claims all assert the same + /// fact but they all trace back to a single upstream source, + /// the 50-way agreement is a single piece of evidence, not + /// 50 independent pieces. The gate rejects pseudo-consensus; + /// genuine multi-root agreement passes. + /// + /// The input list is assumed to already be ABOUT the same + /// assertion (callers group-by canonical claim key before + /// invoking). The gate does NOT canonicalize; that's + /// `SemanticCanonicalization`'s job (future graduation). + /// + /// Matches Amara's 10th-ferry snippet verbatim. + /// + /// Edge cases: + /// * Empty list — zero roots, fails the gate. + /// * Single-claim list — one root, fails. + /// * Duplicate-root lists — fails unless a distinct alternate + /// root also appears. + /// + /// Provenance: Aaron's bullshit-detector framing (bootstrap + /// conversation) + Amara's 3rd-ferry drift-taxonomy pattern-5 + /// + 10th-ferry oracle-rule spec. Sixth graduation under the + /// Otto-105 cadence. + let antiConsensusGate (claims: Claim<'T> list) : Result list, string> = + let agreeingRoots = + claims + |> List.map (fun c -> c.Prov.RootAuthority) + |> Set.ofList + |> Set.count + if agreeingRoots < 2 then + Error "Agreement without independent roots" + else + Ok claims diff --git a/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs b/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs index 2e076a91..2e0316cf 100644 --- a/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs +++ b/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs @@ -186,3 +186,65 @@ let ``groupByCanonical produces distinct-root counts per bucket`` () = bucket |> List.map (fun c -> c.Prov.RootAuthority) |> Set.ofList |> Set.count distinctRoots grouped.[xKey] |> should equal 2 distinctRoots grouped.[yKey] |> should equal 1 + + +// ─── antiConsensusGate ───────── + +let private claimWithRoot (id: string) (root: string) : Veridicality.Claim = + { Id = id + Payload = 0 + Weight = 1L + Prov = { goodProv () with RootAuthority = root } } + +[] +let ``antiConsensusGate rejects empty list`` () = + match Veridicality.antiConsensusGate [] with + | Error _ -> () + | Ok _ -> failwith "expected Error for empty list" + +[] +let ``antiConsensusGate rejects a single-claim list`` () = + let claims = [ claimWithRoot "c1" "root-a" ] + match Veridicality.antiConsensusGate claims with + | Error _ -> () + | Ok _ -> failwith "expected Error for single claim" + +[] +let ``antiConsensusGate rejects many claims from a single root`` () = + // 50-way agreement from one root is still one piece of + // evidence, not 50. + let claims = + [ for i in 1 .. 50 -> claimWithRoot $"c{i}" "root-a" ] + match Veridicality.antiConsensusGate claims with + | Error msg -> msg.Contains("independent") |> should equal true + | Ok _ -> failwith "expected Error for same-root cluster" + +[] +let ``antiConsensusGate accepts two claims from two distinct roots`` () = + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" "root-b" ] + match Veridicality.antiConsensusGate claims with + | Ok returned -> returned |> should equal claims + | Error msg -> failwith $"expected Ok, got Error: {msg}" + +[] +let ``antiConsensusGate accepts many claims spanning multiple roots`` () = + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" "root-a" + claimWithRoot "c3" "root-b" + claimWithRoot "c4" "root-c" ] + match Veridicality.antiConsensusGate claims with + | Ok _ -> () + | Error msg -> failwith $"expected Ok, got Error: {msg}" + +[] +let ``antiConsensusGate returns Ok with the original list unchanged on pass`` () = + // Gate is read-only: it returns the same list it was given. + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" "root-b" ] + match Veridicality.antiConsensusGate claims with + | Ok returned -> returned |> List.length |> should equal 2 + | Error msg -> failwith msg From 4dd30182bc1225c7d8633066a00e4d19177665a7 Mon Sep 17 00:00:00 2001 From: Aaron Stainback Date: Fri, 24 Apr 2026 09:10:53 -0400 Subject: [PATCH 2/2] =?UTF-8?q?fix(#310):=202=20review=20threads=20?= =?UTF-8?q?=E2=80=94=20strip=20history-log=20doc=20+=20filter=20empty=20Ro?= =?UTF-8?q?otAuthority?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two PR-thread findings on Veridicality.fs:antiConsensusGate. Thread 1 (P1, line 158) — doc-hygiene: the `///` block introduced direct contributor/agent names and history-log content (ferry references, graduation cadence, "Attribution:" + "Provenance:" stanzas). Per the repo's standing doc-comment rule, code comments explain the code, not history. The rewritten block keeps only what the function does (inputs, returns, invariants, edge cases, composition notes) and drops the narrative. Thread 2 (P0, line 166) — real behaviour bug: the original implementation mapped every `RootAuthority` string to the distinct- root set, which meant an invalid/missing value (e.g., `""` or whitespace) would count as a distinct root and let a single-source cluster accidentally pass the anti-consensus gate. Fix: filter out `String.IsNullOrWhiteSpace` root authorities before counting. Tolerant-filter contract (degenerate input is skipped, not thrown) matches the rest of the module's semantics; callers that want strict validation should run validateProvenance first. Added three tests: - empty RootAuthority does NOT count as a distinct root - whitespace RootAuthority does NOT count as a distinct root - valid distinct roots still pass even when mixed with empty ones Tests: 26 passing (17 pre-existing + 6 original anti-consensus + 3 new empty-root coverage). Build: 0 Warning / 0 Error. Co-Authored-By: Claude Opus 4.7 --- src/Core/Veridicality.fs | 32 ++++++++--------- .../Algebra/Veridicality.Tests.fs | 36 +++++++++++++++++++ 2 files changed, 51 insertions(+), 17 deletions(-) diff --git a/src/Core/Veridicality.fs b/src/Core/Veridicality.fs index a781e1d3..c29a94ee 100644 --- a/src/Core/Veridicality.fs +++ b/src/Core/Veridicality.fs @@ -199,15 +199,9 @@ module Veridicality = /// **Anti-consensus gate** — claims supporting the same /// assertion must come from at least TWO independent /// `RootAuthority` values before they're allowed to upgrade - /// trust. Returns `Ok claims` when the set of distinct root - /// authorities across the input has cardinality >= 2; - /// `Error msg` otherwise. - /// - /// Operationalises the drift-taxonomy pattern-5 rule - /// (*"agreement is signal, not proof"* — SD-9 soft default - /// from Amara's 3rd ferry) and the 10th-ferry oracle rule - /// *"agreement from one provenance root does not upgrade - /// truth"*. + /// trust. Returns `Ok claims` when the set of distinct + /// non-empty root authorities across the input has + /// cardinality >= 2; `Error msg` otherwise. /// /// Operational intent: if 50 claims all assert the same /// fact but they all trace back to a single upstream source, @@ -217,25 +211,29 @@ module Veridicality = /// /// The input list is assumed to already be ABOUT the same /// assertion (callers group-by canonical claim key before - /// invoking). The gate does NOT canonicalize; that's - /// `SemanticCanonicalization`'s job (future graduation). + /// invoking). The gate does NOT canonicalize; that's the + /// `canonicalKey` / `groupByCanonical` pair's job. /// - /// Matches Amara's 10th-ferry snippet verbatim. + /// **Degenerate-root filter.** Empty / whitespace-only + /// `RootAuthority` values are dropped before counting — + /// they do not count as a distinct root. This matches the + /// tolerant-skip convention of the module's other + /// primitives (degenerate input is skipped rather than + /// throwing). Callers that want strict validation should + /// run `validateProvenance` first. /// /// Edge cases: /// * Empty list — zero roots, fails the gate. /// * Single-claim list — one root, fails. /// * Duplicate-root lists — fails unless a distinct alternate /// root also appears. - /// - /// Provenance: Aaron's bullshit-detector framing (bootstrap - /// conversation) + Amara's 3rd-ferry drift-taxonomy pattern-5 - /// + 10th-ferry oracle-rule spec. Sixth graduation under the - /// Otto-105 cadence. + /// * Lists whose only "second root" is empty/whitespace — + /// fails (empty root does not count). let antiConsensusGate (claims: Claim<'T> list) : Result list, string> = let agreeingRoots = claims |> List.map (fun c -> c.Prov.RootAuthority) + |> List.filter (fun r -> not (String.IsNullOrWhiteSpace r)) |> Set.ofList |> Set.count if agreeingRoots < 2 then diff --git a/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs b/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs index 2e0316cf..6858b55c 100644 --- a/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs +++ b/tests/Tests.FSharp/Algebra/Veridicality.Tests.fs @@ -248,3 +248,39 @@ let ``antiConsensusGate returns Ok with the original list unchanged on pass`` () match Veridicality.antiConsensusGate claims with | Ok returned -> returned |> List.length |> should equal 2 | Error msg -> failwith msg + +[] +let ``antiConsensusGate does NOT count empty RootAuthority as a distinct root`` () = + // Degenerate/missing RootAuthority values must be filtered + // before counting distinct roots — otherwise an empty string + // would inflate the anti-consensus count and let a single- + // source cluster pass the gate. + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" "" ] + match Veridicality.antiConsensusGate claims with + | Error _ -> () + | Ok _ -> failwith "expected Error when the only 'second root' is empty" + +[] +let ``antiConsensusGate does NOT count whitespace RootAuthority as a distinct root`` () = + // Whitespace-only RootAuthority values are treated the same + // as empty — they don't count toward the distinct-root total. + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" " " ] + match Veridicality.antiConsensusGate claims with + | Error _ -> () + | Ok _ -> failwith "expected Error when the only 'second root' is whitespace" + +[] +let ``antiConsensusGate skips empty RootAuthority but still passes on two valid roots`` () = + // Empty-root claims are silently skipped; remaining valid + // roots are counted. Two valid distinct roots → pass. + let claims = + [ claimWithRoot "c1" "root-a" + claimWithRoot "c2" "" + claimWithRoot "c3" "root-b" ] + match Veridicality.antiConsensusGate claims with + | Ok _ -> () + | Error msg -> failwith $"expected Ok (two valid distinct roots), got Error: {msg}"