Skip to content

Import ReleaseInfoSnapshot from the server instead of restating it - #985

Merged
selfcontained merged 1 commit into
mainfrom
agt_156b1adc453d/wire-types-release-info
Aug 20, 2026
Merged

Import ReleaseInfoSnapshot from the server instead of restating it#985
selfcontained merged 1 commit into
mainfrom
agt_156b1adc453d/wire-types-release-info

Conversation

@selfcontained

Copy link
Copy Markdown
Owner

What

apps/web/src/hooks/use-cached-release-info.ts hand-restated the server's ReleaseInfoSnapshot — an 11-field copy of apps/server/src/release-info.ts:31-43. It now import types the server declaration and re-exports it under the same name. +5/-18, one file.

This is sub-item (e) of the server↔web wire-type duplication cluster (the 2026-08-14 audit's duplicate-block scan). (a) use-service-resources #965, (b) reviews three-way #971, (c) brain #976, (d) agent-history #981 are already done.

Why it's tech debt

Two hand-maintained copies of one HTTP response shape. Nothing enforced that they stayed in sync — a field added or renamed server-side would type-check fine on both sides while the web copy silently described a shape the server no longer sends.

Shape A, no new leaf module needed

The two-grep probe from prior runs:

  • grep -n '^export class\|^class' apps/server/src/release-info.tsno class, so useDefineForClassFields (the Single-source the brain wire types shared by server and web #976 TS2401 trap) does not apply.
  • grep -n '^import' apps/server/src/release-info.ts → no service-layer type, and nothing reaching apps/server/src/generated/*.

Better than a probe: apps/web/src/hooks/use-release-stream.ts:26 already type-imports ReleaseChannel from this exact module, so release-info.ts's whole graph is in web's TS program today. pnpm run check:web was run first, with apps/server/src/generated/ still absent (confirmed by ls), so the #863 generated-assets trap is ruled out rather than masked by pnpm run check's side effect.

Web now imports directly from ../../../server/src/release-info rather than routing through use-release-stream.ts. That drops the three type-only imports this hook took from that hub (AssistedUpdateMetadata, PendingMigration, ReleaseChannel), which were only there to build the copy.

Drift: none

Field-for-field identical. The one name difference is cosmetic and pre-resolved: web's PendingMigration is already export type PendingMigration = PendingMigrationSummary (use-release-stream.ts:45), the exact server type the snapshot uses. This is the fifth of six consolidations to find zero drift.

Completeness

grep -rn "absoluteLatestTag:" apps e2e bin scripts (a middle field, counting declaration sites) returns exactly one non-test hit after the change: apps/server/src/release-info.ts:35. Before: two.

Deliberately excluded — near-misses, do not re-litigate

  1. ReleaseInfo in use-release-stream.ts:47-72 — the admin-enriched /api/v1/release/info shape. It looks like the same duplication, but the server builds that response as an anonymous object literal at routes/release.ts:345-360; there is no named server type to alias. Naming one would be adding typing to a route return, not removing a duplicate. Backlogged separately.
  2. UiEvent restated in use-sse.ts:52-97 vs apps/server/src/server/ui-events.ts:8-73** — a much larger duplication in a different domain, and blocked by Shape A: ui-events.tsdeclaresclass UiEventBroker`, so it needs a leaf extraction. Backlogged; out of scope for a one-item run.
  3. CachedInfoResponse ({ snapshot: ReleaseInfoSnapshot | null }) — kept web-local. Its server counterpart is handleCachedInfo's inferred { snapshot } return at routes/release.ts:362, anonymous server-side too; now that the element type is shared, the one-field envelope carries no drift risk.
  4. The { tag; publishedAt; url } inline shape inside the snapshot — moved along with the type, not extracted. It is anonymous on the server as well.

Validation

  • pnpm run check:web (run first, apps/server/src/generated/ absent) ✅
  • pnpm run check
  • pnpm run finalize:web ✅ (built, PWA precache generated)
  • apps/web vitest: 70 files / 1044 tests passed
  • pnpm run test:e2e: 181 passed, 12 skipped
  • Server vitest not run — no backend file changed.

Next run

Sub-item (f): apps/server/src/shared/git/agent-diff.ts vs apps/web/src/hooks/use-agent-diff.ts (3 mirrored windows), then (g) messages/store.ts vs use-agent-messages.ts.

🤖 Generated with Claude Code

apps/web/src/hooks/use-cached-release-info.ts hand-mirrored the server's
ReleaseInfoSnapshot (11 fields) rather than importing it. Web already
type-imports ReleaseChannel from the same module in use-release-stream.ts,
so the direct import needed no new leaf module.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@selfcontained
selfcontained merged commit 0cb603b into main Aug 20, 2026
1 check passed
@selfcontained
selfcontained deleted the agt_156b1adc453d/wire-types-release-info branch August 20, 2026 09:11
selfcontained added a commit that referenced this pull request Aug 21, 2026
apps/web/src/hooks/use-agent-diff.ts hand-restated four types that
apps/server/src/shared/git/agent-diff.ts already declares. This is
sub-item (f) of the server<->web wire-type duplication cluster;
(a)-(e) landed in #965, #971, #976, #981 and #985.

DiffFileStatus, DiffFile and FileDiffResponse were byte-identical and
are now type-only imports re-exported under their existing names, so
every consumer import is unchanged.

DiffResponse had genuinely drifted — the first real drift this cluster
has turned up in six consolidations. The web copy declared
`baseRef: string | null` and omitted the server's
`truncatedFileCount?: number`. Both halves of that are explained by the
route rather than by a mistake in either type: the GET
/api/v1/agents/:id/diff handler substitutes `{ baseRef: null, files: [] }`
when getAgentDiff returns null, so nullability is real on the wire even
though the server's own DiffResponse always carries a merge-base SHA.
The web type is now derived — Omit<DiffResponse, "baseRef"> plus the
nullable baseRef — so the nullability stays documented while
truncatedFileCount and any future field come across for free.

Type-only change; esbuild erases the imports, so nothing from the
server reaches the web bundle.


Claude-Session: https://claude.ai/code/session_014PHD7HNfqtZHTXTLWwxxWo

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant