Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,14 @@ jobs:
# .loopover.yml.example, a dependency the other two generated-artifact-drift checks don't share, so
# deliberately NOT kept uniform with them here (test/unit/ci-generated-artifact-drift-checks.test.ts
# asserts backend||ui is a SUBSET of every condition, not that all three are byte-identical).
# HOISTED above the drift checks (#9521): docs:drift-check imports the tool registry from
# @loopover/contract, so it needs the contract's dist before line-~500 where this step used to sit.
# Unconditional rather than gated on a `contract` path filter: every consumer of it (backend, mcp,
# miner, ui, and now the drift checks) can pull it into scope, and building a zod-only leaf package
# with no dependencies of its own is cheap enough that gating it would buy nothing but a class of
# skipped-build failures.
- name: Build contract package
run: npx turbo run build --filter=@loopover/contract
- name: Docs drift check
if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.engine == 'true' }}
run: npm run docs:drift-check
Expand Down Expand Up @@ -494,14 +502,8 @@ jobs:
key: turbo-code-${{ hashFiles('package-lock.json') }}-${{ github.run_id }}
restore-keys: |
turbo-code-${{ hashFiles('package-lock.json') }}-
# @loopover/contract's "types" resolve to packages/loopover-contract/dist/index.d.ts, and src/ +
# packages/loopover-mcp both import it -- so like the engine below, typecheck cannot run until it
# has been built. Unconditional rather than gated on a `contract` path filter: every consumer of it
# (backend, mcp, miner, ui) can pull it into the typecheck surface, and building a zod-only leaf
# package with no dependencies of its own is cheap enough that gating it would buy nothing but a
# class of skipped-build failures.
- name: Build contract package
run: npx turbo run build --filter=@loopover/contract
# ("Build contract package" now runs earlier in this job, above the drift checks -- see its own
# comment there. Typecheck below still depends on it, same as before, just built sooner.)
# mcp/miner are in this gate because "Typecheck" below already runs for them, and typecheck's real
# surface reaches @loopover/engine -- test/** imports it directly, and its "types" resolve to
# packages/loopover-engine/dist/index.d.ts, which only exists once this step has run. An mcp-only or
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/ui-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ jobs:
# away the result. `npx turbo run build --filter=@loopover/ui` replaces the last two `ui:build`
# steps (`extension:build && miner-extension:build && ui build` already covered by
# @loopover/ui#build's own turbo.json dependsOn) and skips ui-miner entirely.
# @loopover/contract joins the engine filter because `ui:openapi` imports src/openapi/schemas.ts,
# which re-exports the public-API response schemas from @loopover/contract/public-api (#9282/#9521).
# Without it this step failed with ERR_MODULE_NOT_FOUND on dist/public-api.js -- the contract was
# never built here at all, only in the CI workflow's own steps.
- name: Build UI
env:
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
Expand All @@ -87,7 +91,7 @@ jobs:
# production build (ui-deploy.yml) does NOT set this, so the escape hatch is dead-code-eliminated
# from prod. (#authed-route-preview)
VITE_PREVIEW: "1"
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
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

# Deploys stay best-effort when the Cloudflare secrets aren't configured (Reviewbot shows
# before-only) — mirrors ui-preview-deploy.yml's guard.
Expand Down Expand Up @@ -177,7 +181,7 @@ jobs:
env:
VITE_LOOPOVER_API_ORIGIN: https://api.loopover.ai
VITE_PREVIEW: "1"
run: npm run ui:kit:build && npx turbo run build --filter=@loopover/engine && npm run ui:openapi && npx turbo run build --filter=@loopover/ui
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

