Commit Graph

252 Commits

Author SHA1 Message Date
Julien Calixte
2cd3bba98d feat(keymap): extract pure HID decode into host-testable crate
The Key type, US-QWERTY translate, and the edge-detecting boot-report
parser lived in usb_kbd.rs, unreachable by cargo test (the firmware
crate is pinned to the xtensa target). Move them to a dependency-free
#![no_std] + #![forbid(unsafe_code)] crate so the one path that parses
untrusted device bytes can be exercised on the host.

14 tests, including an ASCII-invariant sweep over all 256 usage IDs
(pins the guarantee the editor's byte==char indexing relies on) and a
never-panics fuzz over arbitrary-length/content reports.
2026-07-10 10:36:37 +01:00
Julien Calixte
f373892870 docs: add memory-safety audit of the Rust unsafe/FFI surface 2026-07-10 10:22:21 +01:00
Julien Calixte
371b542d44 docs(roadmap): plan Markdown snippets for v0.6
Trigger-driven expansion (no popup — e-ink refresh + distraction-free),
numbered empty tab stops + $0, no placeholder text, no dynamic values,
pause-triggered panel indicator. Records the why so scope isn't relitigated.
2026-07-08 22:44:33 +02:00
Julien Calixte
f197df6dbe style(editor): drop the mode line to the panel's bottom edge
Move the mode indicator (and the NO KBD flag above it) down so the
mode sits flush at the bottom-left with a 2 px margin, reclaiming the
dead space that was left below it.
2026-07-08 22:01:10 +02:00
Julien Calixte
ad047cc717 docs: reconcile screen-layout docs with the panel changes
Mode now sits at the side panel's bottom-left (not its top), and the
words-this-session field is gone. Update the CONTEXT.md field list and
the product-doc mock + prose to match; note the keyboard flag renders
as NO KBD (Latin-9 has no glyph for the mock's symbols).
2026-07-08 21:48:32 +02:00
Julien Calixte
ba4a8bfad8 feat(editor): put the mode indicator at the side-panel bottom-left
Move the mode + pending-command echo from the panel's top to its
bottom-left (vim `-- INSERT --` style); word count takes the top slot
and the NO KBD flag sits just above the mode. Also drop the
words-this-session field (not wanted) and its session_base_words state.
2026-07-08 21:48:21 +02:00
Julien Calixte
50f650fef6 feat(editor): split display into writing column and side panel
Carve the panel into a ~60-col writing column and a right-hand side
panel (per CONTEXT.md screen regions); the divider sits right of the
x=396 driver seam. The mode indicator and pending-command echo move
into the panel, while the ':' command line stays in the bottom strip.

The panel also shows word count and words-this-session (a snapshot
refreshed on a typing pause / non-Insert action, never per keystroke,
so ordinary typing keeps the fast windowed refresh) and a "NO KBD"
flag while the keyboard is dropped.
2026-07-08 21:33:41 +02:00
Julien Calixte
ddd9c387d3 feat(usb): track keyboard connection state
Add a persistent KBD_PRESENT atomic (set on setup, cleared on unplug)
and a keyboard_present() accessor. DEV_GONE is a one-shot detach event
the client loop consumes, not a state the panel can read.
2026-07-08 21:33:32 +02:00
Julien Calixte
d6b9e46ec1 docs: add index READMEs for docs/ and docs/notes/
Give the two linked-but-unindexed folders a browsable entry point so
directory links land on a real index instead of a bare listing.
2026-07-08 14:45:58 +02:00
Julien Calixte
dbe7720a27 docs: point directory links at their folder's README
Bare-folder links (e.g. docs/postmortems/) only resolve on GitHub's
auto-render; retarget them to the folder's README.md so they open a
real file in any markdown viewer.
2026-07-08 14:45:52 +02:00
Julien Calixte
fcf5815468 docs(roadmap): track the writing column + side panel split in v0.1
The side panel was referenced from v0.2.5/v0.5/v0.8 but never defined or
scheduled. Add it as a v0.1 build item, mark it unbuilt, and link the
CONTEXT.md and product-design definitions.
2026-07-08 11:02:41 +02:00
Julien Calixte
aa068147ab docs(roadmap): replace the mermaid Gantt with Macroplan TOML
The macroplan app parses this source directly; the baseline originals
stay fixed and v0.1's slip is expressed as an at-risk status instead of
a re-drawn bar.
2026-07-08 00:19:28 +02:00
Julien Calixte
c4326421fd docs(readme): link the gix postmortem where it is mentioned 2026-07-08 00:06:31 +02:00
Julien Calixte
fd6ccb729c Merge remote-tracking branch 'origin/main' 2026-07-08 00:05:48 +02:00
Julien Calixte
3128bca4ad docs(readme): sync to bench reality and slim via links
Fix stale claims: hardware IS on bench; git is libgit2/git2 as an
esp-idf component (ADR-004 kill-switch fired), not gix; display is the
in-tree SSD1683 driver; TLS handshake heap is the measured ~35 KB; v0.1
auth is build-time TW_* env vars (ADR-011 open). Replace the hardware
table, Gantt, and "why not" sections with links (docs/hardware.md,
docs/roadmap.md, ADR-001/002); update repo layout from planned to
actual; prune resolved open questions and link postmortems.
2026-07-08 00:04:49 +02:00
Julien Calixte
c82130a616 docs(roadmap): move the macro-plan Gantt in from the README
Kept as the original June 2026 baseline with a note pointing to the
Status block for actuals.
2026-07-08 00:04:32 +02:00
Julien Calixte
571c5ed825 docs(hardware): add part table, rationale, and bench status page
Extracted from the README so it can slim down to a summary + link.
2026-07-08 00:04:25 +02:00
b401ea2906 Updating docs/roadmap.md from Remanso 2026-07-07 23:52:38 +02:00
Julien Calixte
77768cc85f docs(roadmap): sync marks to actual core progress
The editor core runs 2-3 versions ahead of shippable device releases (no
release has shipped; v0.1's hardware gate is still open). Mark done/partial
items across v0.1-v0.7 with [x]/[~], record what landed early (View mode,
d/c operators + text objects, :fmt), and the v0.4 decision to move View off
v/V so Visual can take them.
2026-07-07 23:44:46 +02:00
Julien Calixte
b746d6e3f9 feat(editor): add Markdown affordances and a :fmt formatter
v0.6 affordances plus the v0.4 command-line mechanism, all in editor.rs:

- soft-wrap display lines at word boundaries (layout() no longer cuts
  mid-word; a single word wider than the panel still hard-breaks)
- Enter continues Markdown lists: -/*/+ repeat, N. increments, an empty
  item exits the list; only at end-of-line, indentation preserved
- heading lines (# .. ######) render faux-bold via a 1px double-strike
  (no bold Latin-9 font); checks the logical line so wrapped headings stay bold
- new `:` command mode (Command variant + status-strip echo, Esc/Backspace
  cancel) with one command, `:fmt`: align pipe tables (honoring :--: colons),
  collapse runs of blank lines to one, strip trailing whitespace; caret lands
  on roughly the same line afterwards
2026-07-07 23:44:46 +02:00
Julien Calixte
2aa4032e1f docs(notes): record why the notes repo stays full-size (pre-seed the SD)
The 150 MB of images are remanso's image CDN (it reads image bytes straight
out of git as blobs), not offloadable bloat — LFS/filter-repo/git-rm all
break remanso. libgit2 also lacks blobless partial clone. Decision: leave
the repo untouched and pre-seed the SD with a full clone from a computer, so
the device only ever takes the open + incremental fetch/push path. Also
notes the increment-C constraint: stage specific paths (not add_all) so a
media-excluding sparse checkout is safe.
2026-07-07 22:55:59 +02:00
Julien Calixte
ef755bac81 docs(postmortems): correct the FATFS delete finding (std, not read-only)
The read-only-attribute theory (and the predicted AM_RDO unlink shim) was
wrong. A diagnostic recursive walk showed every entry writable (ro=false)
and succeeded via path-based deletion, so the real blocker was
std::fs::remove_dir_all's openat/unlinkat/fdopendir usage against esp-idf's
path-based FATFS VFS. Update the increment-A/B checklist accordingly.
2026-07-07 22:49:18 +02:00
Julien Calixte
8dece735d1 fix(firmware): delete repo dir via path-based recursion, not remove_dir_all
std::fs::remove_dir_all deletes race-free via the openat/unlinkat/fdopendir
family; esp-idf's path-based FATFS VFS doesn't implement those, so it fails
with EACCES on the first entry. That looked like a read-only-attribute
problem but wasn't. A plain read_dir -> remove_file/remove_dir walk
(remove_tree) uses only what FATFS supports.

Hardware-verified: a diagnostic recursive delete removed the whole
/spiflash/repo (every entry reported writable, ro=false) then re-cloned +
pushed, twice. Wire remove_tree into both the RECLONE_EACH_BOOT reset and
the stale-clone recovery.

The p_open/p_creat shim (39e1155) stays: it keeps libgit2's objects writable
(ro=false), so neither this delete nor libgit2's own gc/repack/fetch-prune
is ever blocked by a read-only file on FAT.
2026-07-07 22:48:17 +02:00
Julien Calixte
8e848894fa feat(firmware): add RECLONE_EACH_BOOT toggle to git_sync
Debug/recovery knob (ships false): when true, wipe /spiflash/repo and re-clone
every boot instead of opening the persistent clone. Doubles as the validation
harness for the FAT read-only-delete fix — the wipe deletes libgit2-created
objects, which only succeeds once they are writable.
2026-07-07 09:33:08 +02:00
Julien Calixte
39e1155bf9 fix(libgit2): create objects writable so FAT can delete them
libgit2 creates loose objects and packfiles with mode 0444 (read-only). FATFS
turns that into the AM_RDO attribute and then refuses to f_unlink them (EACCES),
and esp-idf's FATFS VFS chmod cannot clear AM_RDO — so a written object can
never be deleted, blocking re-clone recovery and (later) fetch/repack.

Shim p_open/p_creat in esp_stubs.c to force owner-write into every create mode
(compile posix.c's originals under throwaway names, same mechanism as p_rename),
so nothing is ever created read-only. Immutability is only a safety hint on an
appliance where nothing but libgit2 touches these files.

Non-regressive on hardware: git_push, which links the same component, still
commits + pushes cleanly with the shim. The delete path itself is validated via
git_sync's RECLONE_EACH_BOOT knob (pending a dedicated flash).
2026-07-07 09:33:08 +02:00
Julien Calixte
4153cae9c4 docs(postmortems): record milestone #2A verified, FAT read-only-delete finding
Increment A (persistent-clone publish cycle) is hardware-verified: clone +
persistent open + fast-forward push. Mark the follow-up checklist: git module
fold is in progress (A done; B = divergence + AM_RDO unlink shim; C = editor
wiring), tech-doc revision done, PAT-in-flash tracked as open ADR-011. Note
the FAT read-only-delete limitation and the real-notes-repo sizing caveat.
2026-07-07 08:35:11 +02:00
Julien Calixte
afa61deaa6 fix(firmware): recover git_sync from a leftover clone dir
859c478 aborted if /spiflash/repo already existed — libgit2 refuses to
clone into a non-empty dir (code=Exists). Remove a writable stale/partial
clone and re-clone.

Milestone #2A now hardware-verified end to end: clone (boot 1) + persistent
open + fast-forward push (boot 2) against origin/main, on the 96 KB git
thread, min heap 6.8 MB — clone/checkout fit the stack, no bump needed.

Known limit: the remove only works on a *writable* leftover. libgit2 marks
objects/packs read-only and FATFS won't f_unlink a read-only file (EACCES);
POSIX chmod does not clear AM_RDO on esp-idf's FATFS VFS (verified). So
re-cloning over a run that wrote objects needs an AM_RDO-clearing unlink
shim in esp_stubs.c (increment B, also needed for fetch/repack); until then
the fallback is a one-time `espflash erase-region 0x310000 0x400000`.
2026-07-07 08:34:09 +02:00
Julien Calixte
859c4787cf feat(firmware): add persistent-clone git sync spike (git_sync)
Milestone #2 increment A: the product's real publish flow, distinct from
git_push's fresh-init-per-boot throwaway branch. open-or-clone a persistent
/spiflash/repo, append to a tracked notes.md, commit on top of the branch,
and fast-forward push over mbedTLS HTTPS+PAT. Reconcile handles up-to-date
and fast-forward; a true divergence (merge commit on FATFS) is deferred to
increment B. Runs on the dedicated 96 KB git thread; gated behind `git`.

Compiles clean; on-device clone/open+fast-forward not yet hardware-verified
(clone/checkout is deeper than the proven init path — the flash may need a
GIT_STACK bump).
2026-07-07 00:06:13 +02:00
Julien Calixte
cce62bf9f7 docs(adr): add ADR-011 for credential provisioning (open)
The PAT-in-flash shortcut (ADR-005 v0.1) is safe for the dev's bench unit
but not for a shipped one: it's plaintext in flash, the same token on every
unit, and rotation needs a reflash. ADR-005 decided the auth model but left
provisioning mechanics open.

Capture that as ADR-011 (Proposed/Open): the options (build-time bake →
on-device paste → eFuse-encrypted NVS, per-device fine-grained PAT, GitHub
App) and the likely shape, deferred beyond v0.1. It gates the first non-dev
distribution. Point the Spike 7 postmortem's shortcut entry at it.
2026-07-07 00:03:55 +02:00
Julien Calixte
2f2f1227ec docs(technical): rewrite git module section for libgit2
The v0.1 technical doc still described the gitoxide/gix plan with an
undecided transport and a 32 KB git_task stack. Bring it in line with
what spike 7 proved:

- gix → libgit2 via the git2 crate (ADR-004 kill-switch fired: no gix push)
- transport settled: HTTPS + PAT over esp-idf mbedTLS, no custom transport
  or reqwest/rustls layer needed
- fail-closed cert verification (embedded GitHub roots + PASSTHROUGH)
- git_task stack 32 KB → 96 KB (measured ~67 KB depth); dedicated thread
- persistent-clone + fast-forward is the product flow (spike used a fresh
  per-boot throwaway branch); add --all stages deletions
- PAT via libgit2 credential callback, never logged/persisted

Also mark the Spike 7 entry done in the spikes list.
2026-07-06 23:52:02 +02:00
Julien Calixte
d397b31ca4 docs(postmortems): mark dedicated git task done
Git now runs on its own 96 KB std::thread and the main-task stack is back
to 12 KB — hardware-verified off-main. Update the follow-up checklist and
the "three bugs" caveat to reflect the completed move.
2026-07-06 23:46:55 +02:00
Julien Calixte
15955a13f4 refactor(firmware): run git on a dedicated large-stack thread
libgit2's init→push chain is deeply stack-hungry (~67 KB measured for a
trivial config write; the push is deeper). Spike 7 sized the shared main
task stack at 96 KB for it, which forced the editor build to over-reserve
~80 KB.

Move git_publish onto its own std::thread (GIT_STACK = 96 KB via
Builder::stack_size) and join it from main. This lets
CONFIG_ESP_MAIN_TASK_STACK_SIZE drop back to 12288 — the Spike-6 value
proven to run the editor plus a TLS-on-main handshake — so the editor no
longer pays for git's stack.

Hardware-verified: the push ran off-main (mbedTLS + FATFS) with no panic,
no stack overflow, and no ENOMEM on the 96 KB spawn. This retires the
earlier "time() only works on the main task" misdiagnosis for good — it
was always the default 4 KB pthread stack overflowing, never thread-vs-main.
2026-07-06 23:46:55 +02:00
Julien Calixte
76fae36178 docs(postmortems): mark cert verification done, record HTTPS+PAT decision
The certificate_check bypass is retired (real trust-store, hardware-verified
2519ed8) and the product sync transport is decided (HTTPS+PAT, not SSH, since
on-device libgit2 is HTTPS-only). Remaining shortcut is PAT-in-flash.
2026-07-06 23:21:09 +02:00
Julien Calixte
2519ed8650 feat(firmware): verify git-push TLS chain against embedded GitHub CAs
Retire the cert-check bypass. Embed GitHub's root CAs (github_roots.pem:
USERTrust ECC/RSA + DigiCert G2/Global Root CA), write them to /spiflash/ca.pem
at boot, and point libgit2's mbedTLS stream at them via
GIT_OPT_SET_SSL_CERT_LOCATIONS (CONFIG_MBEDTLS_FS_IO is on). The push callback
now returns CertificatePassthrough instead of CertificateOk, so the http
transport maps it to `is_valid ? 0 : -1` (httpclient.c:805) -- an untrusted or
MITM cert fails the push (fail-closed), no blanket-accept.

Hardware-verified 2026-07-06: `TLS trust store installed`, chain validated
against the embedded USERTrust ECC root, push accepted (branch
device/1783372683 on jcalixte/typoena-test). Roots must be refreshed if GitHub
rotates CAs; a product would prefer esp-idf's bundle via a custom subtransport.
2026-07-06 23:19:24 +02:00
Julien Calixte
bcb4ffa465 docs(postmortems): record on-device Spike 7 push completion
The full init -> commit -> push over mbedTLS HTTPS now runs on hardware
(verified device-side + via git ls-remote). Documents the three fixes that
got there -- 96 KB main stack, remove-then-rename p_rename, and the utimes
existence-gate that had silently dropped every ODB write -- plus the
shortcuts still standing (cert bypass, PAT-in-flash, SSH-vs-HTTPS remote).
2026-07-06 22:59:49 +02:00
Julien Calixte
a15789a1b4 feat(firmware): add on-device git push spike (git_push)
Wi-Fi + SNTP + flash-FAT + libgit2 in one bench binary: init a fresh working
copy, commit, and push a per-boot device/<unix> branch over mbedTLS HTTPS with
PAT auth. Gated behind the `git` feature; built/flashed via `just flash-git-push`.

Status: local git init verified on hardware; the ODB-write fix and the full
push are not yet confirmed end-to-end on device. Cert verification is bypassed
in the push callback (spike shortcut) -- real trust-store wiring must land
before this leaves the bench.
2026-07-06 00:18:30 +02:00
Julien Calixte
63ffd7e37a feat(firmware): bake and document git push env vars
build.rs embeds TW_REMOTE_URL / TW_GH_USER / TW_PAT / TW_AUTHOR_* via env!()
so only the git_push binary carries them (the editor references none), and
.env.example documents them. NOTE: TW_PAT lands in the git_push flash image
-- a bench-only shortcut (ADR-005); a product must not embed the PAT.
2026-07-06 00:18:22 +02:00
Julien Calixte
8d80168bda fix(libgit2): make the loose ODB persist on FATFS via POSIX shims
The loose ODB silently dropped every write on device: utimes() returned 0
unconditionally, so libgit2's freshen probe (git_futils_touch -> p_utimes)
always reported "object exists" and git_odb_write skipped the write entirely
-- blobs/trees/commits never hit disk and write_tree failed with "invalid
object specified". Gate utimes on file existence (present -> 0, absent ->
ENOENT). Also add a remove-then-rename p_rename (FATFS f_rename can't replace
an existing target, no hardlinks), with posix.c's original scoped out in the
component CMakeLists, plus symlink/gai_strerror link stubs git_push pulls in.

p_rename was verified on hardware (cleared the rename error); the utimes fix
is diagnosed from the failure + libgit2 source and build-verified, on-device
confirmation still pending.
2026-07-06 00:18:16 +02:00
Julien Calixte
a0e58e029a fix(firmware): raise main task stack to 96 KB for libgit2 depth
libgit2's repository_init -> config-write -> FATFS -> wear-leveling chain
nests ~10 GIT_PATH_MAX (4 KB) stack buffers deep; a trivial config write
measured ~67 KB on hardware and overflowed the previous 48 KB, corrupting a
newlib lock handle (LoadProhibited in xQueueGenericSend). Shared with the
editor build, so this is temporary -- git should move to a dedicated
large-stack task and this can drop back to ~16 KB.
2026-07-06 00:18:07 +02:00
Julien Calixte
4441618d0f feat(firmware): add flash-FAT storage partition table
16 MB layout adding a `storage` FAT data partition for the on-device git
working copy. Applied only by `just flash-git-push` (espflash
--partition-table); the editor flash keeps its default single-app layout.
2026-07-06 00:18:00 +02:00
Julien Calixte
ac70d8926f docs(postmortems): record Path 2 -- libgit2 runs on device 2026-07-05 21:28:47 +02:00
Julien Calixte
b216a67c3b chore(firmware): add git2 build/flash/monitor just recipes 2026-07-05 21:28:47 +02:00
Julien Calixte
2b268fe168 feat(firmware): wire libgit2 component + bind git2 in system mode
Adds the extra_components metadata, git2 as an optional dep behind the
`git` feature (default-features off -> no openssl-sys/libssh2-sys), and a
git_smoke bin gated on it. libgit2-sys/libz-sys run in system mode against
fake pkg-config files (empty Libs) so they emit no link flags -- symbols
come from the esp-idf component. Proven on device: git2 version + a blob
SHA1 through the mbedTLS backend.
2026-07-05 21:28:47 +02:00
Julien Calixte
bea3058004 feat(firmware): add libgit2 esp-idf component (CMake + mbedTLS shims)
Builds vendored libgit2 as an esp-idf component so it inherits the
lwip/mbedtls/pthread/vfs/newlib include+link graph -- the include cascade
that sank CFLAGS injection never appears. Configured for mbedTLS via a
hand-written git2_features.h. Small port surface, libgit2 sources
untouched: poll.h shim, lstat==stat (esp_port.h), p_mmap via malloc+read
(esp_map.c), and identity/symlink stubs (esp_stubs.c). Registers empty
when LIBGIT2_SRC is unset so the editor build is unaffected.
2026-07-05 21:28:47 +02:00
Julien Calixte
8883a55df6 feat(firmware): vendor libgit2 v1.9.4 as a git submodule
Pins the exact version libgit2-sys 0.18.5 expects, so git2's safe API can
bind it in system mode. Sits under the esp-idf component that builds it;
clone with --recurse-submodules to fetch the source.
2026-07-05 21:28:47 +02:00
Julien Calixte
9f36d786d0 docs(postmortems): record path-1 dead end, pick libgit2 as component
CFLAGS include-injection cascades through the whole esp-idf component
graph (arpa/inet.h -> sys/ioctl.h -> ...), so path 1 is out. Decide
path 2: libgit2 as an esp-idf CMake component with USE_HTTPS=mbedTLS,
which fixes the includes and the TLS backend together.
2026-07-05 20:09:31 +02:00
Julien Calixte
80f21648ce docs(postmortems): record Spike 7 on-device probe and PSRAM
libgit2 C cross-compiles for xtensa; blocker is missing esp-idf lwIP
headers (arpa/inet.h) because libgit2-sys builds standalone. PSRAM
enabled and hardware-verified.
2026-07-05 20:04:11 +02:00
Julien Calixte
51c8f461c0 feat(firmware): enable octal PSRAM
Adds the 8 MB octal PSRAM to the heap allocator (verified on the N16R8:
detected, memory-tested, 8192K pooled). OCT mode is required or init
fails; speed left at 40 MHz for a safe first enable. Prerequisite for
the git working set and the rope buffer.
2026-07-05 20:04:11 +02:00
Julien Calixte
9c9ad63793 docs: record Spike 7 outcome, gix HTTPS-push kill-switch fired
Postmortem + ADR-004 outcome note + risk-table update: gitoxide has no
HTTP(S) push, so v0.1 falls back to libgit2; the on-device libgit2 <->
mbedtls cross-compile is the next gate.
2026-07-05 19:38:48 +02:00
Julien Calixte
12417a9be7 chore: scope rust-analyzer to the firmware crate
Pin linkedProjects so RA stops checking the new host spike crate
against the xtensa target.
2026-07-05 19:38:48 +02:00