Module: Insika::Parity::Verdict
- Defined in:
- lib/insika/parity/verdict.rb
Overview
C6 — the mechanical fold. Given the window's pairs and the frozen criterion,
produce ONE verdict and show its arithmetic. Pure: no store, no clock beyond
the injected now, no LLM. Every number the Studio prints comes from here,
so two people reading the same pairs get the same answer (E3).
The order of the steps IS the rule:
window -> pre-registration -> buckets -> volume -> sanity -> primary -> guards
Defined Under Namespace
Constant Summary collapse
- DECIDED =
%w[better comparable worse].freeze
Class Method Summary collapse
- .fold(pairs:, criterion:, now: Time.now.utc) ⇒ Object
-
.wilson_lower(successes, n, z: 1.96) ⇒ Object
Wilson score interval, lower bound.
Class Method Details
.fold(pairs:, criterion:, now: Time.now.utc) ⇒ Object
40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/insika/parity/verdict.rb', line 40 def fold(pairs:, criterion:, now: Time.now.utc) rule = criterion.rule from = now - (rule.window_days * 86_400) windowed = Array(pairs).select { |p| in_window?(p, from, now) } checks = [] # 2. Pre-registration: a window whose pairs disagree about which file was # frozen is :invalid — a post-hoc edit produces NO verdict, not a worse one. sha = sha_check(windowed, criterion.sha) if sha && !sha.met checks << sha return build(verdict: :invalid, criterion: criterion, from: from, to: now, rule: rule, counts: nil, daily: [], per_agent: {}, checks: checks, reason: sha.note) end checks << sha if sha # 3. Buckets: decided is better+comparable+worse against the incumbent's # model (`vs: agent`). human-assisted/silent/incomplete/open/split/unknown # are counted and reported, never in the denominator. counts = count(windowed) daily = daily_of(windowed, from, now, rule) per_agent = per_agent_of(windowed) # 4. Volume: every day at the floor, and enough decided pairs in total — # short volume is :insufficient (an honest "keep running"), never a fail. volume = volume_check(daily, rule) checks << volume decided_check = Check.new(id: :min_decided, met: counts[:decided] >= rule.min_decided, actual: counts[:decided], required: rule.min_decided, note: "decided #{counts[:decided]} < #{rule.min_decided}") checks << decided_check unless volume.met && decided_check.met notes = [volume.met ? nil : volume.note, decided_check.met ? nil : decided_check.note].compact return build(verdict: :insufficient, criterion: criterion, from: from, to: now, rule: rule, counts: counts, daily: daily, per_agent: per_agent, checks: checks, reason: notes.join("; ")) end # 5. Sanity: a panel that cannot decide, or a mirror that cannot pair, # is not measuring anything -> :invalid. The denominator is the panel's # own work: judged pairs against a model — human-assisted pairs are # counted and reported, never in a denominator. judged = counts[:judged] - counts[:human_assisted] undecided_rate = judged.positive? ? (counts[:split] + counts[:unknown]).fdiv(judged) : 0.0 undecided = Check.new(id: :undecided, met: undecided_rate <= rule.undecided_rate_ceiling, actual: undecided_rate.round(4), required: "<= #{rule.undecided_rate_ceiling}", note: "undecided rate #{undecided_rate.round(3)} > #{rule.undecided_rate_ceiling} " \ "(#{counts[:split]} split, #{counts[:unknown]} unknown of #{judged} judged)") checks << undecided non_open = windowed.length - counts[:open] incomplete_rate = non_open.positive? ? counts[:incomplete].fdiv(non_open) : 0.0 incomplete = Check.new(id: :incomplete, met: incomplete_rate <= rule.incomplete_rate_ceiling, actual: incomplete_rate.round(4), required: "<= #{rule.incomplete_rate_ceiling}", note: "incomplete rate #{incomplete_rate.round(3)} > #{rule.incomplete_rate_ceiling} " \ "(#{counts[:incomplete]} of #{non_open} non-open pairs never got both halves)") checks << incomplete unless undecided.met && incomplete.met notes = [undecided.met ? nil : undecided.note, incomplete.met ? nil : incomplete.note].compact return build(verdict: :invalid, criterion: criterion, from: from, to: now, rule: rule, counts: counts, daily: daily, per_agent: per_agent, rates: { undecided: undecided_rate, incomplete: incomplete_rate }, checks: checks, reason: notes.join("; ")) end # 6. Primary: the lower bound of the 95% Wilson interval on win-or-tie. # The estimator is NAMED in the criterion, so a point estimate can never be # quietly substituted. win_or_tie = counts[:decided].positive? ? (counts[:better] + counts[:comparable]).fdiv(counts[:decided]) : 0.0 lower = wilson_lower(counts[:better] + counts[:comparable], counts[:decided]) primary = Check.new(id: :primary, met: lower >= rule.win_or_tie_floor, actual: { win_or_tie: win_or_tie.round(4), wilson_lower_95: lower.round(4) }, required: "#{rule.estimator} >= #{rule.win_or_tie_floor}", note: "win-or-tie #{win_or_tie.round(3)} (wilson lower #{lower.round(3)}) " \ "< floor #{rule.win_or_tie_floor}") checks << primary # 7. Guards: the worse tail on its own, and any store blocking its own cut. worse_rate = counts[:decided].positive? ? counts[:worse].fdiv(counts[:decided]) : 0.0 worse = Check.new(id: :worse_rate, met: worse_rate <= rule.worse_rate_ceiling, actual: worse_rate.round(4), required: "<= #{rule.worse_rate_ceiling}", note: "worse rate #{worse_rate.round(3)} > #{rule.worse_rate_ceiling} " \ "(#{counts[:worse]} worse of #{counts[:decided]} decided)") checks << worse agent = per_agent_check(per_agent, rule) checks << agent if agent failed = [primary, worse, agent].compact.reject(&:met) unless failed.empty? return build(verdict: :fail, criterion: criterion, from: from, to: now, rule: rule, counts: counts, daily: daily, per_agent: per_agent, rates: { win_or_tie: win_or_tie, lower: lower, worse: worse_rate, undecided: undecided_rate, incomplete: incomplete_rate }, checks: checks, reason: failed.map(&:note).join("; ")) end build(verdict: :pass, criterion: criterion, from: from, to: now, rule: rule, counts: counts, daily: daily, per_agent: per_agent, rates: { win_or_tie: win_or_tie, lower: lower, worse: worse_rate, undecided: undecided_rate, incomplete: incomplete_rate }, checks: checks, reason: "the cut cleared: every check in the window is green") end |
.wilson_lower(successes, n, z: 1.96) ⇒ Object
Wilson score interval, lower bound. Pure arithmetic, unit-tested against published values (n=210, k=183 -> ~0.819).
147 148 149 150 151 152 153 154 155 156 |
# File 'lib/insika/parity/verdict.rb', line 147 def wilson_lower(successes, n, z: 1.96) return 0.0 if n.to_i <= 0 p_hat = successes.to_f / n z2 = z * z denominator = 1 + (z2 / n) centre = p_hat + (z2 / (2 * n)) spread = Math.sqrt((p_hat * (1 - p_hat) + (z2 / (4 * n))) / n) (centre - (z * spread)) / denominator end |