# The trusted deploy workflow downloads this by name + run-id. It contains only the built bundle
# (server/ + client/) — no secrets, no source needed downstream.
Expand Down
4 changes: 4 additions & 0 deletions apps/loopover-ui/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ bun.lock
routeTree.gen.ts
# Generated -- regenerated by npm run ui:openapi; formatting churn only creates drift-check noise.
public/openapi.json
# Generated -- regenerated by npm run mcp:tool-reference from the @loopover/contract registry (#9521).
# Same reason as openapi.json above: prettier would rewrite the emitted literal (unquoted keys, its own
# string wrapping), and then mcp:tool-reference:check would fail against a file nobody edited.
src/lib/mcp-tool-reference.ts
# Hand-authored docs content: prettier's mdx pass reformats the template-literal code inside
# <CodeBlock code={`...`}> attributes, destroying embedded YAML/compose indentation (#8182 fallout,
# repaired once already). Docs mdx is deliberately formatter-free.
Expand Down
1 change: 1 addition & 0 deletions apps/loopover-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"format:check": "prettier --check ."
},
"dependencies": {
"@loopover/contract": "^0.1.0",
"@loopover/ui-kit": ">=0.1.0 <2.0.0",
"@radix-ui/react-accordion": "^1.2.18",
"@radix-ui/react-alert-dialog": "^1.1.21",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ function renderWithClient(ui: ReactNode) {
}

const FIXTURE: PublicStats = {
// The wire always carries rulePrecision (#8230/#8231). A fixture without it is not a payload the
// current backend can produce -- the one test that needs that shape strips it explicitly.
rulePrecision: {
windowDays: 90,
rules: [],
reversals: { reopened: 0, reverted: 0, superseded: 0 },
latestBacktestRun: null,
},
generatedAt: "2026-07-20T00:00:00.000Z",
updatedAt: "2026-07-20T00:00:00.000Z",
totals: {
Expand All @@ -56,7 +64,23 @@ const FIXTURE: PublicStats = {
},
weekly: { reviewed: 10, merged: 6 },
byProject: [{ project: "owner/repo", reviewed: 100, merged: 60, closed: 30, accuracyPct: 95.5 }],
fleetAccuracy: { accuracyPct: 92, instanceCount: 4, windowDays: 90, gamingFlagsCaught: 1 },
fleetAccuracy: {
accuracyPct: 92,
// Every #8829/#9168 field the wire always carries. The fixture used to omit them, which the
// hand-typed interface allowed and the real payload never does.
accuracyCiPct: null,
mergePrecisionPct: null,
mergePrecisionCiPct: null,
closePrecisionPct: null,
closePrecisionCiPct: null,
coveragePct: null,
decidedCount: null,
guaranteed: { close: null, merge: null },
instanceCount: 4,
basis: "fleet",
windowDays: 90,
gamingFlagsCaught: 1,
},
accuracyTrend: [
{ weekStart: "2026-07-13", merged: 30, closed: 15, reversed: 1, accuracyPct: 97.8 },
],
Expand All @@ -77,8 +101,8 @@ describe("FairnessReportPage (#fairness-analytics)", () => {
rulePrecision: {
windowDays: 90,
rules: [
{ ruleId: "linked_issue_scope_mismatch", decided: 42, precision: 0.952 },
{ ruleId: "slop_gate_score", decided: 3, precision: null },
{ ruleId: "linked_issue_scope_mismatch", decided: 42, confirmed: 40, precision: 0.952 },
{ ruleId: "slop_gate_score", decided: 3, confirmed: 3, precision: null },
],
reversals: { reopened: 2, reverted: 1, superseded: 0 },
latestBacktestRun: { corpusChecksum: "a".repeat(64), at: "2026-07-22T00:00:00.000Z" },
Expand All @@ -102,7 +126,10 @@ describe("FairnessReportPage (#fairness-analytics)", () => {
});

it("hides the per-rule section entirely when the API response predates rulePrecision (deployment skew) or has no rules (#8231)", async () => {
apiFetch.mockResolvedValue({ ok: true, data: FIXTURE, durationMs: 10 });
// Deliberately NOT a PublicStats: an older deployed Worker omits the field entirely, which the current
// schema no longer describes. The cast is the point of the test -- the UI must not throw on that payload.
const { rulePrecision: _omitted, ...withoutRulePrecision } = FIXTURE;
apiFetch.mockResolvedValue({ ok: true, data: withoutRulePrecision, durationMs: 10 });
renderWithClient(<FairnessReportPage />);
await waitFor(() =>
expect(screen.getByText("Is ORB treating contributors fairly?")).toBeTruthy(),
Expand Down
115 changes: 10 additions & 105 deletions apps/loopover-ui/src/components/site/proof-of-power-stats-model.ts
Original file line number Diff line number Diff line change
@@ -1,111 +1,16 @@
import type { z } from "zod";
import type { PublicStatsSchema } from "@loopover/contract/public-api";
// Pure types + helpers for the Proof of Power (#1059) homepage stats band, split from the component so the
// component file only exports components (react-refresh) — mirrors the audit-feed / audit-feed-model split.

export type PublicStats = {
generatedAt: string;
updatedAt: string;
totals: {
handled: number;
reviewed: number;
merged: number;
closed: number;
commented: number;
ignored: number;
manual: number;
error: number;
reversed: number;
filteredPct: number | null;
accuracyPct: number | null;
minutesSaved: number;
};
weekly: { reviewed: number; merged: number };
byProject: Array<{
project: string;
reviewed: number;
merged: number;
closed: number;
accuracyPct: number | null;
}>;
/** Live, fleet-wide accuracy across registered self-hosted ORB instances -- preferred over totals.accuracyPct
* (a frozen own-ledger snapshot) whenever instanceCount > 0. See public-stats.ts's PublicStatsPayload. */
fleetAccuracy: {
accuracyPct: number | null;
/** #8829 fields -- optional-chained at the render site: an older backend simply omits them and the tile
* degrades to the bare figure rather than throwing. */
accuracyCiPct?: { lo: number; hi: number } | null;
mergePrecisionPct?: number | null;
mergePrecisionCiPct?: { lo: number; hi: number } | null;
closePrecisionPct?: number | null;
closePrecisionCiPct?: { lo: number; hi: number } | null;
coveragePct?: number | null;
decidedCount?: number;
/** #9050: `aiJudgedCoveragePct` (renamed from `coveragePct`) is the share of the arm's AI-JUDGED
* sub-population the guarantee covers, not a share of all decided signals (that's the sibling
* `coveragePct` above) -- render the population it's actually over, not a bare percentage.
* `backfilledPct` is null when the stored calibration predates that field. */
guaranteed?: {
close: {
alpha: number;
lambda: number;
aiJudgedCoveragePct: number;
n: number;
backfilledPct: number | null;
} | null;
merge: {
alpha: number;
lambda: number;
aiJudgedCoveragePct: number;
n: number;
backfilledPct: number | null;
} | null;
};
instanceCount: number;
windowDays: number;
/** #9068: null (not 0) when the fleet has fewer than GAMING_MIN_ELIGIBLE eligible instances -- the
* anti-farming detector cannot run below that floor, so a structural zero must never render as "checked,
* found none". */
gamingFlagsCaught: number | null;
};
/** Trailing weekly history of totals.accuracyPct's SAME formula (#4447). */
accuracyTrend: Array<{
weekStart: string;
merged: number;
closed: number;
reversed: number;
accuracyPct: number | null;
}>;
/** Trailing weekly "how often we avoid redoing AI work" trend (#4448). */
reuseRateTrend: Array<{
weekStart: string;
hits: number;
misses: number;
reuseRatePct: number | null;
}>;
/** Trailing weekly review-volume/filtered-rate trend (#4445 follow-up) -- each week is the cohort of PRs
* first published that week; `merged` reflects their CURRENT disposition, not necessarily merged that
* same week. */
reviewVolumeTrend: Array<{
weekStart: string;
reviewed: number;
merged: number;
filteredPct: number | null;
}>;

/** Measured per-rule precision + the reproducibility freeze point (#8230/#8231). Optional-chained by
* consumers: until the backend carrying it is deployed, an older /v1/public/stats response simply won't
* have the field yet, and every surface must degrade to hiding the section rather than throw. */
rulePrecision?: {
windowDays: number;
rules: Array<{
ruleId: string;
decided: number;
/** confirmed / decided; null below the decided-sample floor -- rendered as "insufficient data", NEVER 0%. */
precision: number | null;
}>;
reversals: { reopened: number; reverted: number; superseded: number };
latestBacktestRun: { corpusChecksum: string; at: string } | null;
};
};
// #9282/#9521: DERIVED, not hand-authored. `PublicStats` was a parallel TypeScript interface kept in sync
// with the backend by whoever last touched both files, and it had drifted -- it was missing
// `fleetAccuracy.basis` and `rulePrecision.rules[].confirmed` outright, called `decidedCount` optional where
// the wire says nullable, called `accuracyTrend`'s counts non-null where the wire says nullable, and made
// `rulePrecision` optional where the wire says required. Inferring from the schema the Worker actually
// serves makes the next such change a compile error here instead of broken rendering.
export type { PublicRulePrecision } from "@loopover/contract/public-api";
export type PublicStats = z.infer<typeof PublicStatsSchema>;

/** Relative "updated Ns ago" label from the payload's updatedAt (mirrors MetaStrip's freshness logic). */
export function formatStatsAgo(updatedAt: string | null, nowMs: number): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ import {
// Real prod proportions: reviewed 2,708 (merged 1,392 + closed 724 + commented 514 + manual 78), 33 reversals
// over 2,116 auto-actions → 98.4% accuracy; filtered (reviewed−merged)/reviewed = 48.6%; 2,708×20min ≈ 38 days.
const PAYLOAD: PublicStats = {
// The wire always carries rulePrecision (#8230/#8231). A fixture without it is not a payload the
// current backend can produce -- the one test that needs that shape strips it explicitly.
rulePrecision: {
windowDays: 90,
rules: [],
reversals: { reopened: 0, reverted: 0, superseded: 0 },
latestBacktestRun: null,
},
generatedAt: "2026-06-22T01:00:00.000Z",
updatedAt: "2026-06-22T01:00:00.000Z",
totals: {
Expand Down Expand Up @@ -73,7 +81,23 @@ const PAYLOAD: PublicStats = {
],
// No eligible registered self-hosted instances in this baseline fixture -- the tile falls back to
// totals.accuracyPct (own-ledger). A dedicated test below covers the fleet-eligible path.
fleetAccuracy: { accuracyPct: null, instanceCount: 0, windowDays: 90, gamingFlagsCaught: 0 },
fleetAccuracy: {
accuracyPct: null,
// Every #8829/#9168 field the wire always carries. The fixture used to omit them, which the
// hand-typed interface allowed and the real payload never does.
accuracyCiPct: null,
mergePrecisionPct: null,
mergePrecisionCiPct: null,
closePrecisionPct: null,
closePrecisionCiPct: null,
coveragePct: null,
decidedCount: null,
guaranteed: { close: null, merge: null },
instanceCount: 0,
basis: "single_instance_self_report",
windowDays: 90,
gamingFlagsCaught: 0,
},
accuracyTrend: [
{ weekStart: "2026-05-04", merged: 40, closed: 10, reversed: 2, accuracyPct: 96 },
{ weekStart: "2026-05-11", merged: 42, closed: 9, reversed: 1, accuracyPct: 98 },
Expand Down
Loading
Loading