You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The backend already builds a real, runtime-validating OpenAPI contract from zod schemas
(src/openapi/schemas.ts, generated via @asteasolutions/zod-to-openapi in src/openapi/spec.ts) — but
nothing on the UI side ever reads it. Every API response shape the UI consumes (PublicStats in apps/loopover-ui/src/components/site/proof-of-power-stats-model.ts and its siblings across apps/loopover-ui/src/components/site/*) is a hand-authored TypeScript interface, manually kept in sync
with the backend by whoever last touched both files. This is exactly the failure class the "hand-maintained,
not derived" pattern always eventually produces: a backend field renamed/removed/retyped silently stops
matching its UI-side duplicate, and nothing catches it until a user sees broken rendering — surfaced directly
by #9266's own confirmed field addition, which had to be added to a hand-typed interface with no
compiler or CI signal that it was now out of sync with reality.
Decision needed: which mechanism, and it should be decided here, not re-litigated per PR
Two real options, both used across the TS ecosystem for exactly this problem:
Share the zod schemas directly, z.infer on the UI side. Zero codegen, zero new dependency — the
UI imports the same zod schema objects (via a shared package subpath, mirroring the @loopover/engine/calibration/attester precedent already established in scripts/attested-backtest-run.ts)
and derives its types with z.infer<typeof schema>. A backend schema change becomes a compile error on
the UI side immediately, with no separate generation step to remember to re-run. Requires relocating (or
re-exporting) src/openapi/schemas.ts's schemas somewhere both the Worker app (src/) and the UI app
(apps/loopover-ui) can import — they're separate workspace packages today, and the Worker's own src/
isn't set up as an importable package for siblings.
Generate types from the emitted openapi.json via openapi-typescript. Works across the workspace
boundary without relocating any source (the built openapi.json — apps/loopover-ui/public/openapi.json
— already exists as a build artifact both apps can reach). Adds a real codegen step and a new dependency;
the generated .d.ts needs the same staleness-check treatment cf-typegen:check/ui:openapi:check
already give other generated artifacts in this repo, or it silently drifts exactly like the hand-typed
interfaces do today.
Recommendation: option 1. This is a single monorepo with an existing subpath-export precedent for
exactly this kind of cross-package sharing; it needs no new dependency, no generation step to forget to
re-run, and a schema change fails the UI's own typecheck immediately rather than waiting for a runtime
drift check to catch it on the next CI run. Record the final decision on this issue before implementing —
if there's a reason option 2 is actually preferable (e.g. a real need to consume the OpenAPI contract from
somewhere that isn't a same-monorepo TS package), that reason belongs here, not rediscovered mid-PR.
A drift check, matching this repo's own established convention (cf-typegen:check, ui:openapi:check, import-specifiers:check, etc.) — wired into test:ci so a future hand-typed
duplicate reappearing anywhere in apps/loopover-ui/src/components/site/* fails CI, not just this one
pilot surface regressing back to hand-maintenance.
Do not attempt a big-bang migration of every remaining hand-typed API surface in this issue. Once the
pilot + drift check exist, migrating the rest is incremental, opportunistic work (touch a file, migrate
it) — the drift check is what actually prevents the problem from recurring, not a one-time rewrite.
Deliverables
Decision record on this issue
PublicStats/PublicRulePrecision migrated to the derived-type mechanism
Drift check wired into test:ci
Expected outcome
A backend response-shape change fails fast (compiler or CI), on the pilot surface today and on every
surface migrated after — instead of silently drifting until a user notices broken UI.
maintainer-only — cross-package API contract infrastructure.
Context
The backend already builds a real, runtime-validating OpenAPI contract from zod schemas
(
src/openapi/schemas.ts, generated via@asteasolutions/zod-to-openapiinsrc/openapi/spec.ts) — butnothing on the UI side ever reads it. Every API response shape the UI consumes (
PublicStatsinapps/loopover-ui/src/components/site/proof-of-power-stats-model.tsand its siblings acrossapps/loopover-ui/src/components/site/*) is a hand-authored TypeScript interface, manually kept in syncwith the backend by whoever last touched both files. This is exactly the failure class the "hand-maintained,
not derived" pattern always eventually produces: a backend field renamed/removed/retyped silently stops
matching its UI-side duplicate, and nothing catches it until a user sees broken rendering — surfaced directly
by #9266's own
confirmedfield addition, which had to be added to a hand-typed interface with nocompiler or CI signal that it was now out of sync with reality.
Decision needed: which mechanism, and it should be decided here, not re-litigated per PR
Two real options, both used across the TS ecosystem for exactly this problem:
z.inferon the UI side. Zero codegen, zero new dependency — theUI imports the same zod schema objects (via a shared package subpath, mirroring the
@loopover/engine/calibration/attesterprecedent already established inscripts/attested-backtest-run.ts)and derives its types with
z.infer<typeof schema>. A backend schema change becomes a compile error onthe UI side immediately, with no separate generation step to remember to re-run. Requires relocating (or
re-exporting)
src/openapi/schemas.ts's schemas somewhere both the Worker app (src/) and the UI app(
apps/loopover-ui) can import — they're separate workspace packages today, and the Worker's ownsrc/isn't set up as an importable package for siblings.
openapi.jsonviaopenapi-typescript. Works across the workspaceboundary without relocating any source (the built
openapi.json—apps/loopover-ui/public/openapi.json— already exists as a build artifact both apps can reach). Adds a real codegen step and a new dependency;
the generated
.d.tsneeds the same staleness-check treatmentcf-typegen:check/ui:openapi:checkalready give other generated artifacts in this repo, or it silently drifts exactly like the hand-typed
interfaces do today.
Recommendation: option 1. This is a single monorepo with an existing subpath-export precedent for
exactly this kind of cross-package sharing; it needs no new dependency, no generation step to forget to
re-run, and a schema change fails the UI's own typecheck immediately rather than waiting for a runtime
drift check to catch it on the next CI run. Record the final decision on this issue before implementing —
if there's a reason option 2 is actually preferable (e.g. a real need to consume the OpenAPI contract from
somewhere that isn't a same-monorepo TS package), that reason belongs here, not rediscovered mid-PR.
Requirements
PublicStats/PublicRulePrecision(proof-of-power-stats-model.ts,already touched by eval-interface: serve GET /v1/public/eval-scores — the v1 transport #9215 committed to #9266) — prove the approach end to end before touching anything else.
cf-typegen:check,ui:openapi:check,import-specifiers:check, etc.) — wired intotest:ciso a future hand-typedduplicate reappearing anywhere in
apps/loopover-ui/src/components/site/*fails CI, not just this onepilot surface regressing back to hand-maintenance.
pilot + drift check exist, migrating the rest is incremental, opportunistic work (touch a file, migrate
it) — the drift check is what actually prevents the problem from recurring, not a one-time rewrite.
Deliverables
PublicStats/PublicRulePrecisionmigrated to the derived-type mechanismtest:ciExpected outcome
A backend response-shape change fails fast (compiler or CI), on the pilot surface today and on every
surface migrated after — instead of silently drifting until a user notices broken UI.
maintainer-only — cross-package API contract infrastructure.