docs: refresh v0.1 spec for gct publish flow and dev-only build-time config

Two coupled changes that emerged from one /deep-design session, both touching
the same v0.1 paragraphs:

(1) Align Ctrl-G with the user's existing gct shell alias: git add . ->
    short-circuit if nothing staged -> commit with an ISO-8601 timestamp (no
    wip prefix) -> push -> on push failure, git pull --no-edit then retry.
    Atomic from the user's view. Recorded as ADR-010. The previously-planned
    v0.7 commit-message-prompt item is removed; it contradicts the
    gct/timestamp model.

(2) Replace the v0.1 captive-portal first-run with build-time env-var
    config: build.rs reads TW_* env vars and embeds them as constants. No
    NVS read, no LittleFS mount, no AP mode, no HTTP server. The v0.1
    target user is the dev themselves; the first release usable by non-dev
    users is v0.9, and the v0.9 settings entry is reframed accordingly.
    ADR-005 updated to describe the build-time path and the v0.9 migration.

The two changes share files because the v0.1 spec is one interlocked
document; splitting further would require line-level surgery without
improving auditability.
This commit is contained in:
Julien Calixte
2026-05-14 20:40:09 +02:00
parent 668478aa40
commit 7055d01e9d
5 changed files with 153 additions and 82 deletions

View File

@@ -45,7 +45,7 @@ blocked by I/O.
| Task | Core | Stack | Responsibility |
| ------------- | ---- | ----- | -------------------------------------------------------- |
| `usb_task` | 0 | 8 KB | TinyUSB host loop, decode HID reports, post `KeyEvent`s |
| `wifi_task` | 0 | 8 KB | Provisioning AP or station mode; expose status |
| `wifi_task` | 0 | 8 KB | Station mode only (no provisioning AP); expose status |
| `ui_task` | 1 | 16 KB | Consume `KeyEvent`s, mutate editor state, enqueue render |
| `render_task` | 1 | 12 KB | Drain render queue, do partial/full refresh on EPD |
| `git_task` | 1 | 32 KB | Triggered by `Ctrl-G`; runs gitoxide commit + push |
@@ -60,11 +60,10 @@ read snapshot (render diff).
```
1. ROM → bootloader → app_main
2. Init PSRAM allocator, set as default for large alloc
3. Mount LittleFS on internal flash → read /nvs/config.toml
├─ no config → enter PROVISIONING mode
└─ config OK → continue
4. Mount FAT on SD → verify /sd/repo exists
├─ missing → enter PROVISIONING mode (clone)
3. Config is compiled into the binary (`build.rs` reads env vars) — no
filesystem read needed for it. LittleFS is unused in v0.1.
4. Mount FAT on SD → verify /sd/repo and /sd/repo/notes.md exist
├─ missing → fatal: "missing /sd/repo — re-mount SD and reboot"
└─ present → continue
5. Init SPI bus (shared: EPD + SD on different CS)
6. Init EPD, full refresh: splash + boot log
@@ -125,7 +124,7 @@ stack decision (e.g. fall back to libgit2; switch to a separate SD SPI bus).
| `←` `→` `↑` `↓` | move cursor (visual lines for ↑↓) |
| `Home` `End` | line start / end |
| `Ctrl-S` | save |
| `Ctrl-G` | save (if dirty) + commit + push |
| `Ctrl-G` | publish (save → stage → commit push, with pull-and-retry on remote divergence) |
Anything else is ignored in v0.1.
@@ -178,21 +177,44 @@ PSRAM heap during push is a top-3 watched metric — see
- `gix` with the smart-HTTP transport backed by `esp-idf` mbedtls (via a
custom transport impl, or `gix-transport` with `reqwest`+`rustls-mbedtls`
if that path is cleaner — decided in spike 7).
- Operations needed in v0.1:
- Operations needed in v0.1 (the [`gct` shell function](../CONTEXT.md#user-facing-actions) is the reference):
- `gix::open` the existing working copy at `/sd/repo`
- stage `notes.md` (`gix::index` add)
- commit with author from config, message `"wip <ISO-8601 timestamp>"`
- stage everything under the working copy (`git add .` equivalent)
- short-circuit return if nothing is staged — status: "nothing to publish"
- commit with author from config, message `"<ISO-8601 timestamp>"` (no `wip`
prefix; the timestamp *is* the message)
- push HEAD to `origin/<current branch>`
- on push failure: `git pull --no-edit` (merge), then retry the push once.
Only surface failure to the status line if the pull conflicts or the
second push also fails.
- The PAT is loaded into the Authorization header per request; never logged.
- Push errors propagate as a string back to the status line.
- The whole sequence is atomic from the user's view — see
[`CONTEXT.md` → Publish](../CONTEXT.md#user-facing-actions).
### `provisioning`first-run wizard
### Provisioning — build-time only (no module on device)
- Triggered when `config.toml` is absent OR `/sd/repo` is absent.
- Captive portal posts a JSON blob; device validates by:
1. Connecting to the supplied Wi-Fi credentials.
2. Cloning the supplied repo URL into `/sd/repo` using the supplied PAT.
- Only on both successes does it persist config and reboot into steady state.
v0.1 has **no provisioning module, no NVS config, no LittleFS mount**. All
config (Wi-Fi creds, remote URL, GitHub user, PAT, commit author) is supplied
as environment variables at build time:
```sh
export TW_WIFI_SSID=... TW_WIFI_PASS=...
export TW_REMOTE_URL=... TW_GH_USER=... TW_PAT=...
export TW_AUTHOR_NAME=... TW_AUTHOR_EMAIL=...
cargo espflash --release
```
`build.rs` reads these (or fails the build) and emits constants the runtime
references directly. The `.env` file used to source these is gitignored.
The git working copy is set up out-of-band: the dev clones the remote onto
the mounted SD card from their laptop. There is no "first clone on device"
in v0.1.
Net savings vs. an on-device wizard: ~300500 LoC of firmware (HTTP server,
captive AP, form parsing, validation state machine, NVS read/write, TOML
parser). On-device provisioning + NVS-backed config land in v0.9 when
non-dev users enter the picture.
## Memory plan