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:
share_idis opaque and public. It is not a route/content hash and is not an auth token.- Route/content hashes remain internal implementation details.
slugis short localized presentation text. It must not include distance or unit text such as14-1-mior10k.
The minimal public bundle is:
/r/{share_id}/manifest.json— small metadata only:version,share_id,canonical_slug, title, page-visible prosedescription, optional stats+prosepreview_description, sanitized request paraphrase, sibling alternatives, locale/default locale, units, public stats summary (distance_m, elevation gain/loss, optionaltrail_ratiofrom0.0to1.0, and optional estimated duration), center/bounds, asset refs, and offline/cache hints. When request context is unavailable,request_paraphraseis omitted ornull; it is never serialized as an empty string./r/{share_id}/route.geojson— the only public route geometry source: aFeatureCollectionwith exactly oneFeature, empty properties, and aLineStringgeometry 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 alternativeshare_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.jsonroute_share_presentation.en-US.v1.jsonroute_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.descriptionare route-page-visible prose.option.link_preview.title/option.link_preview.description/option.link_preview.image_pathare social/link-preview metadata. The description may combine localized stats with prose.option.delivery.modecontrols the visible channel body.bare_urlmeans the channel message body is exactly the route page URL.url_with_textmeans producer-authoreddelivery.textis shown before the URL.Noneis not a delivery signal.option.reply_contextcarries the reply binding. At least one ofroute_session_id,share_id, orsource_route_page_idis required, and anyshare_idmust use the same opaque public share-id validator as route pages.final_slate_summary.summaryand optionalcomparison_guidanceare the final route-slate message.option_label_policykeeps 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/sessioncreates or refreshes the browser session and CSRF token.POST /routesstarts a route run for the current browser session.GET /routeslists the current browser session's route runs.GET /routes/{run_id}returns one owned route run.POST /routes/{run_id}/continuecreates a child run from an owned run.POST /routes/{run_id}/publishrecords or reuses the canonical/r/{share_id}/{slug}page for one generated route option.POST /routes/{run_id}/analyticsrecords 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_localeis 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_localeis the locale Cheshire uses for route prose, route stats, unit defaults, route-page metadata, and rendered artifacts. It defaults toviewer_localewhen a route run starts and must be persisted with the run so continuations keep artifact language stable.BrowserRouteRunpersists both fields. Cheshire route calls forwardgeneration_localeaslocaleand 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_deltatext - 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.