Rewrite reference docs as current state; record the style rule

Reference, guidelines and plan documents describe what is and what will be.
Retirement stories, resolved-risk narration and against-the-alternative
argument moved out or were cut; reports keep that material.

What survives the cut is the mechanism that can bite again (driving side is a
country-level property) and the condition that would change a decision (a
permanently installed, car-powered panel), both stated generally.

Also removes a duplicated Japan section from a bad edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013kj58Jugkic783b8ZrtTVZ
This commit is contained in:
2026-09-04 15:23:08 +09:00
parent 102cf38082
commit 574df1471c
4 changed files with 75 additions and 62 deletions
+32 -33
View File
@@ -91,8 +91,8 @@ rather than duplicated per client.
### Swappable providers
The same shape has now been chosen independently three times, so it is the
project's idiom rather than three coincidences:
A component with an expensive or not-yet-existing dependency puts it behind an
interface, with a trivial implementation alongside the real one:
| Component | Interface | Real | Trivial |
|---|---|---|---|
@@ -194,27 +194,22 @@ owns the boundary. For transports without IP — BT SPP, plain USB serial —
Because every display is a bus subscriber, adding, removing or swapping one
touches no acquisition code.
### Video out was dropped
### Directly-driven screens
An earlier revision had a second class: a screen driven directly by the Orin over
SPI/DP/HDMI, rendered by a local process. It is gone, and with it `app-hud`.
A screen driven over DP or HDMI is viable when it is **permanently installed and
powered by the car**. A dash-integrated panel qualifies: plug in DP and it is a
screen, and it adds a renderer without touching the contract, since the bus does
not care what draws it.
The reasoning, in order:
It is not viable for a removable display unit, which would need a second cable:
- **No connector-powered display exists.** DisplayPort's `DP_PWR` supplies 3.3 V
at 500 mA — and VESA-compliant cables deliberately leave it unconnected at the
sink. HDMI's +5 V pin gives 55 mA. A panel needs 5–15 W. So a directly-driven
screen always needs its own power feed, which is the cable the removable display
unit was meant to avoid.
- **DSI is not available.** The Orin Nano module carries DSI on SODIMM pins, but
the developer kit's carrier board does not break them out; DisplayPort is its
only display output. DSI would require a custom carrier board.
- **It bought nothing the browser-shaped path does not.** A data-only client on
one USB cable is simpler, and the display unit stays swappable.
| Supply | Available | A panel needs |
|---|---|---|
| DisplayPort `DP_PWR` | 3.3 V / 500 mA, and VESA cables leave it unconnected at the sink | 5–15 W |
| HDMI +5 V | 55 mA | |
Video out is what generated the enclosure, power and cabling complexity this
project started out trying to solve. Removing it removed the problem rather than
solving it.
DSI is unavailable on this carrier board — the module carries it on SODIMM pins,
but the developer kit does not break them out, so DSI needs a custom carrier.
## Navigation
@@ -252,8 +247,7 @@ Guidance is state, so it belongs on the bus: `yukart/nav/guidance` carries the
next manoeuvre, retained, and a display connecting mid-drive renders immediately.
Route *requests* are request/response and fit MQTT badly. Displays call the
Valhalla HTTP API through `telemetry-gw` instead of inventing a request/reply
topic pair. Recorded as decision 9.
Valhalla HTTP API through `svc-gw`. Recorded as decision 9.
### Map data is not source
@@ -287,18 +281,23 @@ checksum at both ends.
| **CJK labels** | MapLibre cannot ship Japanese glyph sets as SDF ranges — far too large. Set `localIdeographFontFamily` so the browser renders CJK from on-device fonts. Protomaps tiles also carry `name:en`, so romanised labels are an option. |
| **Addressing** | Japanese addresses are block-based, not street-based. Valhalla does not geocode at all, so destination search is a separate problem — do not assume a western geocoder transfers. |
### arm64 — resolved
### Verified on the Jetson
No longer a risk. Verified on `mikkeli-orin-nano-2` (L4T R36.4.7, JetPack 6) on
2026-09-04: `ghcr.io/valhalla/valhalla` publishes a `linux/arm64` manifest, the
image runs natively, and the full pipeline works end to end — config, timezones,
admins, tiles, and a served route.
`ghcr.io/valhalla/valhalla` publishes `linux/arm64` and runs natively on
L4T R36.4.7. Measured:
Measured: Shikoku, 49 MB PBF → 149 MB of routing tiles; Takamatsu → Matsuyama
routed in 157 km / 112 min with Japanese street names intact in the narrative.
| | |
|---|---|
| Shikoku PBF → routing tiles | 49 MB → 149 MB |
| Whole-Japan PBF | 2.4 GB |
| Admin DB, whole Japan | 17 MB |
| Timezone DB | 116 MB |
| Takamatsu → Matsuyama | 157 km, 112 min |
The server logs a benign `tiles.tar No such file or directory` warning and falls
back to the tile directory, which is the form decision 10 selected anyway.
Japanese street names survive intact in the route narrative.
The server logs `tiles.tar No such file or directory` at startup and falls back to
the tile directory, which is the form decision 10 selects. Not an error.
## Decisions
@@ -318,12 +317,12 @@ rewriting the document.
| 9 | Nav guidance published on the bus; route requests over HTTP via `telemetry-gw` | **Proposed** | 2026-09-03 |
| 10 | Map artifacts delivered by `rsync` over LAN only; Valhalla tiles kept as a directory, not a tar | Accepted | 2026-09-03 |
| 11 | Boot ordering is capture-first: acquisition reaches ready before serving or voice | Accepted | 2026-09-03 |
| 12 | The **tablet** owns the car's aux input; TTS crosses as chunked utterances, streaming-capable | Accepted | 2026-09-04 |
| 12 | The tablet owns the car's aux input; TTS crosses as chunked utterances, streaming-capable | Accepted | 2026-09-04 |
| 13 | Reasoning stays in the cloud — a local LLM does not fit beside the voice stack | Accepted | 2026-09-03 |
| 14 | Recordings are bounded with rotation | Accepted | 2026-09-04 |
| 15 | Boundary is two simplex channels — lossy stream down, acknowledged request/response up | Accepted | 2026-09-03 |
| 16 | Cloud links are outbound-initiated only; nothing calls in | Accepted | 2026-09-03 |
| 17 | `svc-gw` combines local hub and app gateway; no client may block the hub | Accepted | 2026-09-03 |
| 18 | Stubs publish the real topic and schema, never a fixture-specific one | Accepted | 2026-09-03 |
| 19 | Services mock themselves behind their own input interface; no separate stub implementations exist | Accepted | 2026-09-04 |
| 20 | Video out (Class A) dropped; every display is data-only. `app-hud` cancelled | Accepted | 2026-09-04 |
| 19 | A service mocks itself behind its own input interface; there is one implementation of each contract | Accepted | 2026-09-04 |
| 20 | Every display is data-only. Directly-driven screens are for permanently installed, car-powered panels only | Accepted | 2026-09-04 |
+11 -14
View File
@@ -44,9 +44,9 @@ have taken their share.
Four rules follow.
**The cloud LLM is load-bearing.** [OVERVIEW.md](../OVERVIEW.md) puts reasoning in
the cloud. That decision is what makes a local voice stack fit at all — moving the
LLM on-device collapses this budget entirely. It is not a free choice to revisit.
**Reasoning runs in the cloud.** The local voice stack fits only because the LLM
is not resident. An on-device LLM and the voice stack do not coexist in this
budget.
**Size Whisper by vocabulary, not by benchmark.** In-car commands are a narrow,
predictable set. `large` buys accuracy this application cannot spend, at a cost
@@ -156,10 +156,10 @@ Same protocol, same receiver. The receiver starts playback on the first chunk,
appends as more arrive, and finishes on the terminator — which is what Android's
`AudioTrack` streaming mode does natively.
**What must not be built:** an utterance as a single response body with a
"download the file, then play the file" player. That works today and forecloses
streaming permanently. Whether the TTS we land on streams is still open, so the
transport must not assume either way.
⚠ **Never ship an utterance as a single response body with a "download, then
play" player.** It works, and it forecloses streaming permanently — the receiver
has no way to begin before the last byte. The chunk boundary is what keeps both
TTS modes available.
### The transport is testable with no real TTS
@@ -171,10 +171,8 @@ completely. Voice quality is irrelevant to every one of those.
### Ducking is the tablet's job
Android's `AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK` ducks other applications' audio —
Spotify included — for the duration of a request, then restores it. This is the
mechanism every navigation app already uses, and it is less work than the PipeWire
policy it replaces. Cross-application ducking is a solved problem on the tablet
and would have been a bespoke one on the Orin.
Spotify included — for the duration of a request, then restores it. Cross-
application ducking is native on the tablet.
### The microphone stays on the Orin
@@ -188,6 +186,5 @@ With no tablet connected the Orin can still hear — the mic is local — but ca
speak. The voice stack expires or drops pending utterances rather than queueing
them unboundedly or blocking on a sink that may never arrive.
The consequence to accept: **unplug the tablet and the car has no voice.** That is
tolerable because unplugging it also removes the music, so the tablet was already
load-bearing for audio. Speech does not make it more so.
**The tablet is required for any audio at all** — it is both the media source and
the speech sink.
+32 -12
View File
@@ -115,10 +115,8 @@ it records the investigation and the dead ends, not just the final commands.
### Scaffolding conventions
No template repository. Template instantiation is fork-at-create, so it could not
reach repos that already exist, and the scaffolding is a handful of markdown files
— lighter than the machinery to manage it. A template earns its place where there
is a real toolchain to carry, which is the ESP32 case above.
Component repos are scaffolded by hand. A template repo is for carrying a real
toolchain, which is the ESP32 case above.
Every component repo carries:
@@ -131,6 +129,33 @@ docs/impl/reports/
docs/BOM.md only where the repo owns hardware
```
### Writing style
Documents describe **what is and what will be**. They are not changelogs — git
holds the sequence.
| Instead of | Write |
|---|---|
| "X was dropped because Y" | what the design **is** |
| "this was a risk, now resolved" | what runs, and the measurements |
| "the earlier decision assumed Z" | the current constraint |
| "X was rejected in favour of Y" | Y, stated as the rule |
⚠ **A sentence justifying a current state by a current reason becomes false when
the reason changes, and nothing announces it** — it stays, confident and
well-written, and reads as fact.
Two things survive the cut, because they stay true when their subject is gone:
- **A mechanism that can bite again.** Not "we picked the wrong extract once", but
"driving side is a country-level property, so a clipped extract routes mirrored".
- **A condition that would change the decision**, stated generally — "a
permanently installed, car-powered panel" rather than a story about a panel that
was considered.
Reports under `impl/reports/` are the exception and the place for all of it: the
investigation, the dead ends, what was measured and found wrong.
### Front matter
Every markdown document carries front matter, with **no exceptions for the hub or
@@ -145,14 +170,9 @@ for any particular repo**. The `type` discriminates:
`status` is one of `active`, `superseded` or `done`.
The extra fields exist for one reason. This document already observes that **plans
go stale and reports do not** — but nothing made staleness visible, so a plan
written months ago looked identical to one written this morning. `status` and
`updated` fix that; `date` on a report records the moment it describes, which is
the whole value of a report.
A `type` with one possible value would be decoration. This one has four and they
are worth grepping for.
`status` and `updated` make a plan's staleness visible — plans go stale and
reports do not, and without them a plan from months ago reads identically to one
written this morning. `date` on a report records the moment it describes.
## Naming
-3
View File
@@ -59,9 +59,6 @@ Order chosen by feedback speed, not ambition.
quickest to iterate on, and is the only display in current scope
- [ ] `app-esp32` — RLCD/e-ink glanceable subset. Deferred, no hardware in scope
Video out was dropped along with `app-hud`; see the display contract in
[ARCHITECTURE.md](ARCHITECTURE.md#display-contract).
## Phase 5 — Camera and inference · *separate task*
Deliberately deferred. It is the reason the compute is a Jetson rather than a