Skip to content

Public Route API, Share, Locale, Progress, and Font Contract

Issues: #1043 and #1134. Parents: #1041 and #1131.

This contract applies to the browser-first route planner at imperfect.co/routes and the api.imperfect.co/routes endpoints. The public browser boundary is intentionally stricter than authenticated app chat: it can show high-level progress, but it must never expose Cheshire internals.

Route-share hard cutover

The route-share contract is deliberately breaking. Old public route objects, old manifests, old hash URLs, and old asset shapes are deleted rather than migrated. Old links may return intentional 404 or 410; no reader should carry a v2/v3 compatibility path. This file defines the reset contract that the #1041 child issues converge on; #1044 owns the imperfect-api endpoint and storage rewiring from the old hash-shaped response to this share-id shape.

The only public route page URL shape is:

/r/{share_id}/{slug}
  • share_id is opaque and public. It is not a route/content hash and is not an auth token.
  • Route/content hashes remain internal implementation details.
  • slug is short localized presentation text. It must not include distance or unit text such as 14-1-mi or 10k.

The minimal public bundle is:

  • /r/{share_id}/manifest.json — small metadata only: version, share_id, canonical_slug, title, page-visible prose description, optional stats+prose preview_description, sanitized request paraphrase, sibling alternatives, locale/default locale, units, public stats summary (distance_m, elevation gain/loss, optional trail_ratio from 0.0 to 1.0, and optional estimated duration), center/bounds, asset refs, and offline/cache hints. When request context is unavailable, request_paraphrase is omitted or null; it is never serialized as an empty string.
  • /r/{share_id}/route.geojson — the only public route geometry source: a FeatureCollection with exactly one Feature, empty properties, and a LineString geometry whose coordinates are the public route polyline.
  • /r/{share_id}/preview.jpg — required, bounded social/link-preview image. It is not a required first-paint map placeholder for the route page, and it must not bake in route copy that belongs in localized message or metadata text.
  • /r/{share_id}/route.gpx — export/Garmin source when needed.
  • /r/{share_id}/manifest.{locale}.json — text-only overlay. It may carry localized title, description, preview_description, request paraphrase, slug, and localized sibling alternative title/description/slug text; it must not duplicate geometry, stats, assets, bounds, or offline hints. The base manifest owns the alternative set and order. Locale overlays are keyed by alternative share_id, may omit unchanged siblings, and consumers keep the base order.

Public manifests must not contain coordinate arrays, route hashes, share tokens, signed URLs, provider ids, backend/profile/provenance, debug ids, SVG refs, high-resolution PNG refs, or internal route-generation metadata. The testable schema lives in app.lib.public_routes.share_contract, with JSON fixtures in tests/fixtures/public_routes/new_contract/. The reusable leak/identity rules live beside it in app/lib/public_routes/public_route_contract_rules.v1.json; regenerate that artifact with python scripts/check_public_route_contract_rules.py --write whenever the Python contract rules change. Cheshire and Looking Glass should vendor the generated artifact instead of copying private regexes or forbidden key sets.

description is visible route-page prose and should not duplicate the stats row. preview_description is metadata copy for HTML/social/link previews and may combine localized stats with public prose. Producers may omit it or send null when no dedicated preview copy exists; an explicit empty string is preserved so consumers can intentionally suppress fallback preview copy. request_paraphrase is the sanitized user intent for page context, not the raw prompt. Producers omit it or send null when no safe paraphrase exists; an empty string is invalid. alternatives contains only sibling route-page links from the same generation turn: public share id, localized slug, owned /r/{share_id}/{slug} path, title/description, and optional public stats. It must never include the current page, private route ids, source session ids, hashes, provider ids, signed URLs, or generation diagnostics. Locale overlays may localize alternative text/slug by share_id, but must not introduce new alternatives, reorder the canonical list, or include the current page.

Route presentation packet

RouteSharePresentation is the versioned service-to-service packet for route messages sent through WhatsApp, iMessage, Heylo, or future channel bridges. It lives beside the public bundle contract in app.lib.public_routes.share_contract because it names the presentation surfaces attached to the same /r/{share_id}/{slug} route pages. The generated non-Python artifacts are checked in under app/lib/public_routes/:

  • route_share_presentation.v1.schema.json
  • route_share_presentation.en-US.v1.json
  • route_share_presentation.es-MX.v1.json

Regenerate them with python scripts/check_route_share_presentation_contract.py --write whenever the Python contract or canonical examples change.

Cheshire produces this packet. imperfect-api relays it into neutral CoachReply/outbox shapes. Channel bridges render the named surfaces and must not synthesize route copy, derive preview metadata from page prose, or treat a missing field as product intent.

