-
Notifications
You must be signed in to change notification settings - Fork 1
core: Veridicality.antiConsensusGate — 6th graduation (10th ferry + SD-9 operationalized) #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -195,3 +195,48 @@ 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 | ||
| /// 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, | ||
| /// 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 the | ||
| /// `canonicalKey` / `groupByCanonical` pair's job. | ||
| /// | ||
| /// **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. | ||
| /// * Lists whose only "second root" is empty/whitespace — | ||
| /// fails (empty root does not count). | ||
| let antiConsensusGate (claims: Claim<'T> list) : Result<Claim<'T> list, string> = | ||
| let agreeingRoots = | ||
| claims | ||
| |> List.map (fun c -> c.Prov.RootAuthority) | ||
| |> List.filter (fun r -> not (String.IsNullOrWhiteSpace r)) | ||
| |> Set.ofList | ||
|
AceHack marked this conversation as resolved.
Comment on lines
+236
to
+237
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The gate drops whitespace-only roots but does not normalize non-empty values before Useful? React with 👍 / 👎. |
||
| |> Set.count | ||
|
AceHack marked this conversation as resolved.
|
||
| if agreeingRoots < 2 then | ||
| Error "Agreement without independent roots" | ||
|
AceHack marked this conversation as resolved.
|
||
| else | ||
| Ok claims | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -186,3 +186,101 @@ 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<int> = | ||||||
| { Id = id | ||||||
| Payload = 0 | ||||||
| Weight = 1L | ||||||
| Prov = { goodProv () with RootAuthority = root } } | ||||||
|
|
||||||
| [<Fact>] | ||||||
|
Comment on lines
+191
to
+199
|
||||||
| let ``antiConsensusGate rejects empty list`` () = | ||||||
| match Veridicality.antiConsensusGate [] with | ||||||
| | Error _ -> () | ||||||
| | Ok _ -> failwith "expected Error for empty list" | ||||||
|
|
||||||
| [<Fact>] | ||||||
| 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" | ||||||
|
|
||||||
| [<Fact>] | ||||||
| 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" | ||||||
|
|
||||||
| [<Fact>] | ||||||
| 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}" | ||||||
|
|
||||||
| [<Fact>] | ||||||
| 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}" | ||||||
|
|
||||||
| [<Fact>] | ||||||
| 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 | ||||||
|
||||||
| | Ok returned -> returned |> List.length |> should equal 2 | |
| | Ok returned -> returned |> should equal claims |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
antiConsensusGatecountsRootAuthorityfrom every claim, butClaim.Weightexplicitly distinguishes assertions (> 0) from retractions (< 0). As written, one positive claim fromroot-aplus one retracting claim fromroot-bwill pass the gate, even though only one root is actually supporting the assertion. This can let raw claim ledgers (that include retractions) incorrectly upgrade trust; the root count should be computed from supporting/net-positive evidence only.Useful? React with 👍 / 👎.