From 47eecdc0585f06071f1589a1aa93d092f0637d27 Mon Sep 17 00:00:00 2001 From: Julien Calixte Date: Thu, 14 May 2026 20:52:41 +0200 Subject: [PATCH] docs(qfd): crosslink ADR mentions with reference-style links Convert every `ADR-NNN` mention in qfd.md to a `[ADR-NNN]` shortcut reference and add the link definitions at the bottom of the file, so each ADR mention navigates to the matching adr.md section. --- docs/qfd.md | 154 ++++++++++++++++++++++++++++------------------------ 1 file changed, 82 insertions(+), 72 deletions(-) diff --git a/docs/qfd.md b/docs/qfd.md index e84835d..475fbbe 100644 --- a/docs/qfd.md +++ b/docs/qfd.md @@ -26,7 +26,7 @@ What a user (= me) values about the device, with importance weights on a | ID | Requirement | Weight | Source | | --- | ------------------------------------------------------- | :----: | ------------------------------------------------------------------------------------------------------------------ | | W1 | Sub-second visible response to typing | 10 | [product → Write](v0.1-mvp-product.md#user-stories), [README → UX](../README.md#ux-boundaries-set-by-the-medium) | -| W2 | `Ctrl-G` reliably **Publishes** to the remote | 9 | [product → Publish](v0.1-mvp-product.md#user-stories), [ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt), [CONTEXT → Publish](../CONTEXT.md#user-facing-actions) | +| W2 | `Ctrl-G` reliably **Publishes** to the remote | 9 | [product → Publish](v0.1-mvp-product.md#user-stories), [ADR-010], [CONTEXT → Publish](../CONTEXT.md#user-facing-actions) | | W3 | Pulling power never corrupts the file | 10 | [product → Recover](v0.1-mvp-product.md#user-stories), [acceptance](v0.1-mvp-product.md#acceptance-criteria) | | W4 | One-shot provisioning, never repeated mid-session | 7 | [product → Provisioning](v0.1-mvp-product.md#provisioning-build-time-dev-only), [roadmap → v0.9](roadmap.md#v09--robustness--) | | W5 | Quick boot to a writing cursor | 6 | [product → acceptance](v0.1-mvp-product.md#acceptance-criteria) (≤ 5 s) | @@ -90,21 +90,21 @@ weighted vote on which functions deserve the most engineering attention. ### Top engineering priorities (from importance) 1. **H2 — partial-refresh region area** (174). Bound how many pixels the - panel has to flip per keypress; ADR-003 is the hardware-side answer. + panel has to flip per keypress; [ADR-003] is the hardware-side answer. 2. **H9 — PSRAM heap during push** (162). gitoxide pack + rope + TLS all - share the same arena; ADR-001 and ADR-004 trade binary size for ecosystem + share the same arena; [ADR-001] and [ADR-004] trade binary size for ecosystem so this becomes the watched metric. 3. **H8 — save durability** (147). Atomic-rename + fsync; FAT's weakness is - acknowledged in ADR-007 and mitigated, not designed around. + acknowledged in [ADR-007] and mitigated, not designed around. 4. **H1 — keypress latency** (138). The single most user-visible number; - ADR-002 and ADR-003 are co-conspirators. -5. **H6 — push success rate** (132). ADR-004 (gitoxide) and ADR-005 (PAT + [ADR-002] and [ADR-003] are co-conspirators. +5. **H6 — push success rate** (132). [ADR-004] (gitoxide) and [ADR-005] (PAT over HTTPS) own this jointly; spike 7 is the kill-switch. 6. **H3 — full-refresh cadence** (126). The ghosting/flash tradeoff; lives in the render layer. The bottom three (H7 push time, H15 build time, H10 binary size) are real -costs but ones we knowingly took on (ADR-001) and are not in the critical +costs but ones we knowingly took on ([ADR-001]) and are not in the critical path of user experience. --- @@ -138,13 +138,13 @@ The roof shows where pushing one function pushes another the wrong way. - **H1 latency ↔ H3 refresh cadence** (mild). More partial refreshes per second pile up ghosting faster, demanding earlier full refreshes — visible flashes that hurt H8 perception and H1 burst behaviour. The - ADR-003 strip aspect is the structural answer: a small framebuffer makes + [ADR-003] strip aspect is the structural answer: a small framebuffer makes _both_ cheaper, not one at the expense of the other. The runtime answer is render §H3: schedule full refreshes on idle ≥ 1 s (v0.1 tech doc). - **H9 heap ↔ H10 binary size** (strong). std + gitoxide + mbedtls inflate - both. We chose to spend on these (ADR-001, ADR-004) because 16 MB flash + both. We chose to spend on these ([ADR-001], [ADR-004]) because 16 MB flash and 8 MB PSRAM make them affordable; the kill-switch is spike 7. If - heap during push refuses to come under 1 MB free, ADR-004 flips to + heap during push refuses to come under 1 MB free, [ADR-004] flips to libgit2-sys for v0.1. - **H9 heap ↔ H5 soak** (strong). A long writing session grows the rope and the glyph cache; pushing on top can OOM. Mitigation: 256 KB file cap @@ -153,7 +153,7 @@ The roof shows where pushing one function pushes another the wrong way. - **H6 push success ↔ H12 Wi-Fi reconnect** (reinforcing). Both come from the same network stack; investing in reconnect backoff helps both. - **H10 binary ↔ H15 build time** (strong). std builds are slow. Accepted - in ADR-001 — refactor leverage (H14) is the long-term payoff, not the + in [ADR-001] — refactor leverage (H14) is the long-term payoff, not the per-build seconds. - **H4 boot ↔ H10 binary** (mild). Larger binary = slower flash load. Affordable at our size class but worth watching as features land. @@ -172,24 +172,24 @@ constrains the choice. Components (with anchoring ADR): -| ID | Component | ADR | -| --- | ------------------------------------ | ------------------- | -| C1 | ESP32-S3-N16R8 SoC | ADR-001, ADR-008 | -| C2 | `esp-idf-rs` (std) + ESP-IDF | ADR-001 | -| C3 | `std::thread` + `crossbeam-channel` | ADR-006 | -| C4 | PSRAM allocator wrapper | ADR-001 | -| C5 | GDEY0579T93 + DESPI-c579 panel | ADR-003 | -| C6 | `embedded-graphics` + e-paper driver | ADR-002, ADR-003 | -| C7 | Custom widget / dirty-rect layer | ADR-002 | -| C8 | `ropey` rope buffer | ADR-001 (ecosystem) | -| C9 | TinyUSB host (`esp-idf` bindings) | ADR-009 | -| C10 | FAT on microSD | ADR-007 | -| C11 | LittleFS on internal flash | ADR-007 | -| C12 | `gitoxide` (`gix-*`) | ADR-004 | -| C13 | mbedtls TLS (via ESP-IDF) | ADR-005 | -| C14 | HTTPS + GitHub PAT auth | ADR-005 | -| C15 | eFuse-derived encryption key | ADR-005, ADR-007 | -| C16 | USB-C wall PSU | ADR-008 | +| ID | Component | ADR | +| --- | ------------------------------------ | ---------------------- | +| C1 | ESP32-S3-N16R8 SoC | [ADR-001], [ADR-008] | +| C2 | `esp-idf-rs` (std) + ESP-IDF | [ADR-001] | +| C3 | `std::thread` + `crossbeam-channel` | [ADR-006] | +| C4 | PSRAM allocator wrapper | [ADR-001] | +| C5 | GDEY0579T93 + DESPI-c579 panel | [ADR-003] | +| C6 | `embedded-graphics` + e-paper driver | [ADR-002], [ADR-003] | +| C7 | Custom widget / dirty-rect layer | [ADR-002] | +| C8 | `ropey` rope buffer | [ADR-001] (ecosystem) | +| C9 | TinyUSB host (`esp-idf` bindings) | [ADR-009] | +| C10 | FAT on microSD | [ADR-007] | +| C11 | LittleFS on internal flash | [ADR-007] | +| C12 | `gitoxide` (`gix-*`) | [ADR-004] | +| C13 | mbedtls TLS (via ESP-IDF) | [ADR-005] | +| C14 | HTTPS + GitHub PAT auth | [ADR-005] | +| C15 | eFuse-derived encryption key | [ADR-005], [ADR-007] | +| C16 | USB-C wall PSU | [ADR-008] | Function-to-component matrix (9 strong / 3 medium / 1 weak): @@ -214,25 +214,24 @@ Function-to-component matrix (9 strong / 3 medium / 1 weak): ### Read across, not down - **C5/C6/C7** (panel + graphics + widget) are the single most leveraged - cluster — they own H1, H2, H3 (the top of the priority list). ADR-002 - and ADR-003 are the ADRs to keep most honest as v0.x progresses. + cluster — they own H1, H2, H3 (the top of the priority list). [ADR-002] + and [ADR-003] are the ADRs to keep most honest as v0.x progresses. - **C12** (`gitoxide`) is overloaded: H6, H7, H9, H10, H11, H14, H15 all - touch it. That's why ADR-004 includes a kill-switch (fall back to + touch it. That's why [ADR-004] includes a kill-switch (fall back to `libgit2-sys` if spike 7 fails). It's also why H9 sits in the top three priorities — `gitoxide`'s memory profile is the unknown. - [ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt) - pins the *shape* of the publish sequence (the `gct` flow); C12 is just the - library that implements it. Changing ADR-010 doesn't change C12's column, - but changing C12 (the kill-switch) does not change ADR-010's user - contract. + [ADR-010] pins the *shape* of the publish sequence (the `gct` flow); C12 + is just the library that implements it. Changing [ADR-010] doesn't change + C12's column, but changing C12 (the kill-switch) does not change + [ADR-010]'s user contract. - **C11** (LittleFS) is unused in v0.1 — config is build-time. Its non-zero - cells in the matrix describe the v0.9+ shape per - [ADR-007](adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config), - not v0.1 reality. + cells in the matrix describe the v0.9+ shape per [ADR-007], not v0.1 + reality. - **C2** (std runtime) sits underneath almost everything, but it's the _enabler_ (H4 boot, H10 binary, H12 Wi-Fi) rather than the bottleneck. - Reversing ADR-001 would force re-deciding ADR-004, ADR-005, ADR-006, - ADR-007 all at once — they're a single decision in three drawers. + Reversing [ADR-001] would force re-deciding [ADR-004], [ADR-005], + [ADR-006], [ADR-007] all at once — they're a single decision in three + drawers. --- @@ -243,9 +242,9 @@ the numbers spikes 2–7 must validate before integration starts. | Rank | Function | Target | Watched on | If we miss it | | ---- | -------------- | ------------------------------------- | ----------------- | ----------------------------------------------------------------------- | -| 1 | H2 region area | ≤ 1 line per keypress | spike 2 + spike 5 | Increase font size to shrink per-glyph dirty rect (ADR-003 consequence) | -| 2 | H9 PSRAM heap | ≥ 1 MB free at push peak | spike 7 | ADR-004 kill-switch → `libgit2-sys`; cap rope at 128 KB | -| 3 | H8 durability | 100 % survive power yank after status | bench HIL | Re-evaluate ADR-007 (move config to internal NVS only) | +| 1 | H2 region area | ≤ 1 line per keypress | spike 2 + spike 5 | Increase font size to shrink per-glyph dirty rect ([ADR-003] consequence) | +| 2 | H9 PSRAM heap | ≥ 1 MB free at push peak | spike 7 | [ADR-004] kill-switch → `libgit2-sys`; cap rope at 128 KB | +| 3 | H8 durability | 100 % survive power yank after status | bench HIL | Re-evaluate [ADR-007] (move config to internal NVS only) | | 4 | H1 latency | ≤ 200 ms keypress→glyph | spike 5 | Larger partial-refresh region; render multi-char bursts | | 5 | H6 push % | ≥ 95 % on healthy Wi-Fi | spike 6 + spike 7 | TLS cipher trim; reconnect backoff tuning | | 6 | H3 cadence | full every ~20 partials | spike 2 | Adjust per panel temperature; defer flash to idle ≥ 1 s | @@ -255,7 +254,7 @@ the numbers spikes 2–7 must validate before integration starts. The two not-in-MVP rows but already-shaped-by-design: | — | H13 current | Measured only in v0.1 | bench multimeter | Cell sizing for v0.8 is data-driven, not spec-sheet | -| — | H11 stacks | Sum ≤ 80 KB | static analysis | Was off-by-2x in ADR-006 pre-fix — corrected in §7 | +| — | H11 stacks | Sum ≤ 80 KB | static analysis | Was off-by-2x in [ADR-006] pre-fix — corrected in §7 | --- @@ -263,30 +262,30 @@ The two not-in-MVP rows but already-shaped-by-design: Plain-language summary of what we accepted in exchange for what. -| Tradeoff | Got | Paid | ADR | -| ------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | ------- | -| std (esp-idf-rs) over no_std (esp-hal) | Heap, threads, VFS, mbedtls, gitoxide-compatible | +1 MB binary, +5–10 min builds | ADR-001 | -| Custom widget layer over Ratatui | Dirty-rects aligned to e-ink regions; 200 KB binary back | 500 LoC we own and maintain | ADR-002 | -| 5.79" strip panel over 7.5" page or 10.3" reader | 27 KB framebuffer, fast partial refresh, "Freewrite" UX | Only ~11 visible lines | ADR-003 | -| `gitoxide` over `libgit2-sys` | Pure Rust, modular, no FFI cross-compile pain | Smart-HTTP path is newer; PSRAM profile unproven (spike 7) | ADR-004 | -| HTTPS + PAT over OAuth device-flow or SSH | Simplest auth that `gitoxide` smart-HTTP already supports | Long-lived secret on device; in v0.1 the PAT is compiled into the binary (dev-only target user makes this acceptable); v0.9 moves it to encrypted NVS | ADR-005 | -| `std::thread` over `embassy` or `tokio` | Boring, debuggable, real stack traces; no exec to tune | ~76 KB total stack across 5 tasks | ADR-006 | -| FAT-on-SD + LittleFS-on-flash split | Desktop can read SD; config survives SD reformat | Two filesystems to manage; FAT's power-loss weakness mitigated by atomic-rename | ADR-007 | -| Wall power for v0.1, battery deferred | Measure real draw before sizing the cell | Tethered MVP; not the final aesthetic | ADR-008 | -| USB host (TinyUSB) over BLE-HID | No radio contention with Wi-Fi during push; keyboard powered from the device | One more USB connector on enclosure | ADR-009 | -| Atomic `Ctrl-G` + auto-timestamp commit message | One key, one outcome; matches the user's existing `gct` workflow; no modal prompt to slow H1 latency | Commit history is timestamp noise; the device may author merge commits the user never sees; reversal would break muscle memory | ADR-010 | +| Tradeoff | Got | Paid | ADR | +| ------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------- | +| std (esp-idf-rs) over no_std (esp-hal) | Heap, threads, VFS, mbedtls, gitoxide-compatible | +1 MB binary, +5–10 min builds | [ADR-001] | +| Custom widget layer over Ratatui | Dirty-rects aligned to e-ink regions; 200 KB binary back | 500 LoC we own and maintain | [ADR-002] | +| 5.79" strip panel over 7.5" page or 10.3" reader | 27 KB framebuffer, fast partial refresh, "Freewrite" UX | Only ~11 visible lines | [ADR-003] | +| `gitoxide` over `libgit2-sys` | Pure Rust, modular, no FFI cross-compile pain | Smart-HTTP path is newer; PSRAM profile unproven (spike 7) | [ADR-004] | +| HTTPS + PAT over OAuth device-flow or SSH | Simplest auth that `gitoxide` smart-HTTP already supports | Long-lived secret on device; in v0.1 the PAT is compiled into the binary (dev-only target user makes this acceptable); v0.9 moves it to encrypted NVS | [ADR-005] | +| `std::thread` over `embassy` or `tokio` | Boring, debuggable, real stack traces; no exec to tune | ~76 KB total stack across 5 tasks | [ADR-006] | +| FAT-on-SD + LittleFS-on-flash split | Desktop can read SD; config survives SD reformat | Two filesystems to manage; FAT's power-loss weakness mitigated by atomic-rename | [ADR-007] | +| Wall power for v0.1, battery deferred | Measure real draw before sizing the cell | Tethered MVP; not the final aesthetic | [ADR-008] | +| USB host (TinyUSB) over BLE-HID | No radio contention with Wi-Fi during push; keyboard powered from the device | One more USB connector on enclosure | [ADR-009] | +| Atomic `Ctrl-G` + auto-timestamp commit message | One key, one outcome; matches the user's existing `gct` workflow; no modal prompt to slow H1 latency | Commit history is timestamp noise; the device may author merge commits the user never sees; reversal would break muscle memory | [ADR-010] | ### Conflicts left explicitly _unresolved_ by v0.1 These are the live tensions we are watching, not deciding harder: -- **ADR-004 vs H9.** If `gitoxide` cannot keep ≥ 1 MB PSRAM free at push +- **[ADR-004] vs H9.** If `gitoxide` cannot keep ≥ 1 MB PSRAM free at push peak, we are committed to switching transports for v0.1, not absorbing the OOM risk. -- **ADR-009 vs H6/H13.** If TinyUSB host turns out unstable (spike 4), +- **[ADR-009] vs H6/H13.** If TinyUSB host turns out unstable (spike 4), BLE-HID is the documented fallback — at the cost of Wi-Fi radio contention during push (re-checking H6). -- **ADR-007 vs H8.** Power loss between FAT rename and dir flush yields +- **[ADR-007] vs H8.** Power loss between FAT rename and dir flush yields the previous saved version. We document this as expected behavior; it becomes a real bug only if soak testing shows it triggering on routine saves. @@ -295,36 +294,36 @@ These are the live tensions we are watching, not deciding harder: ## 8. Inconsistencies spotted and fixed -- **ADR-006 stack figure.** ADR-006 previously said "~40 KB of stack space - for task stacks" — but the v0.1 technical design's task table +- **[ADR-006] stack figure.** [ADR-006] previously said "~40 KB of stack + space for task stacks" — but the v0.1 technical design's task table (`usb 8 + wifi 8 + ui 16 + render 12 + git 32`) sums to **76 KB**. - Updated ADR-006's Consequences section to reflect the actual budget and - cross-reference the tech doc. The 76 KB figure still fits comfortably - in the ESP32-S3's 512 KB internal SRAM, so no design change — just - documentation accuracy. + Updated [ADR-006]'s Consequences section to reflect the actual budget + and cross-reference the tech doc. The 76 KB figure still fits + comfortably in the ESP32-S3's 512 KB internal SRAM, so no design + change — just documentation accuracy. - **Commit-message format triple-mismatch.** README said `git commit -m "wip"`, the v0.1 product doc said `"wip "`, and the user's actual shell alias (`gct` / `git-commit-timestamp`) uses a pure ISO-8601 timestamp with no `wip` prefix. Resolved by aligning all docs on `gct` and recording the decision as - [ADR-010](adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt). + [ADR-010]. Pulled the v0.7 roadmap item "Commit message prompt instead of hard-coded - `wip`" — it's now contradicted by ADR-010 and removed. + `wip`" — it's now contradicted by [ADR-010] and removed. - **First-run flow vs. target user.** The v0.1 product doc described a captive-portal first-run, but the same doc names the v0.1 target user as the dev themselves ("Me. Solo."). Provisioning a solo-dev device through a captive portal is ceremony without a user. Resolved by switching v0.1 to build-time env-var config (no NVS, no LittleFS, no AP mode); on-device provisioning is the v0.9 release that introduces non-dev users. Touches - ADR-005, ADR-007, the v0.1 product + technical docs, and the v0.9 + [ADR-005], [ADR-007], the v0.1 product + technical docs, and the v0.9 roadmap entry. - **Vocabulary leak.** Earlier docs used "commit" and "push" as if they - were distinct user actions; the gct/ADR-010 model collapses them into a + were distinct user actions; the gct/[ADR-010] model collapses them into a single user-facing **Publish**. Resolved by introducing [`CONTEXT.md`](../CONTEXT.md) as the canonical glossary; user-facing text now uses **Save** and **Publish** only. -The minor variance between README's "~12 lines" and product/ADR-003's +The minor variance between README's "~12 lines" and product/[ADR-003]'s "~11 lines" of edit area is within rounding for a 14 px glyph in a 240 px tall edit region and is not load-bearing. @@ -339,3 +338,14 @@ tall edit region and is not load-bearing. reality drifts from estimates. - The WHATs change rarely; the HOWs change with each release; the matrices are recomputed when either side changes. + +[ADR-001]: adr.md#adr-001-language-and-runtime--rust-on-esp-idf-rs-std +[ADR-002]: adr.md#adr-002-ui-strategy--custom-widgets-on-embedded-graphics-not-ratatui +[ADR-003]: adr.md#adr-003-display--gdey0579t93--despi-c579-breakout +[ADR-004]: adr.md#adr-004-git-implementation--gitoxide-gix +[ADR-005]: adr.md#adr-005-auth--https--github-personal-access-token +[ADR-006]: adr.md#adr-006-concurrency--stdthread--channels-no-async-runtime +[ADR-007]: adr.md#adr-007-storage-split--fat-on-sd-for-working-copy-littlefs-on-flash-for-config +[ADR-008]: adr.md#adr-008-mvp-power--wall-powered-battery-deferred-to-v08 +[ADR-009]: adr.md#adr-009-keyboard-transport--usb-host-tinyusb +[ADR-010]: adr.md#adr-010-publish-ux--atomic-ctrl-g-auto-timestamp-commit-message-no-user-prompt