Skip to content

ui: derive API response types from the backend's real zod/OpenAPI schemas, stop hand-duplicating them #9282

Description

@JSONbored

Context

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:

  1. 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.
  2. Generate types from the emitted openapi.json via openapi-typescript. Works across the workspace
    boundary without relocating any source (the built openapi.jsonapps/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.

Requirements

  1. Decide and record the mechanism (above).
  2. Pilot on one real surface first: 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.
  3. 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.
  4. 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.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions