Problem Statement

The current rider interface mixes product behavior with presentation. Route and stop search, trip-planner orchestration, assistant conversation behavior, PWA update handling, external map links, local-data clearing, and several query decisions live in or beside components that the project intends to delete. Some retained logic also sits under the wrong owner. Trip locations live under map state, generic query persistence imports an activity-owned database, and the assistant screen coordinates a second transcript store by hand.

Deleting the current presentation without first defining the retained logic would leave a future UI with two bad options. It could import old markup and renderer modules to reach behavior, or it could rebuild the behavior inside new screens. The first option preserves the coupling. The second creates duplicate rules and makes the UI responsible for planner concurrency, assistant retries, storage, search relevance, and network failure semantics.

The future UI needs a smaller contract. A screen should compose design-system components, read raw product state, and invoke typed product commands. It should not know how the planner worker mounts an image, how assistant intents dispatch, how transit bundles fall back to a last-good version, how the place providers fail over, or which browser stores must be cleared together.

This work must preserve the repository's existing domain boundaries:

  • A route is a public line identity. A service is a directed operating variant.
  • Bus direction and stop order remain authoritative data. Geometry reversal does not invent service direction.
  • Search can find graph-valid candidates, but planner selection still enforces rider-facing access, transfer, ride-count, and duplicate rules.
  • The planner image remains a verified, content-addressed artifact mounted in a browser worker.
  • The assistant model extracts intent. Deterministic code owns routes, stops, fares, transfers, live data, and service-area facts.
  • Current location stays in the browser session. The assistant request does not send GPS coordinates to the model boundary.
  • Vehicle snapshots, live arrivals, and road slowdowns remain different evidence types.
  • packages/contracts owns values that cross the API and web boundary. packages/transit owns transit facts and algorithms shared by existing consumers.

Solution

Why

The logic split exists so one owner implements each product rule. The next UI should not repeat request cancellation, transcript mutation, fallback cache selection, route indexing, or location privacy decisions. A narrow public interface also makes the deletion of the current presentation honest. If a retained module imports a component, stylesheet, icon, route layout, map renderer, or display-only formatter, the split is incomplete.

This is an ownership correction, not a reusable-platform project. The repository already has two shared packages with real consumers. Web-only orchestration stays in the web application until another application proves that it needs the same behavior and can use the same platform assumptions.

What

The target keeps only product behavior, app data access, runtime setup, and shared domain contracts. Each feature exposes the hooks, functions, query options, stores, and types that an actual screen or route loader calls. Everything else stays private to that feature.

The target does not introduce view-models, adapters, facades, service classes, compatibility wrappers, or parallel implementations. Public feature entry modules may whitelist exports, but they contain no behavior. Small features may expose their real module directly when an entry module adds no protection.

The UI receives raw domain values and product state. It does not receive preformatted rows, labels, CSS classes, icon names, GeoJSON created only for rendering, or map-camera instructions. Presentation can format values locally or through the future design system, but it cannot move product decisions back into a screen.

Where

OwnerPlacementReason
App boundaryThe routing and bootstrap area of apps/webRouter registration, URL validation, loader execution, query-client setup, i18n setup, PWA setup, and root error handling are web runtime concerns. Feature logic must not import them.
App dataA lower-level data area in apps/webThe typed API client, browser database, query client, and query persister support several features. Keeping them below features removes the current shared-to-activity dependency.
Transit dataA web feature areaVersioned bundle queries, last-good fallback, live intelligence query options, bundle hooks, and the single transit index are web consumption logic. The schemas and transit algorithms remain in packages/transit.
Bus routesThe bus-route featureRoute search and ordered service-stop lookup are route behavior. Route cards, labels, and map covers are presentation.
Bus stopsThe bus-stop featureStop search, duplicate-name detection, and services serving a stop are stop behavior. Stop rows, sharing dialogs, and disclosure copy are presentation.
Jeepney routesThe jeepney-route featureRoute-name search, Davao place matching, serving-route lookup, and route distance are feature behavior. Landmark icons, distance strings, and map covers are presentation.
ActivityThe activity featureRecent and saved trip state, persistence commands, duplicate prevention, and the recent-item cap belong together. The shared browser database does not belong to this feature.
Trip plannerThe trip-planner featureEndpoint state, provider place search, place retrieval, request validation, worker preparation, latest-request-wins behavior, result selection, activity recording, saved-state commands, and share-link privacy form one product operation.
AssistantThe assistant featureIntent request, validation, deterministic dispatch, session context, transcript, retries, templates, starter actions, and follow-ups form one conversation operation. Components and widgets do not belong here after presentation deletion.
Bus trackerThe bus-tracker featureVehicle snapshot query options belong with the tracker. Transit intelligence query options used across routes, stops, and tracker remain with transit data.
LocalizationThe web i18n areaCatalogs, supported language validation, language detection, and persistence are app-wide logic. Settings markup does not own them.
Local-data settingsThe settings featureClearing local data is one user command that coordinates activity, persisted queries, selected local-storage keys, and named caches.
PWA and browser capabilitiesPWA and platform areas in apps/webService-worker state, online state, native share fallback, and external-map URL construction use browser capabilities but do not decide presentation.
Shared contractspackages/contractsThe API and web app already share assistant request, intent, result, and validation schemas.
Shared transit domainpackages/transitThe API, web app, collector, and scripts already share transit schemas, geometry, fares, place-domain search, planner contracts, worker protocol, and planner algorithms.

User Stories

  1. As a future UI engineer, I want every screen to import product behavior from a named feature entry, so that I can replace the presentation without understanding internal data flow.
  2. As a future UI engineer, I want feature hooks to return raw domain state and typed commands, so that design-system components remain responsible only for rendering and interaction.
  3. As a future UI engineer, I want route loaders to use the same query definitions as screens, so that preload and render paths cannot disagree about cache keys or freshness.
  4. As a future UI engineer, I want route and stop search functions to return routes and stops rather than Fuse-specific match objects, so that the UI does not depend on a search library's rendering metadata.
  5. As a future UI engineer, I want distance, date, movement, and route-label formatting excluded from product logic, so that the future presentation can localize and format values consistently.
  6. As a future UI engineer, I want no map renderer values in the product interface, so that a future map implementation can change without changing transit, planner, or activity logic.
  7. As a rider, I want route search to include route identity, route name, endpoints, and ordered stop names, so that relevant routes remain discoverable after the UI replacement.
  8. As a rider, I want stop search to use stop names and locations, so that stops with shared names can still be found and distinguished.
  9. As a rider, I want a stop's serving services derived from canonical route references and directions, so that the new UI does not guess service direction from labels.
  10. As a rider, I want jeepney route search and destination-serving lookup to keep their current product rules, so that replacing route cards does not change which routes appear.
  11. As a rider, I want provider place suggestions to preserve provider failover and stale-pick suppression, so that an older network response cannot replace my latest location choice.
  12. As a rider, I want trip endpoint text, resolved places, and map-selected coordinates to remain distinct states, so that the planner does not treat unresolved text as a usable coordinate.
  13. As a rider, I want the planner to validate both endpoint coordinates before search, so that invalid input fails before reaching the worker.
  14. As a rider, I want a newer trip request to supersede an older request, so that a slow result for old pins cannot replace the trip I am currently planning.
  15. As a rider, I want planner image verification and worker mounting to remain internal, so that the UI only handles planning, results, and unavailable states.
  16. As a rider, I want planner failures to remain distinct from no-connection results, so that the product does not claim the transit network has no route when the planner could not load.
  17. As a rider, I want a successful plan to add one recent trip through activity logic, so that recent history stays consistent regardless of the presentation.
  18. As a rider, I want saving and unsaving the selected trip to use the canonical trip identity, so that the same itinerary is not duplicated under presentation-specific keys.
  19. As a rider, I want exact coordinates included in a trip share link only for map-selected endpoints, so that named-place shares do not disclose coordinates unnecessarily.
  20. As a rider, I want the logic to tell the UI when a share link contains exact coordinates, so that the UI can ask for confirmation before sharing.
  21. As a rider, I want recent and saved trips to remain local unless a separate sync feature is approved, so that UI replacement does not introduce server storage.
  22. As an assistant user, I want sending a message to update the transcript through one assistant hook, so that the screen does not coordinate a separate transcript store.
  23. As an assistant user, I want selecting an ambiguous place to retry the last applicable request through assistant logic, so that retry behavior remains consistent across presentations.
  24. As an assistant user, I want providing my current location to retry the pending location-dependent action without sending coordinates to the language model, so that location handling keeps its existing privacy boundary.
  25. As an assistant user, I want known action shortcuts to avoid a model call, so that deterministic actions remain fast and predictable.
  26. As an assistant user, I want malformed model output and network failures to produce unknown or unavailable states, so that the new UI cannot display fabricated transit facts.
  27. As an assistant user, I want starter prompts and follow-up actions to remain product-owned data, so that their action IDs and retry behavior are not hidden inside button components.
  28. As a rider using live tracking, I want the vehicle query to preserve its current refresh and stale-time behavior, so that changing the list or map presentation does not change network cadence.
  29. As a rider using transit intelligence, I want route profiles, route service state, slowdowns, stop arrivals, and vehicle profiles exposed as query options, so that screens can compose the exact live data they need.
  30. As a rider using transit intelligence, I want unused intelligence endpoints omitted from the UI import list, so that the future UI does not inherit speculative capabilities.
  31. As an offline rider, I want a cached and verified planner image to remain usable without a route-calculation network request, so that presentation deletion does not break offline planning.
  32. As an offline rider, I want online state and PWA update state returned as data, so that the future UI can choose its own accessible notification components.
  33. As a rider, I want service-worker update logic to avoid importing toast or translation components, so that PWA behavior remains usable outside the current shell.
  34. As a rider, I want native sharing and clipboard fallback to return a typed outcome, so that each screen does not implement browser capability checks and error handling.
  35. As a rider, I want Google Maps and Apple Maps links built from one product function, so that stops, landmarks, and trip results use the same URL rules.
  36. As a rider, I want changing language to validate and persist a supported language, so that settings presentation cannot write unsupported values.
  37. As a rider, I want clearing local data to clear activity, persisted queries, selected local-storage records, and the named app-data caches through one command, so that the UI cannot leave contradictory in-memory and persisted state.
  38. As a maintainer, I want the browser database below feature logic, so that query persistence does not import an activity implementation.
  39. As a maintainer, I want one transit index, so that route, stop, and direction lookup do not use competing shapes or direction conventions.
  40. As a maintainer, I want assistant operations private behind one hook, so that screens cannot call wire, session, dispatch, or template internals out of order.
  41. As a maintainer, I want trip worker protocol and provider clients private behind planner logic, so that screens cannot bypass validation or request ordering.
  42. As a maintainer, I want packages/contracts and packages/transit to remain the only shared packages involved, so that this work does not create a package without a second real application consumer.
  43. As a maintainer, I want imports to point in one direction from UI to features to data and domain, so that presentation deletion cannot break lower layers.
  44. As a test author, I want existing behavior tests to move with their owning logic, so that the refactor does not replace useful tests with implementation-detail tests.
  45. As a test author, I want the two deep public hooks tested at their public interface, so that planner and assistant orchestration can change internally without rewriting screen tests.
  46. As a release reviewer, I want typecheck, focused web tests, and the existing transit tests reported separately, so that one passing command is not treated as proof of the whole change.
  47. As a future Native or second-app maintainer, I want web-only behavior to stay in apps/web until my application is an approved consumer, so that package boundaries are based on actual platform requirements.

