Two consumers decide whether a candidate place is one the trip already has, and they spelled the rule out separately: the bulk importers ask an in-memory set (isPlaceDuplicate), while findDuplicatePlace asks the database because its caller needs the matched row's id. The two had drifted — the SQL copy falls back to a coordinate match for a NAMED candidate, which the in-memory copy deliberately never does, since that would merge the restaurant and the bar at the same address. The order now lives once in @trek/shared as placeMatchStrategies(), and each consumer is a mechanical interpreter of it: provider id (the only part that survives a rename, #1550), then name, then coordinates and only when there is no name. Adds PlacesService.findMatchingPlaceId as the public door for an importer that has to link to the existing place rather than merely skip the candidate. findDuplicatePlace stays private behind it — it also returns google_ftid for the bulk importer's backfill, which is that caller's business and not part of "which place is this?". No behaviour change for either existing caller. The coordinate branch is currently unreachable from the one call site, which sits inside an isPlaceDuplicate() guard that has already matched on name and returned; the drift only bites once something calls it standalone. COORD_DEDUP_TOLERANCE and externalIdsOf move to shared and are re-exported from places.helpers, so collections.service and places.service keep their imports.
@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.