Context
The first planner path prepared route data and searched it at request time. That made sense when the planner was close to direct bundle searches. It became expensive to repeat once the network included directed services, geometry, service identity, transfer edges, and query-specific projections.
Commit 89cad3b records the replacement with a compiler, image reader, mount layer, reverse search, overlay, and candidate selector. Commit 8366918 moved the query work behind a browser worker. Commit ff57ec5 removed the generated image from the application change set and made it a public content-addressed asset.
The previous format description used a different file name and cache layer. Those details are not the current decision.
Decision
Compile static transit topology ahead of time into a versioned wayplan image. The current image uses the magic value WAYPLAN\0 and format version 1. It has a 96-byte header, a 16-byte directory entry for each section, eight-byte section alignment, and 20 required sections covering metadata, strings, services, stops, coordinates, nodes, edges, adjacency, geometry, segments, boards, and service-area rings.
The compiler appends a 32-byte SHA-256 digest of the image content. The build script places that digest in the file name and in planner-image.json, together with the byte count. It compiles twice and compares both the digest and every byte before it publishes the pair.
The web client fetches the manifest and image, checks the byte count and SHA-256, and transfers the image buffer to a dedicated worker. The worker verifies the image again, mounts it, and rejects a format or section mismatch before it announces IMAGE_READY.
The image contains static topology. Origin and destination projections, access and egress edges, enabled modes, live-arrival observations, and result selection remain query-time concerns.
The runtime checks the supplied digest before it exposes the image to search:
const header = readPlannerImageHeader(image) const sections = readSections(image) const sha256 = plannerImageHashParts(image).expectedSha256 if (verifiedSha256 !== sha256) { throw new Error("planner image SHA-256 does not match") } const strings = decodeStrings(Source: planner-mount.ts. The excerpt stops before the section arrays are allocated. That distinction is part of this decision: the bytes are verified first, then the runtime representation is built.
Format facts
| Field | Current value |
|---|---|
| File | planner-v1- |
| Manifest | planner-image.json |
| Magic | WAYPLAN\0 |
| Format version | 1 |
| Header | 96 bytes |
| Directory entry | 16 bytes |
| Section alignment | 8 bytes |
| Coordinate scale | 10,000,000 |
| Trailing integrity value | 32 raw SHA-256 bytes |
The runtime checks the format version, tick duration, coordinate scale, projection tie distance, section count, section widths, offsets, counts, and image digest. It can read section bytes through DataView, but mountPlannerImage decodes strings and copies numeric data into typed arrays. The decision is a compact, verifiable representation, not a promise of zero-copy or zero-allocation execution.
Consequences
Static graph construction no longer belongs to each planning request. After the image is cached and mounted, the browser worker can search static topology without downloading the graph again. The content address identifies the graph version used by the worker. Workbox includes wayplan assets in the current precache glob.
The decision adds maintenance. The compiler and runtime must evolve together. A source change can produce a new image, a worker can reject an image from a different format contract, and a service-worker update can leave a planner unavailable until the correct asset pair is available. The checked-out manifest reports 26,851,432 bytes, but generated size can change.
The worker also has real memory costs. Mounting allocates arrays for coordinates, nodes, edges, adjacency, strings, and sections. Each query allocates overlay and label structures and materializes result objects. Transferring the initial ArrayBuffer avoids a message clone; it does not remove these allocations.
The current browser request passes an empty liveBusArrivals list. This ADR therefore describes a static route engine with an optional observation input, not a live-ETA guarantee. Fares are outside the graph score.
Rejected alternatives
Keeping JSON and rebuilding object graphs at request time retained repeated work at the wrong boundary. Keeping search on the page left graph work coupled to UI responsiveness. Persisting the planner image in a planner-specific IndexedDB store was not chosen; the current web path uses Workbox and CacheStorage.
The image also does not make source data true. Bus stop order, explicit jeepney directions, geometry, and service-area inputs still need validation. The image gives those inputs a stable runtime contract after they pass the build.
For the build and input checks, read Planner Image Compilation Pipeline. For the incident that made the client boundary necessary, read Server-to-client planner pivot.
Status
Accepted. The compiler, reader, mount layer, and web worker currently implement this decision.