Implementation Decisions

Governing consumption rule

The future UI may import only documented feature hooks, functions, stores, query options, and types that encode product behavior. It may not import internal implementation modules.

The UI may not import CSS, markup, route layout, icons, animation configuration, display-only formatters, QR renderers, MapLibre sources or layers, map-camera commands, or GeoJSON produced only to draw a result.

Feature logic may depend on other feature logic where the product operation requires it. It may also depend on app data, packages/contracts, and packages/transit. It may not depend on components, styles, icons, renderers, or route modules.

Route modules and app bootstrap are outer application boundaries. They may compose future screens with feature imports, but no feature or data module may import a route.

Dependency direction

The required value-import direction is:

  1. Future UI and route boundary import feature public interfaces.
  2. Bus-route, bus-stop, jeepney-route, bus-tracker, assistant, activity, and trip-planner features import app-local transit data or app data as needed.
  3. App-local transit data imports the typed API client and packages/transit schemas.
  4. Assistant imports packages/contracts, transit data, and the trip-planner planning operation.
  5. Trip planner imports packages/transit, app data, activity logic, and provider clients.
  6. packages/contracts and packages/transit import no web application code.

There is no reverse dependency from domain, data, or feature logic to presentation.

Package boundaries

Keep packages/contracts. It has a real API and web consumer and owns the assistant values that cross that boundary. Do not move app response rendering, browser session state, or assistant hooks into it.

Keep packages/transit. It has real API, web, collector, and script consumers. It remains the owner of transit schemas, directed services, route and stop identity, fares, Davao place-domain search, trip identity, planner contracts, image format, mount logic, search, selection, and worker protocol.

Do not create another package. The web planner hook, query options, provider place search, browser storage, PWA lifecycle, and assistant conversation state all have web-platform assumptions or only one application consumer.

Remove packages/ui as already decided. No retained product logic may move into it before removal.

If publishing web feature logic, supporting another application, or supporting Native becomes part of the work, stop and reopen this decision. Do not prebuild a portable interface now.

App boundary and data ownership

Keep route validation, route search-parameter schemas, loader prefetch, existence checks, redirects, and not-found decisions at the app boundary. Remove route-owned layout and presentation with the current UI. A loader may call public query options, but it must not duplicate query keys or fetch policy.

Move the typed API client, browser database, query client, and query persister below features in an app data area. This reflects actual use. The database supports both activity records and persisted query state, so activity is not its sole owner.

Keep one browser database and its current tables. Do not split databases merely to make folders look independent. The correction is dependency direction, not a storage migration.

Keep error monitoring and environment-backed feature flags at the app runtime boundary. Remove map-only error categories and map-only persisted preferences when their only consumer disappears.

Transit data interface

Keep two immutable bundle query families. One loads the transit manifest and versioned transit bundle. The other loads the jeepney manifest and versioned jeepney bundle. Both preserve current last-good-version fallback and offline-first behavior.

Keep suspense bundle hooks for screens and feature logic that require a loaded bundle. Keep one transit index that provides the bundle, routes by ID, stops by ID, stops grouped by name, and ordered route-stop lookup.

Merge the current competing transit indexes. The retained index uses one direction conversion rule and returns raw route, stop, and service data. Do not retain route-geometry indexing unless a non-rendering consumer uses it.

Keep only intelligence query options with current callers: route profile, route service state, route slowdowns, road slowdowns, stop arrivals, and vehicle profile. Do not expose readiness, road readiness, vehicle ETA, or stop profile until a screen or loader calls them.

Delete the map tile-manifest query family and tile-manifest type from web logic. Their current web use exists only to feed the deleted renderer.

The transit-data public imports are the transit and jeepney bundle query families, the used intelligence query options, the two bundle hooks, the transit index hook, the transit-index type, and the route-direction type.

Bus-route behavior

Move route search out of the route screen. Search continues to consider route ID, canonical route name, route endpoints, route detail text when it remains domain data, and the ordered stop names from both directional services.

The public search function returns raw transit routes in product order. It does not expose Fuse result objects or character match indices. Search highlighting is presentation.

Expose one ordered-stop lookup that accepts the transit index, route ID, and app route direction, then returns raw transit stops. Do not preserve a helper that returns only formatted names when the canonical stops are already available.

Delete route-name parsing from retained logic when its only purpose is to split a name into display labels. A route remains identified by its canonical ID and name.

Bus-stop behavior

Move stop search out of the stop screen. It searches canonical stop name and location and returns raw transit stops.

Expose duplicate-name detection because it controls whether a rider needs location disambiguation. Return the boolean fact only. The wording remains presentation.

Expose serving-service derivation. It parses canonical route references, preserves AM and PM direction, removes duplicate service references, and resolves the route name when available. Its result contains route identity, route name, and direction, with no icon or row data.

Jeepney-route behavior

Keep direct jeepney route search, Davao place matching, dominant-place selection, serving-route lookup, and LineString distance calculation with the jeepney-route feature.

Direct route search returns route names, not character indices. Davao place matching returns domain search candidates and a dominant place. Serving-route lookup returns the existing ServingJeepneyRoute domain values.

Keep the numeric route distance because a screen can use it as product data. Delete kilometre and metre string formatters.

Do not move Davao place-domain search out of packages/transit. The web feature calls that existing shared algorithm. The feature owns only its screen-relevant orchestration.

Activity interface

Keep the current activity record schema unless implementation discovers a behavior mismatch. This architecture change does not redesign saved data.

Expose one activity hook with recent trips, saved trips, add recent, save, unsave, saved-state lookup, and toggle. Expose the activity-trip type.

Keep database loading available to the app loader, but do not expose the raw Dexie database to screens. Do not expose the raw Zustand store to screens when the activity hook already provides the required operations.

Use the canonical transit trip identity when the planner records or saves a planned itinerary. Do not rebuild identity from display rows.

Trip-planner interface

Create one deep planner hook as the screen entry. It owns endpoint state, result state, selected itinerary, planning state, provider suggestions, place selection, map-coordinate commits, swapping, planning, editing, selected-result changes, recent recording, saving, unsaving, and saved-state lookup.

The hook returns raw TripResult, Trip, location, suggestion, and activity state. It does not return formatted legs, formatted distance, display mode labels, icon names, CSS classes, map GeoJSON, or camera instructions. This is a product-operation hook, not a view-model.

