Implements the activity-sport-type change (specs/activity-sport-type):
- db: nullable `sport_type` column on journal.activities + SportType /
SPORT_TYPES (text().$type<> convention).
- api: optional sportType on the activity read + create schemas (mirrored
SPORT_TYPES; @trails-cool/api stays zod-only).
- write: ActivityInput + createActivity persist it; mapSportType() normalizes
provider strings (Komoot bulk import passes tour.sport; Garmin unset);
threaded through the unified importActivity.
- read/display: sportType added to the detail/feed/profile loaders and the v1
REST endpoints; shared SportBadge (glyph + i18n label) on detail, feed, and
profile; sport-aware feed verb; create-form <select>.
- i18n: journal.activities.sport.* (labels + verbs) in en + de.
- federation: `sport` PropertyValue on the Note when set.
Tests: mapSportType unit table; federation asserts the sport attachment is
present when set and omitted when unset. typecheck + lint + unit all green.
E2E (create→badge) still to add.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Route and Activity existed three times: hand-written interfaces in
packages/types, Zod contracts in packages/api, and Drizzle columns in
packages/db — each with different fields and nullability. The
hand-written ones had drifted so far they had zero importers; the Zod
contracts were advisory because v1 handlers hand-rolled Response.json
shapes nothing validated.
- packages/types keeps only what both apps actually share (Waypoint,
WaypointPoiTags) and documents where row types and wire contracts
live; the dead Route/RouteMetadata/RouteVersion/Activity interfaces
are gone
- packages/db exports canonical inferred row types (RouteRow,
ActivityRow, RouteVersionRow, UserRow)
- packages/api contracts are reconciled with the real wire format
(RouteVersionSchema gains the id and createdBy fields the endpoint
has always returned) and gain Create*ResponseSchemas
- apiJson(schema, payload) in api-guard parses every v1 response
through its contract: drift is now a thrown ZodError in tests/CI,
unknown keys are stripped, and payloads are compile-checked as
z.input of the schema
Enforcement immediately caught two real drifts: nullable DB
descriptions could ship null where the contract promises string (now
coalesced at the boundary), and GET /api/v1/activities/:id was missing
the routeName and photos fields its contract declares.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the deprecated v3 string-method forms with the top-level
validator helpers zod 4 prefers:
- z.string().url() → z.url() (3 sites)
- z.string().uuid() → z.uuid() (5 sites)
- z.string().datetime() → z.iso.datetime() (9 sites)
Also swap the ZodIssueCode.custom compat-shim reference in
demo-bot.server.ts's superRefine for the bare string literal "custom",
which is v4's idiomatic form. No runtime change.
All 147 tests still pass.
The `parsed.error.issues.map(i => ({field, message}))` pattern in the
five journal API route handlers stays as-is — the fields we read are
unchanged in v4, and zod's new error helpers (z.treeifyError,
z.prettifyError) produce different shapes that would change our
public API error contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>