The packet separates the user-visible surfaces deliberately:

  • option.page.title / option.page.description are route-page-visible prose.
  • option.link_preview.title / option.link_preview.description / option.link_preview.image_path are social/link-preview metadata. The description may combine localized stats with prose.
  • option.delivery.mode controls the visible channel body. bare_url means the channel message body is exactly the route page URL. url_with_text means producer-authored delivery.text is shown before the URL. None is not a delivery signal.
  • option.reply_context carries the reply binding. At least one of route_session_id, share_id, or source_route_page_id is required, and any share_id must use the same opaque public share-id validator as route pages.
  • final_slate_summary.summary and optional comparison_guidance are the final route-slate message. option_label_policy keeps lettered option language explicit instead of letting bridges infer picker mechanics.

API surface

The public browser API is session-bound, not a developer API:

  • GET /routes/session creates or refreshes the browser session and CSRF token.
  • POST /routes starts a route run for the current browser session.
  • GET /routes lists the current browser session's route runs.
  • GET /routes/{run_id} returns one owned route run.
  • POST /routes/{run_id}/continue creates a child run from an owned run.
  • POST /routes/{run_id}/publish records or reuses the canonical /r/{share_id}/{slug} page for one generated route option.
  • POST /routes/{run_id}/analytics records browser-visible outcome analytics.

POST /routes and POST /routes/{run_id}/continue consume Cheshire's private browser-route stream server-side, persist a BrowserRouteRun, and return the browser-safe event log for that run. Full live replay and catch-up semantics are owned by issue #993; this contract only guarantees that persisted run rows are safe to replay later.

After #1044, POST /routes/{run_id}/publish is idempotent per route page. It can publish only browser-owned runs that ended in route_options or route_ready and whose matching option already contains a route_page artifact with a canonical /r/{share_id}/{slug} public path. The response returns only browser-safe page data: share_id, slug, page URL/path, manifest URL/path, option ids/labels, title/description, and published_at. It never falls back to signed artifact URLs, Dub/go links, raw Cheshire ids, route/content hashes, share tokens, or debug metadata. Publishing another option from the same run records another page instead of changing prior pages; a selector that does not match a candidate returns not found; runs without an owned page artifact or runs that ended in error return a deterministic not-ready/conflict response.

Locales

  • viewer_locale is the browser-visible locale for shell UI and streaming progress. It comes from the request/browser, can change on reconnect, and is safe to use when replaying canonical progress events to a different viewer.
  • generation_locale is the locale Cheshire uses for route prose, route stats, unit defaults, route-page metadata, and rendered artifacts. It defaults to viewer_locale when a route run starts and must be persisted with the run so continuations keep artifact language stable.
  • BrowserRouteRun persists both fields. Cheshire route calls forward generation_locale as locale and the unit directive derived from that same locale so continuations keep artifact language stable.

Progress

Browser progress stores and streams canonical intents from app.lib.public_routes.contract.PublicRouteProgressIntent, not raw Cheshire events. Raw stream events are used only to choose an intent:

  • thinking_delta -> understanding_request
  • location/geocode tools -> checking_location
  • segment/trail/road/option tools -> searching_segments
  • elevation/terrain tools -> checking_elevation
  • render/map/preview tools -> rendering_maps
  • publish/bundle/share/page tools -> finalizing_route
  • unknown tools -> working

Localization happens after that mapping. The localizer receives only the safe intent id and phrase table label; it never sees chain-of-thought, SQL, code, tool arguments, provider ids, signed URLs, or debug links. Unsupported valid viewer locales keep their locale tag but fall back to English copy.

No browser progress payload may include:

  • raw thinking_delta text
  • raw tool names or tool arguments
  • SQL, Python, stack traces, or debug links
  • Cheshire session ids, provider ids, token counts, signed URLs, or exact private coordinates

Fonts

app.lib.public_routes.contract.public_route_font_contract() is the testable source of truth for route-page/render font stacks.

Script bucket Locales Direction Cheshire render stack Looking Glass CSS stack
Latin default, including en-US, es-MX ltr Inter, Noto Sans, Arial, sans-serif var(--font-inter), Inter, system-ui, platform fallbacks, sans-serif
CJK zh, ja, ko ltr Noto Sans CJK SC, Noto Sans CJK JP, Noto Sans CJK KR, Noto Sans, sans-serif var(--font-noto-sans-cjk), Noto CJK fallbacks, system-ui, sans-serif
Arabic script ar, fa, ps, sd, ur rtl Noto Naskh Arabic, Noto Sans Arabic, Noto Sans, sans-serif var(--font-noto-naskh-arabic), Noto Naskh Arabic, Noto Sans Arabic, system-ui, sans-serif

Hebrew is not an MVP target for this launch; it should get its own explicit font/direction contract before being treated as supported.