Move trip location ownership out of map state. Preserve the distinction among empty, text, provider-search, and map-selected locations. Preserve planning revision or an equivalent internal mechanism only if the hook needs it to replan after a committed location change.

Keep provider autocomplete and retrieval under trip planner because no second feature currently consumes them. Preserve minimum query length, debounce, provider priority, session failover, abort handling, and stale-pick suppression. Provider attribution copy and secondary display-line formatting are not logic.

Keep the planner preparation client, image transport, and worker private. Preserve the accepted planner-image contract, page and worker digest checks, one shared worker, readiness failure, one pending request, request supersession, and stale-response rejection.

Keep trip-share link creation public because a result screen calls it. It accepts a base URL and shareable endpoints, emits a URL, and reports whether exact coordinates are present. Preserve the rule that only map-selected endpoints add coordinates.

The planner hook records a recent trip after a tripFound result. It uses activity logic and canonical trip identity. It does not create a recent entry for planner unavailable, same place, outside service area, too far, or no connecting route.

The planner hook owns selected-trip save and unsave commands because constructing the persisted record from a raw trip is product behavior. The general activity hook remains public for home or activity screens.

Delete the trip display conversion, display schema, data-warning string assembly, distance formatter, trip route GeoJSON builder, result-message helpers, result panels, cards, and route-track rendering from retained logic.

Assistant interface

Create one assistant hook as the screen entry. It owns assistant status, transcript turns, message sending, known action execution, place selection, current-location updates, UI language, and conversation reset.

Sending a message appends the user turn, calls the assistant intent endpoint once, validates and dispatches the accepted intent, and appends the structured assistant turn. Known action IDs continue to bypass inference.

Move transcript mutation into the assistant hook. The screen must not coordinate a separate transcript store, append assistant results, remember the last request, or decide when place and location selection retry the request.

Place selection and location updates preserve current retry behavior. Current location remains in the browser session and does not enter the assistant wire body. Reset clears transcript, response state, selected place, location, conversation ID, and pending retry context.

Keep the structured assistant response union public. Keep assistant status, transcript turn, action ID, prompt, follow-up, location, and language types public when screens need them.

Move starter prompts, examples, and follow-up action definitions out of TSX and into assistant product data. They may carry translation keys and deterministic action IDs. They do not carry component types, icons, or style choices.

Keep chat transport, protocol errors, session implementation, dispatch, intent mapping, place resolution, route operations, trip operations, vehicle snapshot access, FAQ behavior, and template construction private.

Preserve deterministic authority. The assistant model may extract an intent but may not provide route IDs, stop IDs, coordinates, fares, schedules, ETAs, or vehicle positions as facts.

Bus-tracker and intelligence interfaces

Keep the vehicle query with the bus tracker. Preserve its current stale time and refresh interval. It returns the typed API response without movement strings, relative-time strings, route endpoint labels, or list-item state.

Keep route, stop, road, and vehicle intelligence query options with transit data because several features consume them. Screens choose the specific query option they need and render the typed result.

Delete marker validation, movement animation, marker selection, and map-store state when they exist only for the deleted renderer. Vehicle selection may remain local screen state unless another retained product operation proves shared ownership.

Localization and local-data settings

Keep catalogs, i18next type augmentation, supported-language validation, browser-language detection, and initialization in the i18n area.

Expose supported languages, the supported-language type, and one language-change function. The function validates the language, calls i18next, and persists the accepted code. A settings screen must not write local storage directly.

Expose one clear-local-data command. It clears activity records, persisted query records, the in-memory activity state, the query client, last-good bundle versions, and the named app-data caches covered by the current product promise.

Do not claim that this command removes the Workbox precache or every browser cache unless the implementation and existing privacy documentation change together. Remove deleted map-filter storage from the command when the map preference no longer exists.

PWA and platform interfaces

Keep the PWA lifecycle hook, but make it return state and commands. It reports offline readiness and update availability, lets the app acknowledge offline readiness, and exposes the service-worker update command.

The hook may depend on the PWA registration integration and browser service-worker APIs. It may not translate messages, invoke toast, or choose notification actions.

Keep online status as a small browser hook when the future UI still calls it. Do not introduce a network-status store.

Replace the low-level public Web Share and clipboard capability helpers with one screen-called operation. It returns shared, copied, dismissed, or unavailable. It does not show a toast or translate an outcome.

Extract Google Maps and Apple Maps URL construction from the current dialog. Expose provider, place, and request types plus one link function. Delete provider rows, labels, QR rendering, dialog behavior, and styles.

Public imports

The target UI import list is deliberately small:

FeatureUI-callable imports
Transit dataTransit bundle queries, jeepney bundle queries, used intelligence query options, transit bundle hook, jeepney bundle hook, transit index hook, transit index type, route direction type
Bus routesRoute search and ordered route-stop lookup
Bus stopsStop search, duplicate-name detection, serving-service lookup, serving-service type
Jeepney routesJeepney route search, Davao place search hook, serving-route hook, numeric route-distance function, place-search result type
Bus trackerVehicle query options
ActivityActivity hook and activity-trip type
Trip plannerPlanner hook, place-suggestion hook, endpoint/location/suggestion/result/share types, trip share-link function
AssistantAssistant hook, assistant prompt collections, follow-up lookup, assistant status/turn/response/action/prompt/follow-up/location/language types
LocalizationSupported languages, supported-language type, language-change function
SettingsClear-local-data command
PWAPWA lifecycle hook and online-status hook
Browser capabilitiesShare operation and outcome type, external-map link function and request types

The target UI must not import these implementation details:

  • Typed API client
  • Browser database or persistence tables
  • Query-client factory or persister
  • Assistant chat transport, session, dispatch, operation, validation, or template modules
  • Planner preparation client, image transport, worker, or worker protocol
  • Provider router or provider clients
  • Raw Zustand stores when a feature hook exists
  • Route modules from feature logic
  • Map stores, map bridges, sources, layers, projections, camera effects, marker motion, or renderer types
  • CSS, markup, icons, animation constants, display formatters, or design-system internals

Deletions from retained logic

Delete all renderer-only map code from the retained logic set. This includes tile-source selection, map projections, GeoJSON projection, source and layer definitions, camera planning, marker motion, renderer adapters, basemap filters, map colors, and map-specific persisted state.

Delete display-only helpers from retained logic. This includes distance strings, service-date strings, relative-time strings, movement prose, route-name splitting used only by cards, provider attribution prose, secondary-line formatting, trip display records, UI warning aggregation, and match-highlight indices.

Delete all assistant components and widgets while retaining their product commands, response contracts, and deterministic operations.

Delete activity tabs and all route, stop, tracker, jeepney, planner, settings, PWA-notification, map-provider, and assistant presentation modules as part of the separately approved UI deletion. This specification does not define their replacements.

Do not keep compatibility exports for deleted module paths. Callers move to the target public imports in the same implementation change.

Tradeoffs

One planner hook owns more behavior than the current page-level split. That is intentional. Its public interface is smaller than the workflow it hides, and the workflow already changes as one unit.

One assistant hook similarly owns transcript and retries. This removes the ability for a screen to mutate transcript independently. That independent mutation is not a supported product requirement.

Moving the database below features makes its table definitions less feature-local. It fixes the more important dependency problem because both activity and query persistence already use the same database.

Feature public entry modules add a small amount of export boilerplate. Use them only for multi-file features where they prevent UI imports of internals. Do not add entry modules to one-function areas for visual symmetry.

Returning raw domain values means the future UI must format dates, distances, movement, and labels. That is the intended split. Product logic retains facts and commands, while presentation owns words and visual representation.

Keeping web behavior in apps/web means a future Native app cannot import it directly. That limit is preferable to publishing browser-specific worker, CacheStorage, Dexie, i18next, and Web Share assumptions as a false cross-platform contract.

Testing Decisions

Test seam

Use the highest existing behavior seam for each retained operation. The architecture has two deep screen-called seams that need direct tests: the trip-planner hook and the assistant hook. Pure route, stop, jeepney, share-link, provider-router, activity-schema, and planner-image behavior already has focused tests near the owning logic. Package transit invariants remain covered by packages/transit tests.

Do not create test-only interfaces. Do not test private file placement, internal hook decomposition, Zustand implementation, Dexie call order, React Query internals, or worker message assembly when a public behavior test can prove the same requirement.

Planner verification

Retain the existing planner-image transport, provider place-search, picked-location, share-link, activity schema, and transit planner tests.

Add or retain one high-level planner-hook success test only if existing tests cannot prove the extracted behavior. It should plan valid endpoints, return the raw trip result, select the recommended itinerary, and add one recent activity entry.

Add one planner-hook failure or supersession test only if the preparation client tests do not already prove it. A newer request must prevent an older result from replacing current state, or an image/worker failure must become planner unavailable. Do not add both unless both behaviors change during implementation.

Do not recreate tests for trip display conversion, map GeoJSON, map camera fitting, or result markup. Those modules leave retained logic.

Assistant verification

Retain current chat transport, intent, identity, dispatch, and domain tests. They are prior art for the deterministic boundary and safe fallback behavior.

Replace transcript-store and widget-level orchestration tests with at most one assistant-hook main-path test and one key failure-path test if the extraction changes behavior. The main path should prove that sending one message produces one user turn and one structured assistant turn. The failure path should prove unavailable behavior or retry after required context, whichever is not covered by existing operation tests.

Do not test component chips, starter layout, transcript markup, or widget selection after presentation deletion. Test prompt and follow-up product data as plain values only when a regression in action IDs would otherwise go undetected.

Transit and search verification

Retain current bus-route and jeepney-route behavior tests. Move assertions to the raw public search results and ordered-stop/service lookup. Remove assertions that exist only for formatted labels or match-highlight indices.

Keep packages/transit tests as the authority for directed services, route and stop identity, serving-route distance, fare behavior, planner image, access limits, transfers, candidate selection, and canonical trip identity. The web refactor must not duplicate those tests.

Use the current vehicle and intelligence query contracts without adding query-library implementation tests. Typecheck and existing API tests already prove the typed response boundary. Add a focused web query test only if the implementation changes cache keys, stale time, refresh interval, response validation, or fallback behavior.

Activity, settings, PWA, and platform verification

Retain the activity schema test. Add a store test only if moving database ownership changes observable recent or saved behavior.

Keep the share-link privacy test that distinguishes named endpoints from map-selected coordinates. If the shared browser share operation is introduced, one focused test may cover native share, clipboard fallback, and unavailable outcome only when these branches cannot be proved through the current helper tests. Do not add a large browser-capability matrix.

Test external-map link construction as a pure function if the extracted links have no existing coverage. One table with the current Google and Apple place or direction cases is enough. Do not test QR rendering.

Do not add PWA test infrastructure solely for this refactor. Preserve existing registration behavior, typecheck the returned interface, and verify update/offline state manually in the existing app runtime if no current seam can exercise the virtual PWA module cheaply.

Dependency verification

Run a read-only import audit after the implementation. No retained feature or data module may import UI paths, CSS, icons, motion packages, map renderers, or route modules. No app data module may import a feature. No package may import apps/web.

Use the existing web typecheck as the main compile-time proof that screens, loaders, and feature interfaces agree. Run focused web tests for moved logic. Run the transit package tests because planner and transit types remain dependencies. Run root checks only if the implementation changes package exports or crosses existing workspace boundaries.

Report each check separately. A passing typecheck does not prove planner behavior, and a passing package test does not prove import direction.

Acceptance conditions for testing

The implementation is acceptable when:

  1. Existing behavior tests for retained logic pass or move with their owner.
  2. The public planner and assistant operations have no untested orchestration behavior introduced by the extraction.
  3. The web application typechecks against the new public imports.
  4. The transit package tests still pass without new web-specific cases.
  5. The import audit finds no reverse dependency from logic or data to presentation.
  6. No new test framework, snapshot suite, broad matrix, or test-only abstraction was added.

Out of Scope

  • Implementing any move, merge, deletion, or public interface described here.
  • Migrating callers in this documentation task.
  • Designing, building, or selecting replacement screens.
  • Creating or changing design-system components.
  • Defining CSS, visual hierarchy, layout, typography, icons, animation, map styling, or interaction polish.
  • Preserving the current presentation for compatibility.
  • Preserving old import paths or transitional APIs.
  • Creating a view-model, adapter, facade, repository class, service class, or generic feature framework.
  • Replacing TanStack Query, Zustand, Dexie, i18next, the PWA integration, Fuse, or the planner worker.
  • Changing transit topology, planner policy, transfer limits, access limits, selection rules, fares, or service-area behavior.
  • Changing the planner image format, publication process, digest checks, worker protocol, or Workbox caching contract.
  • Changing assistant request schemas, intent kinds, model prompting, API rate limiting, deterministic authority, or response facts.
  • Changing the vehicle API, intelligence API, collector, polling cadence, ETA evidence, or road-intelligence rules.
  • Redesigning the activity schema, introducing account sync, or uploading saved trips.
  • Changing location privacy, coordinate-sharing policy, provider priority, or provider credentials.
  • Claiming that clear-local-data removes Workbox precache or every browser cache.
  • Publishing web feature logic for another application.
  • Supporting Native consumption.
  • Adding a new workspace package.
  • Removing packages/ui in this documentation task. Its removal is a known implementation requirement handled with the presentation deletion.
  • Adding tests for deleted presentation or filling historical test coverage gaps.
  • Publishing this specification to GitHub or any issue tracker.

Further Notes

Handoff context

The current presentation is disposable. Treat every component, route layout, stylesheet, icon binding, animation, map renderer, and display helper as absent when judging retained ownership. Keep behavior only when a route loader, screen, app bootstrap, or another retained operation calls it.

The retained owners identified by the code audit are route validation and loaders, assistant operations, activity state, planner worker use, transit and intelligence queries, localization, PWA lifecycle, provider place search, browser sharing, and external-map URL generation.

The current code already contains most of the required implementation. The work is mainly extraction, deletion, and dependency correction. Do not rewrite transit algorithms, assistant dispatch, worker transport, provider clients, or activity persistence to make the new directory structure look cleaner.

Source disposition ledger

This ledger covers every current source that contains retained product behavior or owns a dependency used by that behavior. Files that contain only markup, styles, icons, animation, map rendering, or display formatting are covered by the approved presentation deletion and are not listed individually.

Current sourceRetained behaviorTarget owner and pathDecisionCurrent callers or proof
app/main.tsxRouter creation, query-client persistence, i18n initialization, and runtime mountingapp/main.tsxKeep the runtime composition. Remove current CSS and presentation imports when the UI is deleted.Application entry and web typecheck
app/routes/*.tsxSearch-parameter schemas, loader prefetch, route and stop existence checks, redirects, and not-found decisionsapp/routes/*Split. Keep route behavior and replace only the deleted route components later. Loaders import feature query options.Current route loaders call transitQueries and jeepneyQueries; route schemas use Zod.
shared/api/client.tsTyped Hono client constructiondata/api-client.tsMove. Keep private to app data and feature implementations.All network query and assistant transport callers
shared/api/queries.tsTransit and jeepney bundle queries, used intelligence queries, vehicle query policy, response parsing, and last-good fallbackfeatures/transit-data/bundle-queries.ts, features/transit-data/intelligence-queries.ts, and features/bus-tracker/vehicle-queries.tsSplit by current use. Delete mapQueries and BusRouteTileManifest because they only feed the map renderer.Route loaders, route and stop intelligence screens, tracker route, planner preparation, and assistant transit operations
shared/lib/query-client.tsOne web query client and its current defaultsdata/query-client.tsMove without wrapping it. It is app runtime data, not a screen import.app/main.tsx, planner preparation, settings clear flow
shared/lib/query-persister.tsTanStack Query persistence backed by the shared browser databasedata/query-persister.tsMove and change its database import to the data owner.app/main.tsx; current reverse dependency on features/activity/activity.db.ts
features/activity/activity.db.tsDexie database, activity table, and persisted-query key-value tabledata/browser-db.tsMove the database. Keep one database and the current tables.Activity store and query persister
shared/lib/client-env.ts and shared/lib/error-monitoring.tsEnvironment reads, privacy-safe error reports, listener installation, and report deduplicationdata/client-env.ts and app/error-monitoring.tsMove environment access below features so the API and provider clients do not import the outer app runtime. Keep monitoring at the app boundary. Delete the map-only error category after the renderer is removed.API client, provider router, root route
shared/types/domain.tsRoute direction, activity constants, feature flags, place-search domain types, and search constantsTheir actual owners under features/transit-data, features/activity, data, and features/trip-planner/place-searchSplit. Delete TransitActionId if it remains only a navigation-display identifier. Delete provider attribution and secondaryLine.Queries, activity store, home presentation, and place-search clients
shared/hooks/use-transit-bundle.ts, use-jeepney-bundle.ts, and use-transit-index.tsLoaded bundle hooks and route/stop lookup indexfeatures/transit-data/bundle-hooks.ts and features/transit-data/transit-index.tsMove and merge with the non-rendering parts of map/pipeline/indexing.ts.Route, stop, jeepney, planner, and assistant callers; existing bus-route tests
map/pipeline/indexing.tsRoute, stop, direction, and ordered-stop indexingfeatures/transit-data/transit-index.tsMerge with use-transit-index.ts. Delete route-geometry indexing unless a retained caller remains.Map pipeline tests and current map store; future route and stop features use the merged index.
shared/hooks/use-search-filter.ts, features/bus-routes/page.tsx, and features/bus-routes/bus-routes.utils.tsBus-route relevance across route facts and ordered stop namesfeatures/bus-routes/search.tsSplit the search behavior from the page. Return raw routes. Delete Fuse match indices and parseRouteName.features/bus-routes/bus-routes.test.ts and current route list
features/bus-routes/details-page.tsx and shared/hooks/use-transit-index.tsOrdered route-stop lookup for a route and directionfeatures/bus-routes/route-stops.tsSplit the raw lookup from the details page. Delete service-date formatting, local display labels, geometry selection, and disclosure behavior.Route details and route-stop tests
features/bus-stops/page.tsxStop relevance by canonical name and location, plus duplicate-name detectionfeatures/bus-stops/search.tsSplit raw search and disambiguation facts from the page. Delete Fuse result metadata.Current stop list and use-transit-index.ts duplicate-name helper
features/bus-stops/stop-details-page.tsxCanonical serving-service derivation and share request constructionfeatures/bus-stops/stop-services.ts and platform/share.tsSplit. Keep raw service facts and use the shared share operation. Delete dialog, toast, and row behavior.Current stop details route
features/jeep-routes/route-search.ts, place-search.ts, serving-routes.ts, and route-geometry.tsRoute-name relevance, Davao place matching, serving routes, and numeric route distanceMatching files under features/jeep-routesKeep or rename in place. Remove match indices and all distance strings. Rename screen-called hooks only where the public import list requires it.features/jeep-routes/jeep-routes.test.ts and current jeepney pages
features/jeep-routes/landmark-utils.ts and features/transit-intelligence/format.tsNo retained product behaviorNoneDelete from logic. They produce translated or display-ready strings only.Presentation callers only
features/activity/activity.schema.ts, activity.store.ts, and use-activity.tsActivity validation, load, recent cap, duplicate handling, save, unsave, and saved-state lookupSame files under features/activity, with an export whitelist in features/activity/index.tsKeep. Change the store to import data/browser-db.ts. Keep the raw store private except for app initialization.activity.schema.test.ts, root loader, activity tabs, and planner
features/trip-planner/page.tsxPlanning state, prewarm, request validation, latest-request-wins behavior, raw result state, recent recording, selected result, edit/reset, and save orchestrationfeatures/trip-planner/use-trip-planner.tsSplit all product behavior into the planner hook. Delete animation, translations, map effects, and result component selection.Current planner page; planner-image and transit planner tests cover its dependencies.
features/trip-planner/form.tsx, picked-location.ts, map/state/trip-planner-location-context.tsx, and map/state/trip-planner-location-types.tsEndpoint state, text invalidation, provider retrieval, stale-pick rejection, coordinate commits, swapping, and initial valuesfeatures/trip-planner/trip-locations.ts and features/trip-planner/use-trip-planner.tsSplit planner behavior from form and map state. Keep pickedTripLocation private. Delete focus, attribution, field order, and markup behavior.picked-location.test.ts, location-context test, form, planner page, and map bridge
shared/hooks/use-place-suggestions.ts, use-debounced-value.ts, shared/place-search/provider-router.ts, mapbox-search-box.ts, geoapify-search.ts, and place-search.tsDebounce, minimum query length, provider priority, session failover, abort behavior, retrieval, and suggestion typesfeatures/trip-planner/place-search/*Move as planner internals except usePlaceSuggestions and screen-needed types. Delete attribution and secondary-line formatters.use-place-suggestions.test.tsx, provider-router.test.ts, and planner form
features/trip-planner/trip-prepare-client.ts and trip-worker.tsVerified image preparation, worker mounting, prewarm, one pending request, supersession, and stale-response rejectionSame names under features/trip-plannerKeep private. The assistant may call one private planning operation; screens do not import the worker protocol.Planner page, assistant hook, and planner-image tests in packages/transit
features/trip-planner/share-link.tsShare URL construction and exact-coordinate disclosure flagSame path under features/trip-plannerKeep public.share-link.test.ts and trip result sharing
features/trip-planner/trip-display.ts, trip.schema.ts, result-message helpers, and map projection helpersNo retained product interfaceNoneDelete from logic. The future UI consumes raw TripResult and Trip; shared fare and transit facts remain in packages/transit.Current result components and display-only tests
assistant/chat-api.ts, contracts.ts, dispatch.ts, domain.ts, faq.ts, identity.ts, intent.ts, live.ts, place-resolution.ts, route-operations.ts, session.ts, template-copy.ts, templates.ts, trip-operations.ts, and vehicles-api.tsAssistant wire handling, validation, deterministic execution, session state, transit operations, and structured responsesMatching private files under features/assistantMove as one feature. Only contracts and screen-needed response types may be re-exported.Existing chat, dispatch, domain, identity, and intent tests
assistant/use-assistant.ts, assistant/transcript-store.ts, and assistant/components/assistant-dock.tsxStatus, transcript mutation, last applicable request, retries after place or location, failures, language, and complete resetfeatures/assistant/use-assistant.tsMerge into one hook. Delete the separate public transcript store and all dock presentation.Current dock is the only place that joins these behaviors.
assistant/components/starter-view.tsx and follow-up-chips.tsxStarter prompts, examples, follow-up mapping, translation keys, and deterministic action IDsfeatures/assistant/prompts.tsSplit product data and lookup from TSX. Delete the rendered buttons and layout.Current assistant starter and response widgets
features/pwa/use-pwa-lifecycle.tsRegistration state, hourly online update checks, update command, and offline-ready acknowledgementfeatures/pwa/use-pwa-lifecycle.tsSplit toast and translation calls out. Return state and commands.Root route; no focused current test
shared/hooks/use-online-status.tsBrowser online and offline event statefeatures/pwa/use-online-status.tsMove and keep public while screens call it.App shell and planner unavailable states
shared/i18n/config.ts, i18next.d.ts, and locale catalogsInitialization, supported-language validation, detection, persistence key, and typed catalogsi18n/*Move. Add the screen-called setLanguage operation beside initialization.app/main.tsx and settings dialog
shared/lib/clear-local-data.ts and the clear flow in features/settings/settings-dialog.tsxCoordinated deletion of activity, persisted queries, query memory, last-good versions, named app-data caches, and in-memory activityfeatures/settings/clear-local-data.tsMerge into one command. Delete confirmation timing, translations, messages, and direct store mutation from logic.Current privacy settings flow
shared/lib/web-share.ts and share handlers in stop and trip result filesNative share, clipboard fallback, dismissal detection, and typed outcomeplatform/share.tsMerge into one public operation. Delete toast and translation behavior.Stop details and trip results
map/components/map-provider-dialog.tsxGoogle and Apple place and direction URL constructionplatform/external-maps.tsSplit createMapLink and its request types from the dialog. Delete provider rows, labels, QR code, icons, and styles.Stop details, jeepney landmarks, and trip results
map/** except the trip-location and indexing items named aboveNo retained product behavior after presentation deletionNoneDelete from logic. This includes stores, bridges, projections, sources, layers, camera effects, marker motion, renderer adapters, colors, filters, and map persistence.Current map renderer and map-only tests

Caller-visible interface contracts

These rules are part of each module's interface. A future screen must not infer them from implementation files.

Public modulePreconditions and legal statesCompletion, failure, and side effects
Transit and intelligence queriesCallers provide canonical IDs and RouteDirection. The query definitions own keys, enabled conditions, freshness, polling, retry, response parsing, and offline fallback. Routes and screens must not recreate those options.Network and validation failures remain TanStack Query errors. Bundle fallback stays internal and never exposes the browser database or last-good storage key.
useActivityActivityTrip.id is the canonical duplicate and saved-state key. addRecent, saveTrip, unsaveTrip, and toggleSave may be called repeatedly.Memory updates immediately. Persistence remains internal. Re-adding a recent moves it to the front and applies the current cap; saving an existing ID is a no-op; unsaving an absent ID is a no-op. Database loading is an app-boundary concern, not a screen command.
usePlaceSuggestionsSearch is disabled until the trimmed query reaches the owned minimum length and enabled is true. canSearch tells the screen whether a request is eligible.The hook owns debounce, cancellation, stale query suppression, caching, and provider failover. An unavailable provider produces { kind: "unavailable" }; it does not throw into the screen or expose provider errors.
useTripPlannerA location is plannable only when both endpoints have coordinates. Updating endpoint text clears its prior coordinates and resolved place. Selecting a suggestion commits only the latest retrieval for that endpoint. The hook exposes only three legal planning combinations: idle with no result, planning with no result, or settled with one raw TripResult.plan() returns without starting work when either coordinate is absent. Starting a plan clears the prior result. A newer request supersedes the older request and stale completion cannot mutate state. Preparation or worker failure settles as plannerUnavailable; normal no-trip outcomes remain their existing TripResult variants. Only tripFound records a recent trip. edit() cancels the active request and clears planning and result state. Save commands require a selected trip and use canonical trip identity.
buildTripShareUrlThe caller supplies a base URL and named endpoints. Coordinates may appear only for endpoints whose source is map.The function is pure. It returns the URL plus hasExactCoordinates. The screen owns confirmation and wording before sharing.
useAssistantsendMessage and runAction are called only while status is idle; no overlapping-command behavior is promised. Known actions bypass model inference. Place and location commands apply only to the current session and pending retry context.Sending appends one user turn before inference and one structured assistant turn after deterministic execution. Expected transport or protocol failure sets unavailable without a fabricated assistant result. Place selection or current-location update retries the last applicable request inside the hook. Current coordinates never enter the model request. reset() clears transcript, response, status, selected place, location, conversation ID, and retry context.
setLanguageAccept only SupportedLanguage. Initialization separately validates stored and browser-derived strings before constructing that type.Change i18next first, then persist the accepted code. Reject if the language change fails and do not persist a value the runtime did not accept. The function does not translate or display confirmation.
clearLocalDataThe UI obtains confirmation before calling. The command itself has no confirmation state and is safe to call more than once.Clear the activity and persisted-query tables, query client, in-memory activity, last-good bundle keys, and named app-data caches. Await every owned deletion. Reject on failure so the UI cannot claim success. The command does not promise to remove Workbox precache or unrelated browser caches.
usePwaLifecycleofflineReady remains true until acknowledgeOfflineReady() runs. updateAvailable remains true until the registered update is applied or the runtime changes it.Hourly checks run only while online and a registration exists. applyUpdate() delegates to the registered service worker and rejects on update failure. The hook never imports translation or toast behavior.
useOnlineStatusNo arguments. In a non-browser environment it starts as online, matching current behavior.Browser online and offline events update the value. The hook creates no shared store and performs no network probe.
shareContentThe caller supplies browser ShareData. The operation may use native share when available, otherwise clipboard.Native success returns shared. Clipboard success returns copied. Native AbortError returns dismissed and does not copy. Missing capability or non-dismissal failure returns unavailable. It never shows or translates feedback.
createMapLinkThe request contains either a place or a destination; origin is optional. The caller chooses google or apple.Return a deterministic encoded URL. A place request takes precedence over direction fields. Provider labels, QR rendering, and opening a new tab are presentation.

Allowed UI and loader imports

The @/ alias continues to resolve to apps/web/src. Each listed module is an allowed import seam. Screens and route loaders must not deep-import another file beneath these feature directories.

Import pathUI-callable exportsRoute-loader use
@/features/transit-datatransitQueries, jeepneyQueries, used intelligenceQueries, useTransitBundle, useJeepneyBundle, useTransitIndex, TransitIndex, RouteDirectionBundle and used intelligence query options
@/features/bus-routessearchBusRoutes, getRouteStopsNone
@/features/bus-stopssearchBusStops, servicesForStop, hasDuplicateStopName, StopServiceNone
@/features/jeep-routessearchJeepRoutes, useDavaoPlaceSearch, useServingJeepneyRoutes, calculateLineStringDistanceMeters, PlaceSearchResultsNone
@/features/bus-trackervehicleQueriesNone
@/features/activityuseActivity, ActivityTripNo screen import for initialization. The root loader calls a private activity-load operation through the app boundary.
@/features/trip-planneruseTripPlanner, usePlaceSuggestions, buildTripShareUrl, and the planner, location, suggestion, result, and share types listed in Public surfaceNone
@/features/assistantuseAssistant, assistantStarters, assistantExamples, followUpsFor, and the assistant types listed in Public surfaceNone
@/i18nsupportedLanguages, setLanguage, SupportedLanguageNone
@/features/settings/clear-local-dataclearLocalDataNone
@/features/pwausePwaLifecycle, useOnlineStatusNone
@/platform/shareshareContent, ShareOutcomeNone
@/platform/external-mapscreateMapLink, MapProvider, MapPlace, MapLinkOptionsNone

An index.ts at a listed feature path is an export whitelist. It contains no behavior and re-exports no internal transport, store, provider client, worker, persistence, renderer, or display helper. The direct settings and platform paths stay direct because adding entry modules to one-operation areas would add no protection.

Acceptance criteria

  1. A future screen can implement each current product operation using only the documented UI-callable imports and design-system components.
  2. No retained logic imports components, CSS, icons, animation, map rendering, or route layout.
  3. No app data module imports a feature.
  4. No feature or app data module is added to a new package.
  5. packages/contracts and packages/transit keep their existing responsibilities and real consumers.
  6. Route and loader behavior uses feature query options and does not duplicate fetch policy.
  7. The trip-planner entry owns endpoint state, provider resolution, worker lifecycle, supersession, results, activity recording, saved-state commands, and share-link privacy without returning display records.
  8. The assistant entry owns transcript, retries, session context, deterministic dispatch, and failures without exposing internal operations.
  9. Transit data has one bundle/index model and no renderer-only tile query in its UI imports.
  10. Route, stop, and jeepney search return raw domain values without highlight metadata or formatted labels.
  11. PWA, share, external-map, language, and clear-data operations return state or outcomes without showing UI.
  12. Deleted presentation helpers have no compatibility exports in retained logic.
  13. Existing retained behavior passes its focused tests, the web typecheck passes, transit tests pass, and the import audit confirms one-way dependencies.
  14. The implementation diff contains no replacement UI, design-system work, package creation, behavior redesign, or unrelated cleanup.

Decision trigger for wider reuse

This specification assumes apps/web is the only consumer of browser orchestration. The API, collector, dashboard, and scripts already consume the appropriate shared packages. If an approved second rider application or Native client needs the same planner, activity, assistant, query, or place-search behavior, pause before moving code. Document the real consumer, platform differences, storage and worker availability, network interface, and privacy contract first. Then decide whether packages/contracts or packages/transit is sufficient or whether a new shared package has proved necessary.

Final constraint

This is a target-state specification. It does not authorize a staged migration, dual interfaces, compatibility layers, or temporary copies. The implementation should move callers to the target owner and remove the obsolete logic in the same change.

Appendix: Prior architecture recommendation (verbatim)

Consumption rule

UI imports only each feature’s documented entry point: domain values, typed commands, stores/hooks, and query options. Feature logic imports only other logic, app data, packages/contracts, or packages/transit; never UI, CSS, icons, renderers, or routes. Internal files are not UI imports.

Target structure

text
1apps/web/src/
2├── app/
3│ ├── main.tsx
4│ ├── routeTree.gen.ts
5│ └── routes/ # route schemas, loaders, not-found decisions
6├── data/
7│ ├── api-client.ts
8│ ├── browser-db.ts
9│ ├── query-client.ts
10│ └── query-persister.ts
11├── features/
12│ ├── activity/
13│ │ ├── index.ts # export whitelist
14│ │ ├── activity.schema.ts
15│ │ ├── activity.store.ts
16│ │ └── use-activity.ts
17│ ├── assistant/
18│ │ ├── index.ts # export whitelist
19│ │ ├── use-assistant.ts
20│ │ ├── contracts.ts
21│ │ ├── prompts.ts
22│ │ ├── chat-api.ts
23│ │ ├── session.ts
24│ │ ├── dispatch.ts
25│ │ ├── domain.ts
26│ │ ├── faq.ts
27│ │ ├── live.ts
28│ │ ├── place-resolution.ts
29│ │ ├── route-operations.ts
30│ │ ├── trip-operations.ts
31│ │ ├── vehicles-api.ts
32│ │ ├── templates.ts
33│ │ └── template-copy.ts
34│ ├── transit-data/
35│ │ ├── index.ts # export whitelist
36│ │ ├── bundle-queries.ts
37│ │ ├── intelligence-queries.ts
38│ │ ├── bundle-hooks.ts
39│ │ └── transit-index.ts
40│ ├── bus-routes/
41│ │ ├── index.ts
42│ │ ├── search.ts
43│ │ └── route-stops.ts
44│ ├── bus-stops/
45│ │ ├── index.ts
46│ │ ├── search.ts
47│ │ └── stop-services.ts
48│ ├── jeep-routes/
49│ │ ├── index.ts
50│ │ ├── route-search.ts
51│ │ ├── place-search.ts
52│ │ ├── serving-routes.ts
53│ │ └── route-geometry.ts
54│ ├── bus-tracker/
55│ │ ├── index.ts
56│ │ └── vehicle-queries.ts
57│ ├── trip-planner/
58│ │ ├── index.ts # export whitelist
59│ │ ├── use-trip-planner.ts
60│ │ ├── trip-locations.ts
61│ │ ├── share-link.ts
62│ │ ├── place-search/
63│ │ │ ├── use-place-suggestions.ts
64│ │ │ ├── provider-router.ts
65│ │ │ ├── mapbox-search-box.ts
66│ │ │ └── geoapify-search.ts
67│ │ ├── trip-prepare-client.ts
68│ │ ├── planner-image-transport.ts
69│ │ └── trip-worker.ts
70│ ├── settings/
71│ │ └── clear-local-data.ts
72│ └── pwa/
73│ ├── index.ts
74│ ├── use-pwa-lifecycle.ts
75│ └── use-online-status.ts
76├── i18n/
77│ ├── index.ts
78│ ├── config.ts
79│ ├── i18next.d.ts
80│ └── locales/
81└── platform/
82 ├── external-maps.ts
83 └── share.ts
84
85packages/
86├── contracts/ # API/web wire schemas
87└── transit/ # shared transit domain and algorithms

Each index.ts above is only an export whitelist, not a facade or implementation layer.

App boundary and data

  • responsibility: Initialize routing/query persistence, validate route input, preload feature queries, and own browser/API storage implementations.
  • public API: None for screens. Loaders consume feature query options; app bootstrap consumes query-client, persister, PWA, i18n, and monitoring setup.
  • allowed dependencies: TanStack Router/Query, Zod, Dexie, the typed API client, feature public interfaces.
  • forbidden dependencies: Data modules importing features; feature logic importing routes; loaders containing layout or display decisions.
  • what belongs: Current route schemas, existence checks, bundle preloading, query client, error monitoring, the shared Dexie database.
  • what does not: Page layout, navigation chrome, error-state markup, or feature product behavior.

Transit data and transit features

  • responsibility: Load versioned transit/jeepney data, index it once, expose live intelligence queries, and implement route/stop/jeep search behavior.
  • public API: Query families, bundle hooks, the transit index, raw search results, route-stop lookup, stop-service lookup, and route distance.
  • allowed dependencies: packages/transit, TanStack Query, typed API client, Fuse internally.
  • forbidden dependencies: MapLibre, GeoJSON sources/layers created only to draw, CSS, highlighting markup, icons, routes.
  • what belongs: Offline fallback-version behavior, query timings, route/stop indexes, fuzzy matching, serving-route lookup, route-distance calculation.
  • what does not: mapQueries, tile-manifest types, Fuse match indices, parsed display labels, date/distance strings, route cards, stop rows, or map covers.

Activity

  • responsibility: Own recent/saved trip records and their persistent state.
  • public API: useActivity, ActivityTrip.
  • allowed dependencies: Zustand, activity schema, app browser database, transit identities supplied by trip-planner logic.
  • forbidden dependencies: Settings UI, trip result markup, query persister ownership, icons, routes.
  • what belongs: Load, add, save, unsave, toggle, duplicate prevention, recent-item cap.
  • what does not: Tabs, cards, formatted mode labels, or query-cache storage.
  • responsibility: Own endpoint state, place resolution, worker lifecycle, request supersession, planning, result selection, activity recording, saving, and share-link privacy.
  • public API: useTripPlanner, usePlaceSuggestions, planner/location/result types, buildTripShareUrl.
  • allowed dependencies: packages/transit, activity logic, app data, provider clients, Web Worker APIs.
  • forbidden dependencies: Map store, map layers, GeoJSON render output, animation, toasts, translations, components, routes.
  • what belongs: Current worker/client/transport, request validation, prewarm, latest-request-wins behavior, automatic recent recording, provider failover, coordinate-sharing warning flag.
  • what does not: tripDisplay, display schemas, distance strings, result messages, map camera fitting, trip-route GeoJSON, panels, fields, or cards.

The provider search stays here because its only real consumer is the trip planner. Moving it to shared or a package would be speculative.

Assistant

  • responsibility: Own the complete conversation lifecycle: remote intent inference, validation, local execution, session context, transcript, retries, templates, and follow-up behavior.
  • public API: useAssistant, response/transcript/action types, starter prompts, examples, and follow-up lookup.
  • allowed dependencies: packages/contracts, packages/transit, transit-data logic, the trip-planner planning operation, typed API client.
  • forbidden dependencies: Assistant components/widgets, icons, CSS, toast, routes, or formatted UI props.
  • what belongs: chat-api, dispatch/domain operations, session, FAQ/live/route/trip operations, structured response templates, transcript mutation.
  • what does not: Dock open state, draft text, Escape handling, composer, chips, transcript markup, widgets, or “thinking” animation.

Localization and settings

  • responsibility: Initialize i18n, validate/persist language choice, and clear all local app data as one operation.
  • public API: supportedLanguages, SupportedLanguage, setLanguage, clearLocalData.
  • allowed dependencies: i18next, app database/query client, Cache Storage, activity store reset.
  • forbidden dependencies: Settings dialogs, language-row copy, marker styles, icons, toasts.
  • what belongs: Catalogs, language detection, persistence, complete cache/database/store clearing.
  • what does not: Settings categories, about copy, confirmation timing, success/error messages, or deleted map preferences.

PWA and platform operations

  • responsibility: Expose online state, service-worker lifecycle, native sharing outcomes, and external-map URLs without deciding presentation.
  • public API: usePwaLifecycle, useOnlineStatus, shareContent, createMapLink, and their input/result types.
  • allowed dependencies: Browser APIs and the PWA registration hook.
  • forbidden dependencies: Toasts, translations, QR rendering, dialogs, icons, map implementations.
  • what belongs: Update polling, offline-ready/update-available state, apply-update command, Web Share/clipboard fallback, Google/Apple URL construction.
  • what does not: Notifications, reload buttons, provider rows, QR codes, geolocation map layers.

Existing packages

  • responsibility: packages/contracts owns API/web wire schemas; packages/transit owns transit data schemas, search/spatial algorithms, fares, trip planning, worker protocol, and shared domain types.
  • public API: Existing package exports for their current API, collector, scripts, and web consumers. Screens consume relevant types through feature entry points rather than the packages’ broad barrels.
  • allowed dependencies: Package-local modules and package dependencies.
  • forbidden dependencies: apps/web, React UI, routes, styles, icons.
  • what belongs: Behavior already shared by real consumers, including tripIdentity, ridesInTrip, transit schemas, planner contracts, and Davao place-domain search.
  • what does not: Web hooks, query options, browser storage, provider search, or presentation.
  • package decision: No new package. The repository shows no second app consuming web feature logic; the two existing packages already cover the real API/web and collector/web sharing.

Dependency graph

text
1App route boundary ───────────────┐
2Future UI ────────────────────────┼──→ feature public interfaces
3 │
4 ├──→ activity ───────────────→ browser DB
5 ├──→ assistant ──────────────→ contracts
6 │ │ → transit data
7 │ └─────────────────→ trip planner
8 ├──→ trip planner ───────────→ activity
9 │ │ → provider clients
10 │ └─────────────────→ packages/transit
11 ├──→ bus routes/stops ───────→ transit data
12 ├──→ jeep routes ────────────→ transit data
13 │ → packages/transit
14 ├──→ bus tracker ────────────→ typed API client
15 ├──→ settings/i18n ──────────→ app data
16 └──→ PWA/platform ───────────→ browser APIs
17
18transit data ─────────────────────→ typed API client
19 └────────────────────→ packages/transit
20
21packages/contracts ───────────────→ no app dependency
22packages/transit ─────────────────→ no app dependency

There is no reverse edge from data/domain/feature logic to UI or routes.

Public surface

Transit data

example.ts
1transitQueries.manifest()
2transitQueries.bundle(version: string)
3
4jeepneyQueries.manifest()
5jeepneyQueries.bundle(version: string)
6
7intelligenceQueries.routeProfile(routeId, direction)
8intelligenceQueries.routeService(routeId, direction)
9intelligenceQueries.routeSlowdowns(routeId, direction)
10intelligenceQueries.roadSlowdowns(routeId, direction)
11intelligenceQueries.stopArrivals(stopId, routeId, direction)
12intelligenceQueries.vehicleProfile(vehicleId)
13
14useTransitBundle()
15useJeepneyBundle()
16useTransitIndex(): TransitIndex
17
18type TransitIndex
19type RouteDirection

Omit currently unused readiness, road-readiness, vehicle-ETA, and stop-profile options from the screen surface.

Bus routes

example.ts
1searchBusRoutes(
2 bundle: TransitBundlePayload,
3 query: string
4): readonly TransitRoute[]
5
6getRouteStops(
7 index: TransitIndex,
8 routeId: string,
9 direction: RouteDirection
10): readonly TransitStop[]

Bus stops

example.ts
1searchBusStops(
2 bundle: TransitBundlePayload,
3 query: string
4): readonly TransitStop[]
5
6servicesForStop(
7 stop: TransitStop,
8 routesById: ReadonlyMap<string, TransitRoute>
9): readonly StopService[]
10
11hasDuplicateStopName(
12 stopName: string,
13 stopsByName: ReadonlyMap<string, readonly TransitStop[]>
14): boolean
15
16type StopService

Jeep routes

example.ts
1searchJeepRoutes(
2 routeNames: readonly string[],
3 query: string
4): readonly string[]
5
6useDavaoPlaceSearch(query: string): PlaceSearchResults
7
8useServingJeepneyRoutes(
9 place: SearchPlace | null
10): readonly ServingJeepneyRoute[]
11
12calculateLineStringDistanceMeters(
13 coordinates: readonly Coordinates[]
14): number
15
16type PlaceSearchResults

Bus tracker

example.ts
1vehicleQueries.all()

Vehicle intelligence remains under intelligenceQueries.vehicleProfile.

Activity

example.ts
1useActivity(): {
2 recents: readonly ActivityTrip[]
3 saved: readonly ActivityTrip[]
4 addRecent(trip: ActivityTrip): void
5 saveTrip(trip: ActivityTrip): void
6 unsaveTrip(id: string): void
7 isTripSaved(id: string): boolean
8 toggleSave(trip: ActivityTrip): void
9}
10
11type ActivityTrip

Do not expose the Dexie instance or raw Zustand store to screens.

Trip planner

example.ts
1useTripPlanner(initial?: TripPlannerInitialValues): {
2 locations: TripLocations
3 result: TripResult | null
4 selectedTripIndex: number
5 isPlanning: boolean
6 updateTextLocation(endpoint: TripEndpointId, value: string): void
7 selectPlace(endpoint: TripEndpointId, place: PlaceSuggestion): Promise<void>
8 setMapLocation(endpoint: TripEndpointId, coordinates: TripCoordinates): void
9 swapLocations(): void
10 plan(): Promise<void>
11 edit(): void
12 selectTrip(index: number): void
13 saveSelectedTrip(): void
14 unsaveSelectedTrip(): void
15 isSelectedTripSaved: boolean
16}
17
18usePlaceSuggestions(
19 query: string,
20 enabled: boolean,
21 scope?: string
22): {
23 outcome: SuggestOutcome
24 isSearching: boolean
25 canSearch: boolean
26}
27
28buildTripShareUrl(base: string, trip: ShareableTrip): TripShareLink
29
30type TripPlannerInitialValues
31type TripEndpointId
32type TripCoordinates
33type TripLocation
34type TripLocations
35type PlaceSuggestion
36type SuggestOutcome
37type TripResult
38type ShareableTrip
39type TripShareLink

The hook returns raw domain results and commands, not formatted rows or a view-model.

Assistant

example.ts
1useAssistant(): {
2 status: AssistantStatus
3 turns: readonly AssistantTurn[]
4 sendMessage(message: string): Promise<void>
5 runAction(actionId: AssistantActionId): Promise<void>
6 selectPlace(placeId: string): Promise<void>
7 setLocation(location: GeoPoint | undefined): Promise<void>
8 setLanguage(language: LanguageCode): void
9 reset(): void
10}
11
12assistantStarters: readonly AssistantPrompt[]
13assistantExamples: readonly AssistantPrompt[]
14
15followUpsFor(
16 resultType: AssistantResponse["resultType"]
17): readonly AssistantFollowUp[]
18
19type AssistantStatus
20type AssistantTurn
21type AssistantResponse
22type AssistantActionId
23type AssistantPrompt
24type AssistantFollowUp
25type GeoPoint
26type LanguageCode

sendMessage, runAction, place selection, and location retry update the transcript internally. Screens do not import dispatch, session, sendMessage, runAction, operation modules, or the transcript store.

Localization and local data

example.ts
1supportedLanguages: readonly SupportedLanguage[]
2setLanguage(language: SupportedLanguage): Promise<void>
3clearLocalData(): Promise<void>
4
5type SupportedLanguage

PWA and platform

example.ts
1usePwaLifecycle(): {
2 offlineReady: boolean
3 updateAvailable: boolean
4 acknowledgeOfflineReady(): void
5 applyUpdate(): Promise<void>
6}
7
8useOnlineStatus(): boolean
9
10shareContent(content: ShareData): Promise<ShareOutcome>
11
12createMapLink(
13 provider: MapProvider,
14 options: MapLinkOptions
15): string
16
17type ShareOutcome = "shared" | "copied" | "dismissed" | "unavailable"
18type MapProvider = "google" | "apple"
19type MapPlace
20type MapLinkOptions

Decisions

  • Keep — packages/contracts. It has real API/web consumers. Tradeoff: assistant wire types remain separately versioned from app response types.
  • Keep — packages/transit. API, web, collector, and scripts already share it. Tradeoff: its broad barrel remains suitable for infrastructure consumers, but web screens should use narrow feature seams.
  • Keep — routes and loaders at the app boundary. They own URL validation, preloading, and not-found decisions. Tradeoff: route files remain framework-specific and are not reusable logic.
  • Keep — the planner worker/client/transport as trip-planner internals. They provide isolation and latest-request-wins behavior. Tradeoff: the assistant may call an internal planning operation, but screens never see worker protocol.
  • Move — shared Dexie ownership from activity to data/browser-db.ts. The activity table and query persister already share the database. Tradeoff: storage record types become app-data concerns rather than activity internals.
  • Move — external provider place search under trip planner. Current usage is only the planner form. Tradeoff: move it again only if a second real feature or app begins using it.
  • Move — trip location state out of map/state and into trip planner. Origin/destination selection is planner behavior, not rendering state. Tradeoff: future maps call planner commands rather than owning those locations.
  • Move — external-map URL construction into platform/external-maps.ts. Stops, landmarks, and trip results all use it. Tradeoff: provider labels and QR presentation remain outside logic.
  • Merge — the two transit indexes into one transit-data index. map/pipeline/indexing.ts and use-transit-index.ts currently model the same bundle differently. Tradeoff: omit route-geometry indexing unless a non-render consumer proves it is needed.
  • Merge — assistant transcript and retry behavior into useAssistant. The dock currently performs product orchestration itself. Tradeoff: the hook becomes deeper, but the screen interface becomes much smaller.
  • Merge — assistant starters, examples, and follow-ups into assistant/prompts.ts. Their actions/messages are product behavior; their rendering is not. Tradeoff: translation labels remain referenced by keys.
  • Merge — local-data clearing into one operation. It should clear Dexie, persisted queries, Cache Storage, last-good versions, and in-memory activity. Tradeoff: the function is web-platform-specific and stays in apps/web.
  • Move — bus/stop/jeep search from screen files into feature functions. Search relevance is product behavior. Tradeoff: match highlighting is deliberately lost because it is presentation-only.
  • Delete-from-logic — map tile queries and BusRouteTileManifest. Their only web consumer is the deleted map renderer. Tradeoff: restore only with a real renderer consumer.
  • Delete-from-logic — tripDisplay, display schemas, tripRouteGeoJson, and TripMapFeature. They create UI-ready structures or renderer input. Tradeoff: future presentation reads raw Trip/TripResult data.
  • Delete-from-logic — parseRouteName, distance/date/movement formatters, provider attribution strings, and secondaryLine. They exist only to display values. Tradeoff: future UI decides its own localized wording.
  • Delete-from-logic — Fuse match indices from public results. useSearchFilter exposes a rendering-library shape. Tradeoff: future highlighting, if required, belongs in presentation.
  • Delete-from-logic — map store, map bridge, layers, projections, sources, camera effects, marker motion, and renderer adapters. No retained product operation requires them after presentation removal. Tradeoff: a future map starts from raw transit/trip domain values.
  • Delete-from-logic — packages/ui. It has no non-UI responsibility or non-web logic consumer. Tradeoff: none for the retained logic.
  • Keep — no new package. Existing cross-app use is already covered by contracts and transit. Tradeoff: web orchestration intentionally remains unavailable to Native until a real Native consumer establishes different platform seams.

No transitional aliases, compatibility barrels, or old-path re-exports should survive.

Current problems

  • Trip planning, cancellation, activity recording, selection, worker orchestration, and map effects are embedded in page.tsx. A future screen would otherwise have to reproduce product behavior.
  • Planner input state, provider retrieval, stale-pick suppression, and endpoint swapping live inside the presentational form.tsx.
  • Trip locations are owned by the map context even though planning is their real owner.
  • Assistant execution is in useAssistant, while transcript mutation, retry-after-place, retry-after-location, and reset semantics are in assistant-dock.tsx. The screen currently has to join two interfaces correctly.
  • Follow-up and starter product commands are embedded in TSX beside their rendering.
  • queries.ts combines immutable bundles, renderer-only tiles, vehicles, intelligence, storage fallback, and response validation into one broad import surface.
  • Transit indexing is duplicated between map and shared hooks, with different shapes and direction conventions.
  • The query persister imports the database from the activity feature, creating a lower-level shared runtime dependency on a feature: query-persister.ts.
  • The activity-owned database also contains the generic query-cache table, so its current folder is not its real owner.
  • PWA lifecycle logic directly invokes translations and toasts instead of returning state and commands: use-pwa-lifecycle.ts.
  • External-map URL generation is private inside a dialog that also owns QR rendering, translations, styles, and icons: map-provider-dialog.tsx.
  • Place provider types and behavior share a module with attribution and secondary-line display helpers.
  • Local-data clearing is split across a shared function, query-client calls, and direct Zustand mutation in the settings presentation.