9 file(s), +236 / -8 lines
| Path | Role | + | - |
|---|---|---|---|
| src/app/[locale]/(workspace)/matters/[matterId]/page.tsx | App route | +5 | -0 |
| src/app/api/internal/matters/[matterId]/frame/versions/route.ts | App route | +32 | -0 |
| src/components/matter-detail/brief/matter-brief-page.tsx | Component | +6 | -3 |
| src/components/matter-detail/brief/matter-brief-sections.tsx | Component | +24 | -4 |
| src/components/matter-detail/brief/presentation.test.ts | Component | +80 | -0 |
| src/components/matter-detail/brief/presentation.ts | Component | +36 | -1 |
| src/components/matter-detail/brief/use-field-versions.ts | Component | +24 | -0 |
| src/lib/kubb-client.ts | Library | +12 | -0 |
| src/lib/matters/gateway.ts | Library | +17 | -0 |
frame/versions
The Brief's per-field history control (the gutter History popover,
the full-history modal, ⌘Z/⌘⇧Z, and Restore this version) used to
show a single, current-only row: the brief payload carries only the newest
frame version, so fieldVersionStack was an honest one-entry
stub. The backend now serves the full append-only history at
GET /app/matters/{id}/frame/versions
(MatterFrameVersionHistoryResponse, newest-first). This story
wires the timeline to that endpoint so every field shows its real
multi-version history.
src/app/api/internal/matters/[matterId]/frame/versions/route.ts
(GET), mirroring the sibling …/brief proxy: parse the matter
id, call the gateway, map GatewayError to an
ErrorResponse. The browser only ever talks to
/api/internal/*; the gateway key + Clerk bearer stay
server-side.
mattersGateway.frameVersions(matterId)
fetches as unknown and runs it through
assertContract(matterFrameVersionHistoryResponseSchema, …)
(HR-21 — runtime contract validation at the trust boundary).
toBrowserRoute rule maps
/app/matters/{id}/frame/versions →
/api/internal/matters/{id}/frame/versions, placed before
the bare /frame and /app/matters/{id} rules so
it is never shadowed.
page.tsx prefetches
the versions query into its HydrationBoundary alongside the
brief and frame-status, so the first paint already carries history (no
client refetch flash).
useFieldVersions(matterId, field, fallback) reads the shared
versions query (TanStack dedupes the four call sites on the page into
one request) and maps it with the pure
mapFieldVersions(items, field) helper:
envelope.frame[field] —
the MatterFrameEnvelope (the brief flattens it; the version
projection keeps it nested). The helper reads
envelope.frame ?? envelope so it is robust to either shape.
value = textField(frame, field) (same accessor the current
value renders through), author = authorDisplayName ?? (agentRunId ?
"Vega" : "Counsel"), actorKind from
agentRunId, id = `${field}:v${version}`.
core_dispute adds no core-dispute row).
Input is newest-first, so output is too.
fallback
(fieldVersionStack), so the timeline never flashes blank on a
field that already shows a value.
matterId threads from matter-brief-page.tsx
(matter.matterId) into BriefHeader
(frame_summary) and the EditableFields
(core_dispute, theory_of_case,
settlement_posture). Each builds its versions from
useFieldVersions instead of the sync stub. The
EditableFieldWithHistory presentation component is unchanged —
it already renders any newest-first SummaryVersion[], so
undo/redo and Restore work across the real versions for free.
pnpm lint (0 errors), pnpm build (green; the new
/api/internal/matters/[matterId]/frame/versions route is
registered), and a new presentation.test.ts covering
mapFieldVersions (order, dedupe/drop-empty, agent-vs-counsel
authors, flattened-envelope fallback) all pass.