Skip to content

fix(review): correct risk-control calibration and the published guarantee's fleet integrity#9228

Merged
JSONbored merged 4 commits into
mainfrom
fix/9048-9050-9066-9068-risk-control-guarantee
Jul 27, 2026
Merged

fix(review): correct risk-control calibration and the published guarantee's fleet integrity#9228
JSONbored merged 4 commits into
mainfrom
fix/9048-9050-9066-9068-risk-control-guarantee

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

Four related fixes to the published statistical guarantee subsystem:

Test plan

Closes #9048
Closes #9050
Closes #9066
Closes #9068

…calibration delta (#9048, #9066)

calibrateActThreshold returned "insufficient_labels" from two branches with different
"have" semantics: a genuine label shortfall (have = total pairs) and a residual
high-confidence stratum too small to certify despite ample total labels (have = that
stratum's size). The latter now returns a distinct "no_certifiable_threshold" status
carrying totalPairs/bestN/bestLambda/bestUpperBound, rendered through its own message
and a distinct risk_control_no_certifiable_threshold audit event so the label
burn-down no longer conflates "needs labels" with "needs a better error rate".

Separately, the ascending-lambda scan reported whichever of K observed-confidence
candidates passed first without correcting for testing K of them — the advertised
1-delta confidence overstated what the scan actually delivered. Each candidate is now
tested at a Bonferroni-split delta/K, so the certified lambda is valid at the full,
originally-advertised delta regardless of which candidate passes. Chosen over a true
fixed-sequence rewrite as the smaller, safer diff against the existing ascending scan.

Also adds AND dr2.action = dal.verdict to the calibration join (latent-risk hardening:
today's data is not mis-joined, but a later HOLD/MERGE record on the same PR could
otherwise shadow the acted CLOSE record a label adjudicates) and tags each calibration
pair's provenance (backfilled vs live, via the backfill's configDigest sentinel) so a
published guarantee can later say how much of its evidence is reconstructed history.
…ction, pool decisionAccuracy (#9068)

orb_signals ingest stored any object-shaped risk_control payload verbatim, so a
registered instance with a stale or misconfigured alpha (or an outright refused
calibration) could reach the public guarantee unchecked. handleOrbIngest now runs
the payload through validateCalibrationPayload (status === "calibrated", alpha/
lambda/coverage in range, nAtLambda clearing the zero-error floor for its own
alpha/delta) before it reaches orb_risk_control_arms at all.

gamingPatternFlags compared each eligible instance against the fleet median, which
is structurally unfireable below 3 eligible instances (an instance IS the median at
n=1) and can never flag "low reversal" once the fleet's own reversal-rate median is
exactly 0 (a common, healthy-fleet case) since a fraction of zero can never be
undercut. computeFleetAnalytics now gates detection on eligible.length >= 3
(surfaced via the new gamingDetectionEligible field) and falls back to an absolute
reversal-rate floor when the fleet median is zero.

fleet.decisionAccuracy published the per-instance MEDIAN while accuracyCiPct
(public-stats.ts) is a Wilson interval over the POOLED counts — different
estimands that only coincide at equal per-instance volumes. fleet.decisionAccuracy
now publishes the pooled proportion directly (the same population the interval
describes); the per-instance median survives as the new decisionAccuracyMedian
diagnostic field.

The underlying per-instance last-writer-wins fleet-key bug this issue also
describes was already fixed by #9177 (orb_risk_control_arms is keyed per
instance_id/arm and public-stats already aggregates across registered instances at
read time) — this change covers the remaining validation-before-publish and
detection-floor gaps.
…date it before serving (#9050)

readGuarantee published "coveragePct" as if it were a share of all closes, adjacent
to a different fleetAccuracy.coveragePct that IS a share of all decided signals --
one word, two denominators. The guarantee's own field is actually the share of the
arm's AI-JUDGED sub-population the threshold covers (loadCalibrationPairs can only
join a confidence to decisions an AI-judgment blocker ran on, a minority of real
closes). Renamed to aiJudgedCoveragePct and the homepage string now names the
sub-population explicitly instead of leaving a bare percentage next to its sibling.

readGuarantee also now re-validates every stored orb_risk_control_arms row through
risk-control.ts's validateCalibrationPayload (defense in depth alongside the
ingest-side check from the companion #9068 fix) and walks all registered rows for
an arm in nAtLambda-descending order instead of trusting only the top one, so a
single malformed or stale peer can no longer hide a good row behind it.

Each calibration pair now carries whether it's backfilled (the 2026-07
calibration-corpus backfill's configDigest sentinel) or live; calibrateActThreshold
surfaces the split as backfilledPairs, and the public guarantee renders it as
backfilledPct so a guarantee resting mostly on reconstructed history says so.

Regenerated apps/loopover-ui/public/openapi.json for the schema rename/nullability.
…ions

npm run ui:lint's format:check caught unformatted lines from the #9050 fleetAccuracy.guaranteed rename in proof-of-power-stats-model.ts and its new test.
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
loopover-ui b89464d Commit Preview URL

Branch Preview URL
Jul 27 2026, 08:06 AM

@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 952 bytes (0.01%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
loopover-ui 7.43MB 952 bytes (0.01%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-C7lyb3Lm.js (New) 2.17MB 2.17MB 100.0% 🚀
assets/tanstack-vendor-CU40gYNJ.js (New) 804.04kB 804.04kB 100.0% 🚀
openapi.json 442 bytes 532.1kB 0.08%
assets/docs.fumadocs-spike-api-reference-rSxjBJgD.js (New) 442.88kB 442.88kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-BQzDzcQl.js (New) 201.71kB 201.71kB 100.0% 🚀
assets/modal-CcbYYGKq.js (New) 184.39kB 184.39kB 100.0% 🚀
assets/client-B6J6msoT.js (New) 146.06kB 146.06kB 100.0% 🚀
assets/maintainer-panel-C1ixBXGW.js (New) 79.0kB 79.0kB 100.0% 🚀
assets/routes-Q7O2gpEI.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-ChXpQI-h.js (New) 27.52kB 27.52kB 100.0% 🚀
assets/app-DDj7pzTG.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-azmdXeTB.js (New) 22.28kB 22.28kB 100.0% 🚀
assets/miner-panel-CU3d3RIa.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-CVI9vMG0.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-CsmVsrdB.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-CskkxX_i.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-B5ulGkWb.js (New) 15.37kB 15.37kB 100.0% 🚀
assets/playground-panel-BREt4XE5.js (New) 14.43kB 14.43kB 100.0% 🚀
assets/fairness-CcDfl1Sc.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-BKiMJEVB.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-D573cG3S.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-41DNlhdm.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-BBsxtfut.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-C-CEBvQU.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-CrmyLCUk.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-Dns62Sky.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-BpYRl76k.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-CsUH3Z9s.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-CLSqC3NK.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/docs.index-C6GZnSYw.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-9UDI4hEo.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-Bf8emTRf.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-BijUoLLs.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-DUH_t2T8.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-BeeIcXFZ.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-DNSrCRg3.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-DGQQYHWk.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-CSKBhYgT.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-Av4nSpuF.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-C_obDxP7.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-amI3totg.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-BeIL8gMB.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-C9roQDJc.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DuXF6ZLb.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-xZ8eROcE.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-C0NBhMyF.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-B1Px2r2P.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-BjF0xmUT.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-B9xcTrqI.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-C2CCRb5z.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-DgYL1ub5.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-DsLSESc9.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-MGihYq-8.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-C7FHMEFQ.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-BB6r6fuI.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-BgyxzvBS.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square--Y7cspS1.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-PDAGuBk_.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-iG8QjHTE.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-CSGA7_0g.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-Cok123j4.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-C9SRST_o.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-Cf9AbH3o.js (Deleted) -2.17MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-AP0TueuY.js (Deleted) -803.86kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-Dy-83ekW.js (Deleted) -442.88kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-B71bsT07.js (Deleted) -201.71kB 0 bytes -100.0% 🗑️
assets/modal-DDGlOYav.js (Deleted) -184.39kB 0 bytes -100.0% 🗑️
assets/client-CWIGjSRg.js (Deleted) -146.06kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-QeJ45iks.js (Deleted) -79.0kB 0 bytes -100.0% 🗑️
assets/routes-99LpxxrV.js (Deleted) -35.77kB 0 bytes -100.0% 🗑️
assets/owner-panel-BlvSERyf.js (Deleted) -27.52kB 0 bytes -100.0% 🗑️
assets/app-BymTp6KX.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-iVpPzx--.js (Deleted) -22.28kB 0 bytes -100.0% 🗑️
assets/miner-panel-CQznKIL6.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-Dh6jXJqL.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-DE1BL7kT.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-BC34eLhy.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-Be3UwUYx.js (Deleted) -15.37kB 0 bytes -100.0% 🗑️
assets/playground-panel-WTEQ2MdO.js (Deleted) -14.43kB 0 bytes -100.0% 🗑️
assets/fairness-D9gK2DgW.js (Deleted) -10.6kB 0 bytes -100.0% 🗑️
assets/app.audit-_CqsIBKu.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-DvSIGuYM.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-DkxBcxuc.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-BmceDscj.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-kSVMY__v.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-Canca6sX.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-DL7M2qOg.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-C11cw9oy.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-BTq-K_VF.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-UEHgM9kT.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/docs.index-D7pTVrSg.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-E1oJfyBh.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BRIu7j4W.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-Dmn22gPI.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-CY2UJeu6.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-OGI4S3_W.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-CA6183eh.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-CJWVgTGA.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-B1E9fn9Y.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-qqx3U2Bi.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog-CiLvefOA.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-Cb1p7aWP.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-B9kBQyW1.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-B7Xj69cD.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-BDdO0DHa.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-BQ1D46A_.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-Bpza_47A.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-toGLBRxx.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner--TtLeI8t.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-wjnZCqYM.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-Cm_O3LFj.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-DYixLXkU.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-CPfYPT46.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-BsG0FOqb.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-t55NB8Dm.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-Big9PXPb.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-P2nDU39B.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-BC7gEn_U.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-B3mwZfV4.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-Bj-8zF9o.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-DoJAJzxk.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-ChQ0O352.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-Ccl-znKw.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@JSONbored
JSONbored merged commit 194fa65 into main Jul 27, 2026
7 of 8 checks passed
@JSONbored
JSONbored deleted the fix/9048-9050-9066-9068-risk-control-guarantee branch July 27, 2026 08:09
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

❌ 4 Tests Failed:

Tests completed Failed Passed Skipped
22240 4 22236 21
View the top 3 failed test(s) by shortest run time
test/integration/orb-relay.test.ts > POST /v1/orb/relay/pull > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.00569s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:1160:24
test/integration/orb-onboarding.test.ts > Central Orb installation registry routes (/v1/internal/orb/installations) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.0141s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-onboarding.test.ts:57:14
test/integration/orb-ingest.test.ts > Orb instance registry routes (/v1/internal/orb/instances) > tolerates a list query that omits results (rows.results ?? [])
Stack Traces | 0.015s run time
SyntaxError: Unexpected token 'I', "Internal S"... is not valid JSON
 ❯ test/integration/orb-ingest.test.ts:427:14
test/integration/orb-relay.test.ts > POST /v1/orb/relay/register > REGRESSION (#4995): a DB error inside validateOrbRelayEnrollment's own lookup ALSO returns a clean 503 broker_error, not a framework 500 (the earlier of the two DB-touching calls in this handler)
Stack Traces | 0.0717s run time
AssertionError: expected 500 to be 503 // Object.is equality

- Expected
+ Received

- 503
+ 500

 ❯ test/integration/orb-relay.test.ts:155:24

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment