Three holes, one seam. Leaving the corridor popup by "more details" dropped the kind and the dwell it had just worked out, so a fuel stop silently became a numbered destination that counted in every total, in the badge and in the PDF. Once set, the kind could never be changed back through the web UI at all. And the REST route did not validate the field: both request schemas are open objects, so stop_type: 'banana' was written straight to the column, where isServiceStopType read it as false and the stop counted as a destination again. The form now carries the field, so the popup's answer survives the trip through it, an ordinary place can become a service stop, and a wrong kind can be taken off. Editing keeps stop_type and deliberately leaves duration_minutes out: how long a stay takes belongs to the rail's own dialog, and sending it from here would overwrite what was set there. The schemas gain the one field rather than closing: roughly 190 call sites post whatever their form holds, and a closed object would reject all of them at once. The doc comment claiming the list was already enforced is corrected — it was describing an intention. The two driving findings move into the stop's badge row in warning colours, beside the stay and the walk from the road. All three say what this stop costs, and one row reads as one answer rather than as notes stacked underneath.
@trek/shared
Single source of truth for TREK's API contracts, expressed as Zod schemas and consumed by both the server (request validation + inferred DTO types) and the client (typed requests/responses).
This package is part of the incremental NestJS + React 19 migration (see the "Brownfield Rewrite" board). It is intentionally dormant until modules start importing it — adding it changes nothing for users.
Rules
- One folder per domain:
src/<domain>/<domain>.schema.ts(+.spec.ts). - Domain-agnostic building blocks live in
src/common/. - A route is only considered migrated once its contract lives here.
- Schemas are the source of truth; server DTOs and client types are inferred from them
(
z.infer<typeof schema>), never hand-duplicated.
Consumption (dev)
Both apps resolve @trek/shared to this package's TypeScript source:
- Server (
tsx): viapathsinserver/tsconfig.json. - Client (
vite): viaresolve.aliasinclient/vite.config.ts(+pathsfor the type-checker).
Production packaging (Docker / workspace wiring) is introduced in card F2, when the server first depends on this package at runtime. Until then prod builds are untouched.
Not yet here
The canonical error envelope is finalised in card F5 (it must match TREK's current Express error responses byte-for-byte), so it is deliberately not invented in F1.