Skip to content

fix(public-stats): stop calling a one-instance sample a fleet, and withhold the pooled count at n=2 (#9168) - #9574

Merged
loopover-orb[bot] merged 1 commit into
mainfrom
fix/fleet-accuracy-small-n
Jul 28, 2026
Merged

fix(public-stats): stop calling a one-instance sample a fleet, and withhold the pooled count at n=2 (#9168)#9574
loopover-orb[bot] merged 1 commit into
mainfrom
fix/fleet-accuracy-small-n

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Closes #9168

Summary

fleetAccuracy had a per-instance volume floor (MIN_DECIDED) but nothing bounding the number of instances. At the one registered instance live today, it publishes one operator's own outcomes under fleet framing — next to a risk-control guarantee calibrated by that same instance, which invites a reader to treat one party's self-report as two independent sources.

#9228 already landed the detector half of #9168 (gamingFlagsCaught is null, not 0, below GAMING_MIN_ELIGIBLE). This is the framing half, which did not.

What changes

FLEET_FRAMING_MIN_INSTANCES, defined as GAMING_MIN_ELIGIBLE. The n at which a median becomes robust to a single bad contributor is the same n at which "this far above the median" becomes satisfiable. Pinning them together means a reader never sees fleet framing sitting next to "the detector could not run" — and neither floor can be moved without the other.

A basis discriminator on the published block: "fleet" or "single_instance_self_report". The numbers are real and stay published at every n; only the claim about what they are changes.

The pooled decidedCount is withheld at exactly 1 < instanceCount < floor. The count is a plain sum, and this deployment's own volume is already public via byProject — so at n=2 a reader recovers the other instance's decision volume by subtraction. For a hosted tenant that volume is a business metric (how many PRs they ship, how many get closed), not ours to publish.

  • At n=1 there is nothing to subtract — the sum is our own, already-public figure.
  • At n ≥ floor the sum no longer isolates any single participant.
  • Rates are safe at every n, since a proportion carries no volume. Only the count is withheld.

The median-robustness comment was wrong. analytics.ts justified the median as "robust to a single bad contributor" unconditionally. At n=1 the median is that instance's value; at n=2 it is the mean of two, which a single bad contributor moves by half its error. That claim was the reason the median was chosen, and it was doing no work at the n we actually run at. Corrected to state the n it requires.

The DB-failure fallback fails closed (fleetFramingEligible: false) with the rest of that path — if the fleet tables cannot be read, we certainly cannot claim a fleet.

OpenAPI

The spec is a hand-maintained zod mirror, so basis and the now-nullable decidedCount are added there too and apps/loopover-ui/public/openapi.json regenerated — otherwise the spec silently drifts from the response it documents.

Deliberately not decided here

Whether a hosted tenant's outcomes enter the public aggregate at all, and if so opt-in or opt-out. That is a product/privacy call, not a code one, and #9168 asks for it to be recorded before the first tenant lands.

This change is safe under either answer: it withholds the leaky figure by default rather than presuming consent. If the answer turns out to be "tenants opt in and pooled counts are fine", the floor relaxes; if it is "never", nothing further is needed.

Testing

#9168 called out that the outlier and gaming thresholds "have never been exercised against real multi-instance data". They now are — n=1, n=2, and n=3 fixtures, plus:

  • the eligible-vs-registered distinction (volume and registration both gate the floor)
  • the fail-closed path when the fleet tables are unreadable
  • a public-surface assertion that a "fleet" label always arrives with a real gamingFlagsCaught (0, not null), since the two share a floor
  • the n=1 case pinned as a regression test, since that is the shape live in production today

src/orb/analytics.ts and src/review/public-stats.ts: 0 uncovered changed lines, 0 uncovered branches.

Note on issue linkage

One issue rather than the usual bundle: the sibling ORB issues in this range are either now rescoped after verification (#9136 — see my comment there; roughly half of it was already fixed and one section is factually stale) or must land alone by their own requirement (#9541). #9068 and #8835, which #9168 cross-references, are already closed.

…thhold the pooled count at n=2 (#9168)

`fleetAccuracy` had a per-instance volume floor (MIN_DECIDED) but nothing bounding
the NUMBER of instances. At the one registered instance live today it publishes one
operator's own outcomes under fleet framing, next to a risk-control guarantee
calibrated by that same instance -- which invites a reader to treat one party's
self-report as two independent sources.

#9228 already fixed the detector half of this issue (gamingFlagsCaught is null, not
0, below GAMING_MIN_ELIGIBLE). This is the framing half, which did not land.

WHAT CHANGES

- FLEET_FRAMING_MIN_INSTANCES, deliberately defined AS GAMING_MIN_ELIGIBLE: the n at
  which a median becomes robust to a single bad contributor is the same n at which
  "this far above the median" becomes satisfiable. Pinning them together means a
  reader never sees fleet framing next to "the detector could not run".
- A `basis` discriminator on the published block: "fleet" or
  "single_instance_self_report". The numbers are real and stay published at every n --
  only the claim about what they are changes.
- The pooled `decidedCount` is withheld (null) at exactly 1 < instanceCount < floor.
  The count is a plain SUM, and this deployment's own volume is already public via
  byProject, so at n=2 a reader recovers the OTHER instance's decision volume by
  subtraction. For a hosted tenant that volume is a business metric -- how many PRs
  they ship, how many get closed -- and not ours to publish. At n=1 there is nothing
  to subtract; at n >= floor the sum no longer isolates anyone. RATES are safe at
  every n, since a proportion carries no volume, so only the count is withheld.
- The median-robustness comment in analytics.ts claimed robustness unconditionally.
  Corrected to state the n it actually requires, since that claim was the reason the
  median was chosen and it was doing no work at n=1.
- The DB-failure fallback fails closed (fleetFramingEligible: false) with the rest of
  that path -- if the fleet tables cannot be read we certainly cannot claim a fleet.

The OpenAPI schema is a hand-maintained zod mirror, so `basis` and the now-nullable
`decidedCount` are added there too, and apps/loopover-ui/public/openapi.json
regenerated -- otherwise the spec silently drifts from the response it documents.

NOT DECIDED HERE: whether a hosted tenant's outcomes should enter the public
aggregate at all, and if so opt-in or opt-out. That is a product/privacy call, not a
code one. This change is safe under either answer -- it withholds the leaky figure by
default rather than presuming consent -- and the issue records the question.

Tests: the outlier and gaming thresholds had never been exercised against
multi-instance fixtures, which #9168 called out. Now covered at n=1, n=2, n=3, with
the eligible-vs-registered distinction (volume and registration both gate the floor)
and the fail-closed path. 0 uncovered changed lines or branches.
@loopover-orb

loopover-orb Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Tip

✅ LoopOver review result - approve/merge recommended

Review updated: 2026-07-28 11:05:02 UTC

6 files · 1 AI reviewer · no blockers · readiness 100/100 · CI green · clean

✅ Suggested Action - Approve/Merge

  • safe to merge

Review summary
This PR adds a `FLEET_FRAMING_MIN_INSTANCES` floor (pinned to `GAMING_MIN_ELIGIBLE`) and a `basis` discriminator so a one-instance sample is no longer labeled a 'fleet' next to a risk-control guarantee calibrated by the same instance, and withholds the pooled `decidedCount` at exactly 2 instances to prevent recovering the other participant's volume by subtraction. I traced the `decidedCount` gating logic in public-stats.ts (`instanceCount > 1 && !fleetFramingEligible ? null : pooledVerdicts`) against the four boundary cases (n=0,1,2,3) and it matches the new tests exactly, and the openapi/zod schema updates correctly mark `decidedCount` nullable and add the required `basis` enum. The tests use real DB fixtures (registered instances + signal rows) rather than fabricated payloads, genuinely exercising the code paths they claim to cover.

Nits — 4 non-blocking
  • The UI consumer type in apps/loopover-ui/src/components/site/proof-of-power-stats-model.ts still declares `decidedCount?: number` (optional, non-null) — since that file isn't in this diff, confirm it's updated separately to `number | null` and to render the new `basis` field, or the homepage stats tile may silently mishandle the new null value.
  • src/orb/analytics.ts and src/review/public-stats.ts are already near/over the repo's 400-line file-length convention flagged by the size smells — worth a follow-up split, not blocking for this fix.
  • Confirm the UI stats component (proof-of-power-stats-model.ts and its renderer) is updated in a companion PR to type `decidedCount` as nullable and to surface `basis` in the copy, since publishing `null` without a UI fallback would just show a blank/undefined figure instead of the intended 'self-report' framing.
  • PR author also opened the linked issue — Link an issue that was opened by a different contributor, or provide a rationale for why this self-authored issue represents genuine discovery work.

Decision drivers

  • ✅ Code review — No blockers (1 reviewer)
  • ✅ Gate result — Passing (No configured blocker found.)
Context & advisory signals — never blocks the verdict
Signal Result Evidence
Linked issue ✅ Linked #9168
Related work ✅ No active overlap found No same-issue or scoped active PR overlap found.
Change scope ✅ 20/20 Low review scope from cached public metadata (1 linked issue).
Validation posture ✅ 25/25 PR body includes validation/test evidence.
Contributor workload ✅ 10/10 Author activity: 14 registered-repo PR(s), 13 merged, 309 issue(s).
Contributor context ✅ Confirmed Gittensor contributor JSONbored; Gittensor profile; 14 PR(s), 309 issue(s).
Improvement ✅ Minor risk: clean · value: minor
Linked issue satisfaction

Addressed
The PR adds FLEET_FRAMING_MIN_INSTANCES (=3) with a basis discriminator ("fleet" vs "single_instance_self_report"), withholds the pooled decidedCount at n=2 to prevent subtraction-based tenant volume leakage, corrects the median-robustness comment in analytics.ts, and adds both-arms/gaming-null/multi-instance test fixtures as required, while noting gamingFlagsCaught null-below-floor was already ha

Review context
  • Author: JSONbored
  • Role context: owner (maintainer lane)
  • Public audience mode: oss maintainer
  • Lane context: Repository is configured for direct PR review.
  • Public profile languages: Python, TypeScript, Ruby, Go, MDX, Shell, Solidity, JavaScript
  • Official Gittensor activity: 14 PR(s), 309 issue(s).
  • PR-specific overlap: none found.
Contributor next steps
  • Start here: Treat this as maintainer-lane context rather than normal contributor-lane activity.
Signal definitions
  • Related work = same linked issue, overlapping active PRs, or title/path similarity.
  • Change scope = cached public metadata such as size labels, draft state, and review-burden hints.
  • Validation posture = whether the PR provides enough public validation/test evidence for maintainer review.
  • Contributor workload = public contributor activity and cleanup pressure, not a repo-wide quality failure.
  • Contributor context = public GitHub/Gittensor identity context; non-Gittensor status is not a blocker.
🧪 Chat with LoopOver

Ask LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.

  • @loopover ask &lt;question&gt; answers contribution-quality Q&A with source citations and freshness.
  • @loopover chat &lt;question&gt; answers in natural prose from cached decision-pack facts via local inference (maintainer/collaborator; read-only).
  • A plain-language @loopover mention with a real question is routed to the closest matching read-only command automatically — no exact syntax required.

Full command reference: https://loopover.ai/docs/loopover-commands

🧪 Experimental — new and may change.

Decision record
  • action: merge · clause: success
  • config: b4617de8e12edcb5df80199c339900595b1e0bfb76375c598d158e805f6720f3 · pack: oss-anti-slop · ci: passed
  • record: d3769110b0510239d0a032c866cd4ded191f886b90bf8752119a7cb637910c14 (schema v5, head 29627ee)
Visual preview
Route Viewport Before (production) After (this PR's preview) Diff
/ desktop before /
before /
after /
after /
/ mobile before / (mobile)
before / (mobile)
after / (mobile)
after / (mobile)

Click any thumbnail to open the full-size screenshot. Before = production · After = this PR's preview deploy.

Scroll preview
Route Before (production) After (this PR's preview)
/ before / (scroll)
before / (scroll)
after / (scroll)
after / (scroll)

A short scroll-through clip (desktop) — click either thumbnail to open the full animation. Evidence for scroll-linked behavior a single screenshot can't show.

🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed


💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →.

Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.

  • Re-run LoopOver review

@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 29627ee Commit Preview URL

Branch Preview URL
Jul 28 2026, 10:40 AM

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Bundle Report

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

Detailed changes
Bundle name Size Change
loopover-ui 7.66MB 349 bytes (0.0%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: loopover-ui

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/add-scalar-classes-CzsZjBp5.js (New) 2.16MB 2.16MB 100.0% 🚀
assets/tanstack-vendor-hPfft2uo.js (New) 866.09kB 866.09kB 100.0% 🚀
openapi.json 252 bytes 630.89kB 0.04%
assets/docs.fumadocs-spike-api-reference-DHav1n6d.js (New) 443.45kB 443.45kB 100.0% 🚀
assets/AgentScalarChatInterface.vue-DFDx2O-b.js (New) 201.7kB 201.7kB 100.0% 🚀
assets/modal-8qWQUoIw.js (New) 184.5kB 184.5kB 100.0% 🚀
assets/client-DmBLXNTN.js (New) 151.47kB 151.47kB 100.0% 🚀
assets/maintainer-panel-Bsu8XqZS.js (New) 78.99kB 78.99kB 100.0% 🚀
assets/routes-Cd37ugs0.js (New) 35.96kB 35.96kB 100.0% 🚀
assets/owner-panel-DZ3CdbSu.js (New) 27.92kB 27.92kB 100.0% 🚀
assets/app-DzVGOipY.js (New) 25.78kB 25.78kB 100.0% 🚀
assets/ui-vendor-BAHx250m.js (New) 24.57kB 24.57kB 100.0% 🚀
assets/miner-panel-BWK8-jZu.js (New) 20.24kB 20.24kB 100.0% 🚀
assets/app.runs-z_xaFKMf.js (New) 20.22kB 20.22kB 100.0% 🚀
assets/api._op-z-wjf_bn.js (New) 17.57kB 17.57kB 100.0% 🚀
assets/self-hosting-docs-audit-B5B_yM6S.js (New) 16.6kB 16.6kB 100.0% 🚀
assets/docs._slug-Did43G6J.js (New) 15.52kB 15.52kB 100.0% 🚀
assets/playground-panel-Be8Qq7cF.js (New) 14.42kB 14.42kB 100.0% 🚀
assets/fairness-Gssv-L_I.js (New) 10.73kB 10.73kB 100.0% 🚀
assets/app.audit-bJWPBjpj.js (New) 10.08kB 10.08kB 100.0% 🚀
assets/app.config-generator-OrpLyF70.js (New) 10.06kB 10.06kB 100.0% 🚀
assets/maintainers-BwwyBuso.js (New) 8.06kB 8.06kB 100.0% 🚀
assets/miners-D9abaPSS.js (New) 7.91kB 7.91kB 100.0% 🚀
assets/agents-Cbko_XPo.js (New) 7.74kB 7.74kB 100.0% 🚀
assets/commands-panel-hzW2QsD1.js (New) 6.65kB 6.65kB 100.0% 🚀
assets/maintainer-workflow-CDtNg06P.js (New) 6.52kB 6.52kB 100.0% 🚀
assets/digest-panel-CGbdDTAN.js (New) 6.15kB 6.15kB 100.0% 🚀
assets/repos._owner._repo.quality-BkeU3YDt.js (New) 6.14kB 6.14kB 100.0% 🚀
assets/docs-nav-CqMI2HEk.js (New) 6.01kB 6.01kB 100.0% 🚀
assets/docs.index-DsKXlk90.js (New) 5.95kB 5.95kB 100.0% 🚀
assets/api.index-BUGpIFMe.js (New) 4.7kB 4.7kB 100.0% 🚀
assets/docs-9p-EgvPH.js (New) 2.7kB 2.7kB 100.0% 🚀
assets/api-DLvc8siE.js (New) 2.69kB 2.69kB 100.0% 🚀
assets/docs-page-6ltADNVN.js (New) 2.1kB 2.1kB 100.0% 🚀
assets/table-Bt2a1yvO.js (New) 1.75kB 1.75kB 100.0% 🚀
assets/app.workbench-FzOV4FmA.js (New) 1.58kB 1.58kB 100.0% 🚀
assets/tabs-D9WqkRhd.js (New) 1.39kB 1.39kB 100.0% 🚀
assets/app.repos-hJVqMI59.js (New) 1.07kB 1.07kB 100.0% 🚀
assets/input-BBVu6eB7.js (New) 796 bytes 796 bytes 100.0% 🚀
assets/file-cog-CFtQ2eQQ.js (New) 758 bytes 758 bytes 100.0% 🚀
assets/app.maintainer-CF-pfgPl.js (New) 502 bytes 502 bytes 100.0% 🚀
assets/app.owner-BVqT76ea.js (New) 474 bytes 474 bytes 100.0% 🚀
assets/app.commands-D9HW4azP.js (New) 455 bytes 455 bytes 100.0% 🚀
assets/app.playground-DinkEwKf.js (New) 442 bytes 442 bytes 100.0% 🚀
assets/index-DAoJyv1a.js (New) 438 bytes 438 bytes 100.0% 🚀
assets/app.digest-CmjfHjtL.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/eye-off-0F2NgXk-.js (New) 430 bytes 430 bytes 100.0% 🚀
assets/app.miner-SVapCkQ9.js (New) 422 bytes 422 bytes 100.0% 🚀
assets/key-round-D1BvCWlX.js (New) 355 bytes 355 bytes 100.0% 🚀
assets/bot-9knDs4Xx.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/trash-2-C2jfgChb.js (New) 328 bytes 328 bytes 100.0% 🚀
assets/save-D34r8_wp.js (New) 327 bytes 327 bytes 100.0% 🚀
assets/git-pull-request-arrow-CVMzKg7L.js (New) 321 bytes 321 bytes 100.0% 🚀
assets/list-checks-D-sBWpDk.js (New) 279 bytes 279 bytes 100.0% 🚀
assets/compass-KliiXcC6.js (New) 251 bytes 251 bytes 100.0% 🚀
assets/history-HQeJWkuG.js (New) 237 bytes 237 bytes 100.0% 🚀
assets/message-square-GlAmpI37.js (New) 233 bytes 233 bytes 100.0% 🚀
assets/lock-BG-cmgYH.js (New) 206 bytes 206 bytes 100.0% 🚀
assets/rotate-cw-BS6c1Sbv.js (New) 201 bytes 201 bytes 100.0% 🚀
assets/play-B7zBtKkU.js (New) 190 bytes 190 bytes 100.0% 🚀
assets/circle-check-s8b4d_Z5.js (New) 178 bytes 178 bytes 100.0% 🚀
assets/search-W96kUT38.js (New) 174 bytes 174 bytes 100.0% 🚀
assets/add-scalar-classes-CYbNOnEV.js (Deleted) -2.16MB 0 bytes -100.0% 🗑️
assets/tanstack-vendor-BsErCrLz.js (Deleted) -865.99kB 0 bytes -100.0% 🗑️
assets/docs.fumadocs-spike-api-reference-BkBDEKKn.js (Deleted) -443.45kB 0 bytes -100.0% 🗑️
assets/AgentScalarChatInterface.vue-CxTEgKqL.js (Deleted) -201.7kB 0 bytes -100.0% 🗑️
assets/modal-BvuP0842.js (Deleted) -184.5kB 0 bytes -100.0% 🗑️
assets/client-CWA8Dz3J.js (Deleted) -151.47kB 0 bytes -100.0% 🗑️
assets/maintainer-panel-C9A52DqO.js (Deleted) -78.99kB 0 bytes -100.0% 🗑️
assets/routes-ChWzi9jO.js (Deleted) -35.96kB 0 bytes -100.0% 🗑️
assets/owner-panel-KQjjR-qB.js (Deleted) -27.92kB 0 bytes -100.0% 🗑️
assets/app-CU0RVvq3.js (Deleted) -25.78kB 0 bytes -100.0% 🗑️
assets/ui-vendor-CiHqZdx2.js (Deleted) -24.57kB 0 bytes -100.0% 🗑️
assets/miner-panel-zVh86H-x.js (Deleted) -20.24kB 0 bytes -100.0% 🗑️
assets/app.runs-DTeORplD.js (Deleted) -20.22kB 0 bytes -100.0% 🗑️
assets/api._op-CYFHqa3X.js (Deleted) -17.57kB 0 bytes -100.0% 🗑️
assets/self-hosting-docs-audit-CXX-MeUM.js (Deleted) -16.6kB 0 bytes -100.0% 🗑️
assets/docs._slug-D2lZRmDP.js (Deleted) -15.52kB 0 bytes -100.0% 🗑️
assets/playground-panel-BZgaH6Fn.js (Deleted) -14.42kB 0 bytes -100.0% 🗑️
assets/fairness-RIGQh-lF.js (Deleted) -10.73kB 0 bytes -100.0% 🗑️
assets/app.audit-CQj1083V.js (Deleted) -10.08kB 0 bytes -100.0% 🗑️
assets/app.config-generator-7TnvYiB2.js (Deleted) -10.06kB 0 bytes -100.0% 🗑️
assets/maintainers-ippVh3Rk.js (Deleted) -8.06kB 0 bytes -100.0% 🗑️
assets/miners-DZ2M_rKC.js (Deleted) -7.91kB 0 bytes -100.0% 🗑️
assets/agents-B-rMxOua.js (Deleted) -7.74kB 0 bytes -100.0% 🗑️
assets/commands-panel-BHE4Dykx.js (Deleted) -6.65kB 0 bytes -100.0% 🗑️
assets/maintainer-workflow-BzHjvhZf.js (Deleted) -6.52kB 0 bytes -100.0% 🗑️
assets/digest-panel-VrcC8sLb.js (Deleted) -6.15kB 0 bytes -100.0% 🗑️
assets/repos._owner._repo.quality-p6sYIACO.js (Deleted) -6.14kB 0 bytes -100.0% 🗑️
assets/docs-nav-D25tLdpt.js (Deleted) -6.01kB 0 bytes -100.0% 🗑️
assets/docs.index-cKCECan_.js (Deleted) -5.95kB 0 bytes -100.0% 🗑️
assets/api.index-BNCAzI-M.js (Deleted) -4.7kB 0 bytes -100.0% 🗑️
assets/docs-BvW1pryo.js (Deleted) -2.7kB 0 bytes -100.0% 🗑️
assets/api-BBCWgGvI.js (Deleted) -2.69kB 0 bytes -100.0% 🗑️
assets/docs-page-Csc2ZXGf.js (Deleted) -2.1kB 0 bytes -100.0% 🗑️
assets/table-D4OBK90G.js (Deleted) -1.75kB 0 bytes -100.0% 🗑️
assets/app.workbench-DXLIrxat.js (Deleted) -1.58kB 0 bytes -100.0% 🗑️
assets/tabs-DFSJx3CQ.js (Deleted) -1.39kB 0 bytes -100.0% 🗑️
assets/app.repos-CSST0zel.js (Deleted) -1.07kB 0 bytes -100.0% 🗑️
assets/input-ifCg_i-4.js (Deleted) -796 bytes 0 bytes -100.0% 🗑️
assets/file-cog--tq5fJQE.js (Deleted) -758 bytes 0 bytes -100.0% 🗑️
assets/app.maintainer-DyzBBi-o.js (Deleted) -502 bytes 0 bytes -100.0% 🗑️
assets/app.owner-CSoKFq9Q.js (Deleted) -474 bytes 0 bytes -100.0% 🗑️
assets/app.commands-BI2cMhD5.js (Deleted) -455 bytes 0 bytes -100.0% 🗑️
assets/app.playground-BtMUETVn.js (Deleted) -442 bytes 0 bytes -100.0% 🗑️
assets/index-D41Z91lm.js (Deleted) -438 bytes 0 bytes -100.0% 🗑️
assets/app.digest-NpEADLZY.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/eye-off-ePXc5djW.js (Deleted) -430 bytes 0 bytes -100.0% 🗑️
assets/app.miner-Bj3LSN04.js (Deleted) -422 bytes 0 bytes -100.0% 🗑️
assets/key-round-k3MkIdYu.js (Deleted) -355 bytes 0 bytes -100.0% 🗑️
assets/bot-CmRLSQ-t.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/trash-2-BJKhTP0s.js (Deleted) -328 bytes 0 bytes -100.0% 🗑️
assets/save-Bpl7X6ye.js (Deleted) -327 bytes 0 bytes -100.0% 🗑️
assets/git-pull-request-arrow-C4y8z_Fy.js (Deleted) -321 bytes 0 bytes -100.0% 🗑️
assets/list-checks-CDy3kTwz.js (Deleted) -279 bytes 0 bytes -100.0% 🗑️
assets/compass-C1UB-JHW.js (Deleted) -251 bytes 0 bytes -100.0% 🗑️
assets/history-aanmfgPj.js (Deleted) -237 bytes 0 bytes -100.0% 🗑️
assets/message-square-Wwhx1Zac.js (Deleted) -233 bytes 0 bytes -100.0% 🗑️
assets/lock-SjTvHtWN.js (Deleted) -206 bytes 0 bytes -100.0% 🗑️
assets/rotate-cw-DqkSAjRd.js (Deleted) -201 bytes 0 bytes -100.0% 🗑️
assets/play-BSjUoCsC.js (Deleted) -190 bytes 0 bytes -100.0% 🗑️
assets/circle-check-B3jz2g3l.js (Deleted) -178 bytes 0 bytes -100.0% 🗑️
assets/search-C3VG5bua.js (Deleted) -174 bytes 0 bytes -100.0% 🗑️

@codecov

codecov Bot commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 88.73%. Comparing base (f221acf) to head (29627ee).
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #9574      +/-   ##
==========================================
- Coverage   89.62%   88.73%   -0.90%     
==========================================
  Files         868      868              
  Lines      110873   110876       +3     
  Branches    26360    26362       +2     
==========================================
- Hits        99371    98383     -988     
- Misses      10237    11519    +1282     
+ Partials     1265      974     -291     
Flag Coverage Δ
backend 93.71% <100.00%> (-1.62%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/openapi/schemas.ts 100.00% <ø> (ø)
src/orb/analytics.ts 100.00% <100.00%> (ø)
src/review/public-stats.ts 97.27% <100.00%> (+0.05%) ⬆️

... and 3 files with indirect coverage changes

@loopover-orb loopover-orb Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LoopOver approves — the gate is satisfied and CI is green.

@loopover-orb
loopover-orb Bot merged commit 3bffb87 into main Jul 28, 2026
11 checks passed
@loopover-orb
loopover-orb Bot deleted the fix/fleet-accuracy-small-n branch July 28, 2026 11:05
JSONbored added a commit that referenced this pull request Jul 28, 2026
…d shape #9565 added

Two rebase follow-ups after #9565 and #9574 landed.

1. scripts/actionlint.ts gets its `.ts` extension back. This PR had removed it to satisfy
   check-import-specifiers, which broke the script outright -- it runs under
   `node --experimental-strip-types`, whose ESM resolver does no extension resolution, so
   the process dies at startup with ERR_MODULE_NOT_FOUND. #9565 independently reached the
   same conclusion and added TYPE_STRIPPED_ENTRYPOINTS to the checker for exactly this
   file, so the extension is now permitted where it is required. Verified by running
   `npm run actionlint`, which fails before this change and passes after.

   #9565's version of the checker is taken wholesale over this PR's: it solves the same
   two problems (that entrypoint set, plus allowlisting check-dead-source-files-script.test.ts
   for its string fixtures), and re-litigating a file main just rewrote buys nothing.

2. src/mcp/server.ts's `loginRepoPullShape` is removed -- dead on arrival in #9565, and
   the first thing the newly-enabled noUnusedLocals caught on main. Which is the point of
   this PR: dead code now surfaces at the commit that introduces it rather than at the next
   audit.

The engine bump lands at 3.16.1 (main released 3.16.0 while this was open). It is required
by check-engine-parity: this PR removes two dead TYPE-only imports from
packages/loopover-engine/src/advisory/gate-advisory.ts, and the parity contract holds that
file in lockstep with its host twin src/rules/advisory.ts. There is no matching host edit to
make -- the engine copy is a deliberately slimmed re-implementation (#4881) omitting the
functions that use those types -- so the version bump is the sanctioned way to record a
one-sided change. No behaviour change: type-only imports are erased at compile time.
packages/loopover-miner/expected-engine.version moves with it, as its own check requires.
JSONbored added a commit that referenced this pull request Jul 28, 2026
…d shape #9565 added

Two rebase follow-ups after #9565 and #9574 landed.

1. scripts/actionlint.ts gets its `.ts` extension back. This PR had removed it to satisfy
   check-import-specifiers, which broke the script outright -- it runs under
   `node --experimental-strip-types`, whose ESM resolver does no extension resolution, so
   the process dies at startup with ERR_MODULE_NOT_FOUND. #9565 independently reached the
   same conclusion and added TYPE_STRIPPED_ENTRYPOINTS to the checker for exactly this
   file, so the extension is now permitted where it is required. Verified by running
   `npm run actionlint`, which fails before this change and passes after.

   #9565's version of the checker is taken wholesale over this PR's: it solves the same
   two problems (that entrypoint set, plus allowlisting check-dead-source-files-script.test.ts
   for its string fixtures), and re-litigating a file main just rewrote buys nothing.

2. src/mcp/server.ts's `loginRepoPullShape` is removed -- dead on arrival in #9565, and
   the first thing the newly-enabled noUnusedLocals caught on main. Which is the point of
   this PR: dead code now surfaces at the commit that introduces it rather than at the next
   audit.

The engine bump lands at 3.16.1 (main released 3.16.0 while this was open). It is required
by check-engine-parity: this PR removes two dead TYPE-only imports from
packages/loopover-engine/src/advisory/gate-advisory.ts, and the parity contract holds that
file in lockstep with its host twin src/rules/advisory.ts. There is no matching host edit to
make -- the engine copy is a deliberately slimmed re-implementation (#4881) omitting the
functions that use those types -- so the version bump is the sanctioned way to record a
one-sided change. No behaviour change: type-only imports are erased at compile time.
packages/loopover-miner/expected-engine.version moves with it, as its own check requires.
JSONbored added a commit that referenced this pull request Jul 28, 2026
…#9553, #9570, #9571, #9572) (#9573)

* build(typescript): enable noUnusedLocals/noUnusedParameters repo-wide (#9553)

Dead code is the substrate every drift bug in the 2026-07-27 audit grew on: a stale
import or an orphaned constant reads exactly like a live wire, so the next person
greps, finds it, and reasons about a code path that no longer runs.

Enabling the flags made the compiler enumerate all 515 instances. Every one in
src/** and packages/** was traced to its replacement before deletion -- all 82 were
genuine supersession leftovers, no behaviour bug hiding among them -- but the triage
turned up three real problems that were invisible under the noise:

1. src/queue/processors.ts -- sweepRepoBacklogConvergence accepted `requestedBy`
   ("schedule" | "api" | "test") and dropped it. Every sibling sweep stamps it into
   recordAuditEvent's metadata; both agent.sweep.backlog_convergence events here
   omitted it, so those records could not be attributed to a schedule vs a manual API
   trigger. Now wired into both. (Note the mechanical fix would have been to rename it
   `_requestedBy`, which cements the gap instead of closing it.)

2. test/unit/openapi.test.ts -- the #9302 REST<->MCP parity guard asserted against
   src/mcp/server.ts's gatePrecisionOutputSchema and maintainerMeasurementReportOutputSchema,
   which the tools stopped registering when #9518 moved their outputs to
   @loopover/contract. The shapes are still identical, so nothing had drifted YET --
   but the guard was watching objects no runtime reads and would not have caught a
   future contract change. Re-anchored onto GetGatePrecisionOutput.shape /
   GetOutcomeCalibrationOutput.shape, which is what the tools actually register, and
   what that file's own header comment already claimed it did.

3. src/github/resolve-command.ts was reachable only from its own test, because
   src/review/review-memory-wire.ts carried a SECOND byte-identical copy of
   normalizeResolveFindingRef (regex included) and that copy was the one production
   used. Two independent implementations of the same public-safety validation, free to
   drift. Deduped onto the original via re-export; dead-source-files:check now passes
   on a file it was about to start failing on.

Also corrects packages/loopover-engine/src/scoring/preview.ts's header, which claimed
a ReDoS guarantee via a hasUnsafeWildcardCount import that had been dead since
625e236 deduped its label matching onto label-match.ts. The guarantee is real and
unchanged; it now arrives through labelMatchesPattern, and the comment says so.

Pre-existing import-specifier violations fixed in the same pass, since the tree has to
be green for the flags to mean anything:
- scripts/actionlint.ts imported a `.ts` specifier (TS5097)
- test/unit/contract-registry.test.ts had three `.js` specifiers in a Bundler zone
- check-dead-source-files-script.test.ts tripped check-import-specifiers on its own
  string FIXTURES, the same self-referential false positive that checker's
  ALLOWED_FILENAMES already documents for its own test

Mechanics: unused parameters are renamed with a leading underscore, never deleted --
they are positional, so removing one silently re-binds every later argument. Everything
else was removed by its real TypeScript AST node span (a regex pass was tried first and
mis-bounded declarations badly enough to produce unparseable files).

Full suite green: 23,894 passed, 0 failed. tsc clean with the flags on.

* chore(engine): bump to 3.15.4 for the dead type-import removal in gate-advisory.ts

check-engine-parity holds the two hand-duplicated gate-decision twins
(packages/loopover-engine/src/advisory/gate-advisory.ts and src/rules/advisory.ts)
in lockstep: touching one without the other requires an engine version bump. That is
the mechanism, and it is doing its job here.

the engine twin. They are genuinely dead there and NOT in the host: the engine copy is
a deliberately slimmed re-implementation (#4881) that omits buildIssueAdvisory /
addIssueFindings / collisionClustersForPull, which are what use those types on the host
side. So there is no matching host edit to make -- the asymmetry is correct, and the
version bump is the sanctioned way to record it.

No behaviour change: type-only imports are erased at compile time. The bump exists so
the parity contract stays enforceable, not because the gate decides anything
differently. packages/loopover-miner/expected-engine.version moves in lockstep, as its
own check requires.

* fix(scripts,mcp): restore actionlint's `.ts` specifier and drop a dead shape #9565 added

Two rebase follow-ups after #9565 and #9574 landed.

1. scripts/actionlint.ts gets its `.ts` extension back. This PR had removed it to satisfy
   check-import-specifiers, which broke the script outright -- it runs under
   `node --experimental-strip-types`, whose ESM resolver does no extension resolution, so
   the process dies at startup with ERR_MODULE_NOT_FOUND. #9565 independently reached the
   same conclusion and added TYPE_STRIPPED_ENTRYPOINTS to the checker for exactly this
   file, so the extension is now permitted where it is required. Verified by running
   `npm run actionlint`, which fails before this change and passes after.

   #9565's version of the checker is taken wholesale over this PR's: it solves the same
   two problems (that entrypoint set, plus allowlisting check-dead-source-files-script.test.ts
   for its string fixtures), and re-litigating a file main just rewrote buys nothing.

2. src/mcp/server.ts's `loginRepoPullShape` is removed -- dead on arrival in #9565, and
   the first thing the newly-enabled noUnusedLocals caught on main. Which is the point of
   this PR: dead code now surfaces at the commit that introduces it rather than at the next
   audit.

The engine bump lands at 3.16.1 (main released 3.16.0 while this was open). It is required
by check-engine-parity: this PR removes two dead TYPE-only imports from
packages/loopover-engine/src/advisory/gate-advisory.ts, and the parity contract holds that
file in lockstep with its host twin src/rules/advisory.ts. There is no matching host edit to
make -- the engine copy is a deliberately slimmed re-implementation (#4881) omitting the
functions that use those types -- so the version bump is the sanctioned way to record a
one-sided change. No behaviour change: type-only imports are erased at compile time.
packages/loopover-miner/expected-engine.version moves with it, as its own check requires.

* chore(release): sync .release-please-manifest.json to the 3.16.1 engine bump

The manifest is a generated artifact that must move with any package.json version, and
release-manifest:sync:check fails CI when it drifts. Regenerated with the repo's own
`npm run release-manifest:sync` rather than hand-edited.

* chore: prune the dead symbols the new flags caught in newly-merged code

Rebase onto main after #9579 and #9580 landed. The newly-enabled noUnusedLocals
immediately flagged twelve dead symbols in code merged since this PR opened -- including
two I left in #9580 myself:

  - src/queue/processors.ts: PrCommandPrologueOutcome imported but unused (the adapter's
    annotated return type was dropped in favour of inference), plus eight unused bindings
    in the prologue destructures -- handlers that do not need `pr`, `settings`,
    `authorization` or `command` were still pulling them out.
  - src/queue/pr-command-prologue.ts: LoopOverMentionCommandName, superseded by
    LoopOverActionCommandName once the spec narrowed to action verbs.
  - src/mcp/dispatch-telemetry-sink.ts: an unused McpToolCallTelemetry import from #9579.

Which is the point of the PR: the flags catch dead code at the commit that introduces it
rather than at the next audit. Zero behaviour change -- every removal is a binding or an
import TypeScript proved unreferenced, and the suite passes 24,014 tests.

The rebase conflict itself was in processors.ts's import block: main added the
pr-command-prologue import on the same lines this PR removed the unused runRetentionPrune
one. Both intents kept.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…can deploy

Cloudflare Workers Builds has failed on every recent commit, which is the
deploy path for loopover-ui — no GitHub Actions workflow runs `wrangler deploy`
(ui-deploy.yml only validates). Production has consequently been serving stale
code: #9574's `fleetAccuracy.basis` and #9594's anchor-payload auth exemption
both landed on main and neither is live.

The build command is `npm run build:cloudflare` -> `npm ci && npm run ui:build`.
`ui:build` runs `turbo run build --filter=@loopover/engine`, then `ui:openapi`,
whose `scripts/write-ui-openapi.ts` reaches `src/openapi/schemas.ts` and imports
`@loopover/contract/dist/public-api.js`. Nothing in that chain builds
`@loopover/contract` — `@loopover/engine` does not depend on it — so on a clean
checkout the import resolves to a directory that was never emitted:

  Error [ERR_MODULE_NOT_FOUND]: Cannot find module
    '/opt/buildhome/repo/node_modules/@loopover/contract/dist/public-api.js'
    imported from /opt/buildhome/repo/src/openapi/schemas.ts

It passes in `test:ci` only by accident of ordering: `npm run typecheck` runs
first, and the `//#typecheck` turbo task already carries an explicit
`@loopover/contract#build` edge — added for exactly this failure mode, and
documented in turbo.json as "the root package.json dependency does NOT create a
build edge for a root task". Cloudflare runs `ui:build` on its own, with no
typecheck ahead of it, so the incidental ordering never applies.

Adding the same edge to `ui:build`'s own turbo invocation, which is the level
that actually owns the dependency.

Verified against a genuinely clean tree (package dists and .tsbuildinfo files
removed, TURBO_FORCE=1 to defeat the shared worktree cache — the first two
attempts at reproducing this were masked by a stale tsbuildinfo and then by a
turbo cache replay): without this change `ui:build` exits 1 on the error above;
with it, exit 0 and both dist artifacts are emitted.
JSONbored added a commit that referenced this pull request Jul 29, 2026
…checkout can deploy

Cloudflare Workers Builds is the deploy path for BOTH workers — no GitHub
Actions workflow runs `wrangler deploy` (ui-deploy.yml only validates) — and
both have failed on every recent commit. Production has consequently been
serving stale code: #9574's `fleetAccuracy.basis` and #9594's anchor-payload
auth exemption both landed on main and neither is live.

One root cause, two scripts: nothing builds `@loopover/contract` before
something needs its `dist/`. `@loopover/engine` does not depend on it, so
building engine alone leaves it unemitted.

loopover-ui — `npm run ui:build` runs `turbo run build --filter=@loopover/engine`,
then `ui:openapi`, whose `scripts/write-ui-openapi.ts` reaches
`src/openapi/schemas.ts` and imports `@loopover/contract/dist/public-api.js`:

  Error [ERR_MODULE_NOT_FOUND]: Cannot find module
    '/opt/buildhome/repo/node_modules/@loopover/contract/dist/public-api.js'

loopover-api — `npm run deploy:api` builds only `@loopover/engine`, then
`wrangler deploy` bundles `src/index.ts`, which reaches `@loopover/contract`
from src/mcp/*, src/openapi/schemas.ts and src/orb/broker-client.ts:

  ✘ [ERROR] Build failed with 8 errors:
    ✘ [ERROR] Could not resolve "@loopover/contract"

Both pass in `test:ci` only by accident of ordering: `npm run typecheck` runs
first, and the `//#typecheck` turbo task already carries an explicit
`@loopover/contract#build` edge — added for exactly this failure mode, and
documented in turbo.json as "the root package.json dependency does NOT create a
build edge for a root task". Neither deploy command runs a typecheck ahead of
itself, so the incidental ordering never applies.

Adding the same edge to each deploy path's own turbo invocation, which is the
level that actually owns the dependency. `deploy:api` also moves from a bare
`npm --workspace ... run build` to turbo so both paths express it identically.

Verified against a genuinely clean tree (package dists and .tsbuildinfo files
removed, TURBO_FORCE=1 to defeat the shared worktree cache — the first two
reproduction attempts were masked by a stale tsbuildinfo and then by a turbo
cache replay). ui:build: exit 1 -> exit 0. wrangler deploy --dry-run: 8
unresolved "@loopover/contract" imports -> 0, exit 0.
loopover-orb Bot pushed a commit that referenced this pull request Jul 29, 2026
…checkout can deploy (#9732)

Cloudflare Workers Builds is the deploy path for BOTH workers — no GitHub
Actions workflow runs `wrangler deploy` (ui-deploy.yml only validates) — and
both have failed on every recent commit. Production has consequently been
serving stale code: #9574's `fleetAccuracy.basis` and #9594's anchor-payload
auth exemption both landed on main and neither is live.

One root cause, two scripts: nothing builds `@loopover/contract` before
something needs its `dist/`. `@loopover/engine` does not depend on it, so
building engine alone leaves it unemitted.

loopover-ui — `npm run ui:build` runs `turbo run build --filter=@loopover/engine`,
then `ui:openapi`, whose `scripts/write-ui-openapi.ts` reaches
`src/openapi/schemas.ts` and imports `@loopover/contract/dist/public-api.js`:

  Error [ERR_MODULE_NOT_FOUND]: Cannot find module
    '/opt/buildhome/repo/node_modules/@loopover/contract/dist/public-api.js'

loopover-api — `npm run deploy:api` builds only `@loopover/engine`, then
`wrangler deploy` bundles `src/index.ts`, which reaches `@loopover/contract`
from src/mcp/*, src/openapi/schemas.ts and src/orb/broker-client.ts:

  ✘ [ERROR] Build failed with 8 errors:
    ✘ [ERROR] Could not resolve "@loopover/contract"

Both pass in `test:ci` only by accident of ordering: `npm run typecheck` runs
first, and the `//#typecheck` turbo task already carries an explicit
`@loopover/contract#build` edge — added for exactly this failure mode, and
documented in turbo.json as "the root package.json dependency does NOT create a
build edge for a root task". Neither deploy command runs a typecheck ahead of
itself, so the incidental ordering never applies.

Adding the same edge to each deploy path's own turbo invocation, which is the
level that actually owns the dependency. `deploy:api` also moves from a bare
`npm --workspace ... run build` to turbo so both paths express it identically.

Verified against a genuinely clean tree (package dists and .tsbuildinfo files
removed, TURBO_FORCE=1 to defeat the shared worktree cache — the first two
reproduction attempts were masked by a stale tsbuildinfo and then by a turbo
cache replay). ui:build: exit 1 -> exit 0. wrangler deploy --dry-run: 8
unresolved "@loopover/contract" imports -> 0, exit 0.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant