Skip to content

Commit 63251f5

Browse files
Merge branch 'main' into feat/proof-page-9569
2 parents ca666b8 + 3200abc commit 63251f5

114 files changed

Lines changed: 11412 additions & 1054 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,14 @@ jobs:
352352
# .loopover.yml.example, a dependency the other two generated-artifact-drift checks don't share, so
353353
# deliberately NOT kept uniform with them here (test/unit/ci-generated-artifact-drift-checks.test.ts
354354
# asserts backend||ui is a SUBSET of every condition, not that all three are byte-identical).
355+
# HOISTED above the drift checks (#9521): docs:drift-check imports the tool registry from
356+
# @loopover/contract, so it needs the contract's dist before line-~500 where this step used to sit.
357+
# Unconditional rather than gated on a `contract` path filter: every consumer of it (backend, mcp,
358+
# miner, ui, and now the drift checks) can pull it into scope, and building a zod-only leaf package
359+
# with no dependencies of its own is cheap enough that gating it would buy nothing but a class of
360+
# skipped-build failures.
361+
- name: Build contract package
362+
run: npx turbo run build --filter=@loopover/contract
355363
- name: Docs drift check
356364
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.engine == 'true' }}
357365
run: npm run docs:drift-check
@@ -494,14 +502,8 @@ jobs:
494502
key: turbo-code-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}
495503
restore-keys: |
496504
turbo-code-${{ hashFiles('package-lock.json') }}-
497-
# @loopover/contract's "types" resolve to packages/loopover-contract/dist/index.d.ts, and src/ +
498-
# packages/loopover-mcp both import it -- so like the engine below, typecheck cannot run until it
499-
# has been built. Unconditional rather than gated on a `contract` path filter: every consumer of it
500-
# (backend, mcp, miner, ui) can pull it into the typecheck surface, and building a zod-only leaf
501-
# package with no dependencies of its own is cheap enough that gating it would buy nothing but a
502-
# class of skipped-build failures.
503-
- name: Build contract package
504-
run: npx turbo run build --filter=@loopover/contract
505+
# ("Build contract package" now runs earlier in this job, above the drift checks -- see its own
506+
# comment there. Typecheck below still depends on it, same as before, just built sooner.)
505507
# mcp/miner are in this gate because "Typecheck" below already runs for them, and typecheck's real
506508
# surface reaches @loopover/engine -- test/** imports it directly, and its "types" resolve to
507509
# packages/loopover-engine/dist/index.d.ts, which only exists once this step has run. An mcp-only or

.github/workflows/self-host-maintenance.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
# Manual maintenance hook for externally reachable self-host stacks. This is deliberately not scheduled while
22
# the review stack is running without colocated GitHub Actions runners.
33
#
4+
# #9522: this workflow remains the SCHEDULED/batch path and gains no new duties. The interactive path for the
5+
# same jobs is now the MCP tool `loopover_fleet_run_job`, which takes a `job` and `mode: enqueue|run` and
6+
# drives the identical dispatcher (src/queue/job-dispatch.ts's processJob) -- so a job run from an agent and
7+
# a job run from this matrix cannot diverge. Use this workflow for fleet-wide sweeps; use the tool to run one
8+
# job against one instance while debugging.
9+
#
410
# #4899: generalized from a single hardcoded instance to a fleet matrix. Each self-host instance is modeled
511
# as its own GitHub Environment (Settings -> Environments), carrying that instance's own `SELF_HOST_URL`
612
# environment variable and `INTERNAL_JOB_TOKEN` environment secret -- this is the supported way to give a

.github/workflows/ui-preview.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ jobs:
7979
# away the result. `npx turbo run build --filter=@loopover/ui` replaces the last two `ui:build`
8080
# steps (`extension:build && miner-extension:build && ui build` already covered by
8181
# @loopover/ui#build's own turbo.json dependsOn) and skips ui-miner entirely.
82+
# @loopover/contract joins the engine filter because `ui:openapi` imports src/openapi/schemas.ts,
83+
# which re-exports the public-API response schemas from @loopover/contract/public-api (#9282/#9521).
84+
# Without it this step failed with ERR_MODULE_NOT_FOUND on dist/public-api.js -- the contract was
85+
# never built here at all, only in the CI workflow's own steps.
8286
- name: Build UI
8387
env:
8488
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
@@ -87,7 +91,7 @@ jobs:
8791
# production build (ui-deploy.yml) does NOT set this, so the escape hatch is dead-code-eliminated
8892
# from prod. (#authed-route-preview)
8993
VITE_PREVIEW: "1"
90-
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
94+
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine --filter=@loopover/contract && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
9195

9296
# Deploys stay best-effort when the Cloudflare secrets aren't configured (Reviewbot shows
9397
# before-only) — mirrors ui-preview-deploy.yml's guard.
@@ -177,7 +181,7 @@ jobs:
177181
env:
178182
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
179183
VITE_PREVIEW: "1"
180-
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
184+
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine --filter=@loopover/contract && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
181185

182186
# The trusted deploy workflow downloads this by name + run-id. It contains only the built bundle
183187
# (server/ + client/) — no secrets, no source needed downstream.

apps/loopover-ui/.prettierignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ bun.lock
88
routeTree.gen.ts
99
# Generated -- regenerated by npm run ui:openapi; formatting churn only creates drift-check noise.
1010
public/openapi.json
11+
# Generated -- regenerated by npm run mcp:tool-reference from the @loopover/contract registry (#9521).
12+
# Same reason as openapi.json above: prettier would rewrite the emitted literal (unquoted keys, its own
13+
# string wrapping), and then mcp:tool-reference:check would fail against a file nobody edited.
14+
src/lib/mcp-tool-reference.ts
1115
# Hand-authored docs content: prettier's mdx pass reformats the template-literal code inside
1216
# <CodeBlock code={`...`}> attributes, destroying embedded YAML/compose indentation (#8182 fallout,
1317
# repaired once already). Docs mdx is deliberately formatter-free.

apps/loopover-ui/content/docs/self-hosting-operations.mdx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,31 @@ curl http://localhost:8787/ready
3333
curl http://localhost:8787/metrics`}
3434
/>
3535

36+
## Running maintenance jobs
37+
38+
Every `/v1/internal/jobs/*` maintenance job is reachable three ways, and all three run the same dispatcher —
39+
a job run one way cannot behave differently from the same job run another way.
40+
41+
- **Interactively, over MCP.** `loopover_fleet_run_job` takes a `job` name and `mode`: `enqueue` queues it
42+
for the worker, `run` executes it inline and waits. This is the path to use when debugging one job against
43+
one instance. It requires the `INTERNAL_JOB_TOKEN` credential, and answers with the supported modes if you
44+
ask for one a job does not offer (not every job has an inline runner).
45+
- **Directly, over HTTP.** `POST /v1/internal/jobs/<job>` to enqueue, `POST /v1/internal/jobs/<job>/run` to
46+
run inline — the same bearer token.
47+
- **On a schedule, fleet-wide.** The `self-host-maintenance` GitHub workflow sweeps every configured
48+
instance. That remains the batch path.
49+
50+
<CodeBlock
51+
lang="bash"
52+
code={`# enqueue one job on this instance
53+
curl -X POST http://localhost:8787/v1/internal/jobs/refresh-registry \\
54+
-H "authorization: Bearer $INTERNAL_JOB_TOKEN"
55+
56+
# run it inline and see the result
57+
curl -X POST http://localhost:8787/v1/internal/jobs/refresh-registry/run \\
58+
-H "authorization: Bearer $INTERNAL_JOB_TOKEN"`}
59+
/>
60+
3661
## Important log events
3762

3863
<CodeBlock

apps/loopover-ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"format:check": "prettier --check ."
2424
},
2525
"dependencies": {
26+
"@loopover/contract": "^0.1.0",
2627
"@loopover/ui-kit": ">=0.1.0 <2.0.0",
2728
"@radix-ui/react-accordion": "^1.2.18",
2829
"@radix-ui/react-alert-dialog": "^1.1.21",

apps/loopover-ui/src/components/site/fairness-report-page.test.tsx

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ function renderWithClient(ui: ReactNode) {
3838
}
3939

4040
const FIXTURE: PublicStats = {
41+
// The wire always carries rulePrecision (#8230/#8231). A fixture without it is not a payload the
42+
// current backend can produce -- the one test that needs that shape strips it explicitly.
43+
rulePrecision: {
44+
windowDays: 90,
45+
rules: [],
46+
reversals: { reopened: 0, reverted: 0, superseded: 0 },
47+
latestBacktestRun: null,
48+
},
4149
generatedAt: "2026-07-20T00:00:00.000Z",
4250
updatedAt: "2026-07-20T00:00:00.000Z",
4351
totals: {
@@ -56,7 +64,23 @@ const FIXTURE: PublicStats = {
5664
},
5765
weekly: { reviewed: 10, merged: 6 },
5866
byProject: [{ project: "owner/repo", reviewed: 100, merged: 60, closed: 30, accuracyPct: 95.5 }],
59-
fleetAccuracy: { accuracyPct: 92, instanceCount: 4, windowDays: 90, gamingFlagsCaught: 1 },
67+
fleetAccuracy: {
68+
accuracyPct: 92,
69+
// Every #8829/#9168 field the wire always carries. The fixture used to omit them, which the
70+
// hand-typed interface allowed and the real payload never does.
71+
accuracyCiPct: null,
72+
mergePrecisionPct: null,
73+
mergePrecisionCiPct: null,
74+
closePrecisionPct: null,
75+
closePrecisionCiPct: null,
76+
coveragePct: null,
77+
decidedCount: null,
78+
guaranteed: { close: null, merge: null },
79+
instanceCount: 4,
80+
basis: "fleet",
81+
windowDays: 90,
82+
gamingFlagsCaught: 1,
83+
},
6084
accuracyTrend: [
6185
{ weekStart: "2026-07-13", merged: 30, closed: 15, reversed: 1, accuracyPct: 97.8 },
6286
],
@@ -77,8 +101,8 @@ describe("FairnessReportPage (#fairness-analytics)", () => {
77101
rulePrecision: {
78102
windowDays: 90,
79103
rules: [
80-
{ ruleId: "linked_issue_scope_mismatch", decided: 42, precision: 0.952 },
81-
{ ruleId: "slop_gate_score", decided: 3, precision: null },
104+
{ ruleId: "linked_issue_scope_mismatch", decided: 42, confirmed: 40, precision: 0.952 },
105+
{ ruleId: "slop_gate_score", decided: 3, confirmed: 3, precision: null },
82106
],
83107
reversals: { reopened: 2, reverted: 1, superseded: 0 },
84108
latestBacktestRun: { corpusChecksum: "a".repeat(64), at: "2026-07-22T00:00:00.000Z" },
@@ -102,7 +126,10 @@ describe("FairnessReportPage (#fairness-analytics)", () => {
102126
});
103127

104128
it("hides the per-rule section entirely when the API response predates rulePrecision (deployment skew) or has no rules (#8231)", async () => {
105-
apiFetch.mockResolvedValue({ ok: true, data: FIXTURE, durationMs: 10 });
129+
// Deliberately NOT a PublicStats: an older deployed Worker omits the field entirely, which the current
130+
// schema no longer describes. The cast is the point of the test -- the UI must not throw on that payload.
131+
const { rulePrecision: _omitted, ...withoutRulePrecision } = FIXTURE;
132+
apiFetch.mockResolvedValue({ ok: true, data: withoutRulePrecision, durationMs: 10 });
106133
renderWithClient(<FairnessReportPage />);
107134
await waitFor(() =>
108135
expect(screen.getByText("Is ORB treating contributors fairly?")).toBeTruthy(),

apps/loopover-ui/src/components/site/proof-of-power-stats-model.ts

Lines changed: 10 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,111 +1,16 @@
1+
import type { z } from "zod";
2+
import type { PublicStatsSchema } from "@loopover/contract/public-api";
13
// Pure types + helpers for the Proof of Power (#1059) homepage stats band, split from the component so the
24
// component file only exports components (react-refresh) — mirrors the audit-feed / audit-feed-model split.
35

4-
export type PublicStats = {
5-
generatedAt: string;
6-
updatedAt: string;
7-
totals: {
8-
handled: number;
9-
reviewed: number;
10-
merged: number;
11-
closed: number;
12-
commented: number;
13-
ignored: number;
14-
manual: number;
15-
error: number;
16-
reversed: number;
17-
filteredPct: number | null;
18-
accuracyPct: number | null;
19-
minutesSaved: number;
20-
};
21-
weekly: { reviewed: number; merged: number };
22-
byProject: Array<{
23-
project: string;
24-
reviewed: number;
25-
merged: number;
26-
closed: number;
27-
accuracyPct: number | null;
28-
}>;
29-
/** Live, fleet-wide accuracy across registered self-hosted ORB instances -- preferred over totals.accuracyPct
30-
* (a frozen own-ledger snapshot) whenever instanceCount > 0. See public-stats.ts's PublicStatsPayload. */
31-
fleetAccuracy: {
32-
accuracyPct: number | null;
33-
/** #8829 fields -- optional-chained at the render site: an older backend simply omits them and the tile
34-
* degrades to the bare figure rather than throwing. */
35-
accuracyCiPct?: { lo: number; hi: number } | null;
36-
mergePrecisionPct?: number | null;
37-
mergePrecisionCiPct?: { lo: number; hi: number } | null;
38-
closePrecisionPct?: number | null;
39-
closePrecisionCiPct?: { lo: number; hi: number } | null;
40-
coveragePct?: number | null;
41-
decidedCount?: number;
42-
/** #9050: `aiJudgedCoveragePct` (renamed from `coveragePct`) is the share of the arm's AI-JUDGED
43-
* sub-population the guarantee covers, not a share of all decided signals (that's the sibling
44-
* `coveragePct` above) -- render the population it's actually over, not a bare percentage.
45-
* `backfilledPct` is null when the stored calibration predates that field. */
46-
guaranteed?: {
47-
close: {
48-
alpha: number;
49-
lambda: number;
50-
aiJudgedCoveragePct: number;
51-
n: number;
52-
backfilledPct: number | null;
53-
} | null;
54-
merge: {
55-
alpha: number;
56-
lambda: number;
57-
aiJudgedCoveragePct: number;
58-
n: number;
59-
backfilledPct: number | null;
60-
} | null;
61-
};
62-
instanceCount: number;
63-
windowDays: number;
64-
/** #9068: null (not 0) when the fleet has fewer than GAMING_MIN_ELIGIBLE eligible instances -- the
65-
* anti-farming detector cannot run below that floor, so a structural zero must never render as "checked,
66-
* found none". */
67-
gamingFlagsCaught: number | null;
68-
};
69-
/** Trailing weekly history of totals.accuracyPct's SAME formula (#4447). */
70-
accuracyTrend: Array<{
71-
weekStart: string;
72-
merged: number;
73-
closed: number;
74-
reversed: number;
75-
accuracyPct: number | null;
76-
}>;
77-
/** Trailing weekly "how often we avoid redoing AI work" trend (#4448). */
78-
reuseRateTrend: Array<{
79-
weekStart: string;
80-
hits: number;
81-
misses: number;
82-
reuseRatePct: number | null;
83-
}>;
84-
/** Trailing weekly review-volume/filtered-rate trend (#4445 follow-up) -- each week is the cohort of PRs
85-
* first published that week; `merged` reflects their CURRENT disposition, not necessarily merged that
86-
* same week. */
87-
reviewVolumeTrend: Array<{
88-
weekStart: string;
89-
reviewed: number;
90-
merged: number;
91-
filteredPct: number | null;
92-
}>;
93-
94-
/** Measured per-rule precision + the reproducibility freeze point (#8230/#8231). Optional-chained by
95-
* consumers: until the backend carrying it is deployed, an older /v1/public/stats response simply won't
96-
* have the field yet, and every surface must degrade to hiding the section rather than throw. */
97-
rulePrecision?: {
98-
windowDays: number;
99-
rules: Array<{
100-
ruleId: string;
101-
decided: number;
102-
/** confirmed / decided; null below the decided-sample floor -- rendered as "insufficient data", NEVER 0%. */
103-
precision: number | null;
104-
}>;
105-
reversals: { reopened: number; reverted: number; superseded: number };
106-
latestBacktestRun: { corpusChecksum: string; at: string } | null;
107-
};
108-
};
6+
// #9282/#9521: DERIVED, not hand-authored. `PublicStats` was a parallel TypeScript interface kept in sync
7+
// with the backend by whoever last touched both files, and it had drifted -- it was missing
8+
// `fleetAccuracy.basis` and `rulePrecision.rules[].confirmed` outright, called `decidedCount` optional where
9+
// the wire says nullable, called `accuracyTrend`'s counts non-null where the wire says nullable, and made
10+
// `rulePrecision` optional where the wire says required. Inferring from the schema the Worker actually
11+
// serves makes the next such change a compile error here instead of broken rendering.
12+
export type { PublicRulePrecision } from "@loopover/contract/public-api";
13+
export type PublicStats = z.infer<typeof PublicStatsSchema>;
10914

11015
/** Relative "updated Ns ago" label from the payload's updatedAt (mirrors MetaStrip's freshness logic). */
11116
export function formatStatsAgo(updatedAt: string | null, nowMs: number): string {

apps/loopover-ui/src/components/site/proof-of-power-stats.test.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,14 @@ import {
3737
// Real prod proportions: reviewed 2,708 (merged 1,392 + closed 724 + commented 514 + manual 78), 33 reversals
3838
// over 2,116 auto-actions → 98.4% accuracy; filtered (reviewed−merged)/reviewed = 48.6%; 2,708×20min ≈ 38 days.
3939
const PAYLOAD: PublicStats = {
40+
// The wire always carries rulePrecision (#8230/#8231). A fixture without it is not a payload the
41+
// current backend can produce -- the one test that needs that shape strips it explicitly.
42+
rulePrecision: {
43+
windowDays: 90,
44+
rules: [],
45+
reversals: { reopened: 0, reverted: 0, superseded: 0 },
46+
latestBacktestRun: null,
47+
},
4048
generatedAt: "2026-06-22T01:00:00.000Z",
4149
updatedAt: "2026-06-22T01:00:00.000Z",
4250
totals: {
@@ -73,7 +81,23 @@ const PAYLOAD: PublicStats = {
7381
],
7482
// No eligible registered self-hosted instances in this baseline fixture -- the tile falls back to
7583
// totals.accuracyPct (own-ledger). A dedicated test below covers the fleet-eligible path.
76-
fleetAccuracy: { accuracyPct: null, instanceCount: 0, windowDays: 90, gamingFlagsCaught: 0 },
84+
fleetAccuracy: {
85+
accuracyPct: null,
86+
// Every #8829/#9168 field the wire always carries. The fixture used to omit them, which the
87+
// hand-typed interface allowed and the real payload never does.
88+
accuracyCiPct: null,
89+
mergePrecisionPct: null,
90+
mergePrecisionCiPct: null,
91+
closePrecisionPct: null,
92+
closePrecisionCiPct: null,
93+
coveragePct: null,
94+
decidedCount: null,
95+
guaranteed: { close: null, merge: null },
96+
instanceCount: 0,
97+
basis: "single_instance_self_report",
98+
windowDays: 90,
99+
gamingFlagsCaught: 0,
100+
},
77101
accuracyTrend: [
78102
{ weekStart: "2026-05-04", merged: 40, closed: 10, reversed: 2, accuracyPct: 96 },
79103
{ weekStart: "2026-05-11", merged: 42, closed: 9, reversed: 1, accuracyPct: 98 },

0 commit comments

Comments
 (0)