|
| 1 | +# ADR 0040 — Ship GitHub import first as a SHA-pinned connected-board subset |
| 2 | + |
| 3 | +**Status:** Accepted (2026-08-22). Scopes a connected working-loop subset of |
| 4 | +story A-33. Extends [ADR-0009](0009-runtime-connection-manager.md) (runtime |
| 5 | +connection sessions) and [ADR-0010](0010-working-loop-in-memory-document.md) |
| 6 | +(durable local persistence deferred to A-24). It does **not** close full A-33, |
| 7 | +FR-IMPORT-4, FR-IMPORT-6, or DAT-6. |
| 8 | + |
| 9 | +## Context |
| 10 | + |
| 11 | +PyBLE already has a connected Files surface and a verified |
| 12 | +`Connection.putFile` path. The next useful increment is therefore a bounded |
| 13 | +way to browse a public GitHub repository and copy selected Python examples to |
| 14 | +the connected board. The full product contract is broader: imported files |
| 15 | +must also become durable local project files and the app must record their |
| 16 | +provenance. Those capabilities depend on the A-24 Drift project store, which |
| 17 | +is not part of the current working loop. |
| 18 | + |
| 19 | +Remote source is untrusted, mutable, and network-dependent. A branch name can |
| 20 | +move between directory browse and content download; repository entries can be |
| 21 | +symlinks or submodules; file names can become unsafe board paths; GitHub can |
| 22 | +rate-limit unauthenticated clients; and a multi-file PBLE/1 upload is not an |
| 23 | +atomic transaction. The UI must not imply that browsing is execution, that a |
| 24 | +batch is all-or-nothing, or that a branch label identifies immutable bytes. |
| 25 | + |
| 26 | +This feature is authored fresh against GitHub's public REST contract and |
| 27 | +PyBLE's existing `Connection` seam. A behavioral request to offer convenient |
| 28 | +example download does not authorize copying another application's source, |
| 29 | +assets, endpoint construction, data model, or interaction implementation. |
| 30 | + |
| 31 | +## Decision |
| 32 | + |
| 33 | +**The first GitHub-import increment is a connected Files action that resolves |
| 34 | +one public repository/ref to an immutable commit, lazily browses that snapshot, |
| 35 | +downloads and validates one bounded batch completely, and only then uploads |
| 36 | +the selected `.py` files sequentially to the current board directory.** |
| 37 | + |
| 38 | +1. **Repository input is deliberately narrow.** The user enters a canonical |
| 39 | + public repository-root URL of the form |
| 40 | + `https://github.com/<owner>/<repository>` and an optional ref. One trailing |
| 41 | + slash may be normalized away. Credentials, ports, query/fragment data, |
| 42 | + `.git`, `tree`/`blob` subpaths, non-HTTPS schemes, non-`github.com` hosts, |
| 43 | + and any path other than exactly two non-empty segments are rejected. The |
| 44 | + app sends no account credential, token, cookie, board datum, or user file. |
| 45 | + If ref is blank, it reads the repository's public `default_branch`; an |
| 46 | + explicit ref may name a public branch, tag, or commit. |
| 47 | + |
| 48 | +2. **Every browse is pinned before it begins.** The GitHub REST API resolves |
| 49 | + the explicit or default ref once to the returned full commit SHA. The UI |
| 50 | + displays that SHA and every subsequent directory/content request is derived |
| 51 | + from that immutable commit, never from the moving ref and never from a |
| 52 | + response-provided arbitrary download URL. A refresh of the ref is an |
| 53 | + explicit new resolution and clears the old tree and selection. |
| 54 | + |
| 55 | +3. **Directory browsing is lazy and selection is folder-scoped.** Resolving a |
| 56 | + repository does not recursively enumerate its tree. Opening a directory |
| 57 | + fetches only that directory at the pinned commit; Up is bounded at the repo |
| 58 | + root. A selection contains direct children of one currently chosen remote |
| 59 | + directory only. Changing directory clears it. Only Git-tree entries with an |
| 60 | + ordinary blob mode (`100644` or `100755`) and a lowercase `.py` suffix are |
| 61 | + eligible. Directories remain browsable; symlink mode `120000`, submodule |
| 62 | + mode `160000`, non-files, `.mpy`, `.pyc`, and every other suffix are visible |
| 63 | + only as ineligible or omitted and are never fetched or written by this |
| 64 | + increment. |
| 65 | + |
| 66 | +4. **The board target is explicit and flat for this subset.** Opening the |
| 67 | + action captures the Files controller's current board directory (`cwd`). |
| 68 | + Each selected direct child maps to |
| 69 | + `join(capturedCwd, basename(remotePath))`; no remote parent is recreated and |
| 70 | + this subset never calls `Connection.mkdir`. Before download, the review |
| 71 | + surface shows every exact remote path and exact board target path. It |
| 72 | + rejects empty/dot names, separators, NUL, duplicate targets, paths outside |
| 73 | + the captured directory, and targets over PBLE/1's 128-byte UTF-8 path |
| 74 | + ceiling. Preserving a remote hierarchy is a future, separately specified |
| 75 | + extension. |
| 76 | + |
| 77 | +5. **Overwrite is always an informed choice.** Review lists the captured board |
| 78 | + directory and marks exact target names already present. Existing |
| 79 | + directories or other non-regular conflicts block the batch. Existing files |
| 80 | + require a separate, explicit confirmation naming the target paths; selecting |
| 81 | + Import is not itself overwrite consent. Immediately before the first PUT, |
| 82 | + the controller lists the directory again. A new or changed conflict |
| 83 | + invalidates prior consent and returns to review without writing. |
| 84 | + |
| 85 | +6. **All bytes are acquired and validated before any board mutation.** The |
| 86 | + controller fetches every selected object from the pinned snapshot into one |
| 87 | + bounded in-memory candidate batch. Each raw file is at most 256 KiB; the |
| 88 | + sum of raw selected bytes is at most 1 MiB. Declared sizes are an early |
| 89 | + rejection only; actual decoded byte counts are authoritative. Every file |
| 90 | + must decode as strict UTF-8 and contain no NUL byte. A missing, changed-type, |
| 91 | + mismatched-path/object, HTTP, rate-limit, decode, or bound failure rejects |
| 92 | + the whole candidate before the first `putFile`. Candidate bytes are not |
| 93 | + interpreted, preview-executed, persisted, or logged. |
| 94 | + |
| 95 | +7. **One connection session owns the commit.** The action captures the stable |
| 96 | + `Connection` facade's opaque local session stamp with `cwd`. The stamp must |
| 97 | + still be current before every board preflight and immediately before each |
| 98 | + `putFile`; attach or detach advances it, including reconnecting the same |
| 99 | + board. A mismatch stops the batch before the next board verb. This stamp is |
| 100 | + in-memory action consistency only—not identity, authentication, persisted |
| 101 | + provenance, or PBLE/1 wire data. |
| 102 | + |
| 103 | +8. **The commit is sequential and honestly non-atomic.** After the entire |
| 104 | + candidate is valid and overwrite consent is current, files are uploaded in |
| 105 | + the stable order displayed by review through sequential |
| 106 | + `Connection.putFile(targetPath, bytes)`. There is no `mkdir`, parallel PUT, |
| 107 | + rollback claim, automatic editor open, Save, Run, or `runFile`/`runSource`. |
| 108 | + On the first upload failure, cancellation request, or stale session, no |
| 109 | + further PUT begins. The result distinguishes succeeded, failed/current, and |
| 110 | + unattempted target paths and says that successful earlier files may already |
| 111 | + be present. The Files controller refreshes the captured directory after any |
| 112 | + outcome that may have written a file. |
| 113 | + |
| 114 | +9. **Network state and cancellation are first-class.** Resolve, browse, fetch, |
| 115 | + review, upload, cancelled, failed, partial, and complete are distinct states; |
| 116 | + one action cannot be started twice. Browse/fetch cancellation aborts a |
| 117 | + request where supported and always ignores late responses through an |
| 118 | + operation generation. It causes zero board writes before commit. During a |
| 119 | + PUT, cancellation is cooperative: the in-flight verified PUT may finish, |
| 120 | + but no next PUT starts, and the partial result remains visible. HTTP |
| 121 | + offline/not-found/forbidden/rate-limit/server/malformed-response failures |
| 122 | + map to localized user actions. `403`/`429` rate limiting shows the public |
| 123 | + API limit and reset/retry guidance when GitHub supplies it; the app performs |
| 124 | + no tight or unbounded automatic retry. |
| 125 | + |
| 126 | +10. **The surface is adaptive and accessible.** Import examples is an action |
| 127 | + on the connected Files surface, enabled only for a ready connection. Below |
| 128 | + 600 dp the same controller is presented as a scroll-controlled modal bottom |
| 129 | + sheet; at 600 dp and wider it is a dialog. It survives keyboard insets and |
| 130 | + 2× text without clipped actions; controls are at least 48 dp; directory |
| 131 | + rows, file eligibility/selection, loading state, pinned commit, target |
| 132 | + mapping, overwrite warning, progress, cancellation, and final result have |
| 133 | + localized labels and semantics. Focus moves to the first invalid field or |
| 134 | + error and returns to the invoking Files action on dismissal; important |
| 135 | + errors and the final outcome are announced once. |
| 136 | + |
| 137 | +11. **The delivery remains test-driven and clean-room.** Unit tests cover URL |
| 138 | + and host rejection, ref-to-SHA pinning, lazy traversal, entry filtering, |
| 139 | + safe target derivation, UTF-8/NUL/size bounds, rate/error mapping, operation |
| 140 | + generations, and result accounting. Widget tests use an injected fake |
| 141 | + GitHub client and `FakeConnection` to prove overwrite consent, all-fetches- |
| 142 | + before-first-PUT, sequential order, session changes, cancellation, partial |
| 143 | + truth, refresh, and zero open/run/mkdir calls. Goldens cover compact and |
| 144 | + wide layouts, portrait/landscape, loading/error/review/partial states, 2× |
| 145 | + text, high contrast, and keyboard inset. An integration/HIL row imports a |
| 146 | + small public pinned fixture into a disposable board directory and verifies |
| 147 | + exact bytes and no execution. No test depends on copying another app or on |
| 148 | + an unbounded live-network response. |
| 149 | + |
| 150 | +12. **Full A-33 remains open.** This increment keeps candidates only in |
| 151 | + bounded memory long enough to write the connected board. FR-IMPORT-4's |
| 152 | + durable local project copy and FR-IMPORT-6/DAT-6's provenance record are |
| 153 | + carried, not dropped, and re-freeze with A-24/full A-33. A later increment |
| 154 | + must persist the exact pinned commit SHA and imported bytes transactionally |
| 155 | + before it may claim offline continuity or full import provenance. |
| 156 | + |
| 157 | +## Alternatives considered |
| 158 | + |
| 159 | +- **Download raw branch URLs directly.** Rejected because a branch can move |
| 160 | + between browse and fetch, and response-provided URLs broaden the network |
| 161 | + trust boundary unnecessarily. |
| 162 | +- **Recursively import a remote tree and recreate it with `mkdir`.** Rejected |
| 163 | + for this subset: recursion increases API/rate/path/conflict complexity and |
| 164 | + PBLE/1 has no multi-file transaction. One selected folder of basenames is |
| 165 | + useful, bounded, and truthful. |
| 166 | +- **Stream each GitHub response straight into `putFile`.** Rejected because a |
| 167 | + late invalid or oversized file would leave an avoidable partial board batch. |
| 168 | + Fetch-first gives all network/content validation a zero-mutation failure |
| 169 | + boundary. |
| 170 | +- **Treat sequential PUTs as atomic or roll them back.** Rejected because a |
| 171 | + compensating delete could destroy an overwritten pre-existing file and the |
| 172 | + protocol supplies no directory transaction. The result reports reality. |
| 173 | +- **Implement Drift files/provenance in this increment.** Rejected because it |
| 174 | + would pull the A-24 persistence schema into a connected UI slice. The full |
| 175 | + requirements remain explicit rather than being weakened. |
| 176 | + |
| 177 | +## Consequences |
| 178 | + |
| 179 | +- Users can deliberately copy public Python examples into the exact board |
| 180 | + folder they are viewing, with immutable source identity and bounded memory. |
| 181 | +- GitHub is the sole optional network surface. Editing, Files, BLE, and Run |
| 182 | + remain usable during GitHub outage or rate limiting. |
| 183 | +- The board may contain a truthful partial batch after a PUT failure or |
| 184 | + cancellation. The app exposes that state and refreshes instead of claiming |
| 185 | + rollback or success. |
| 186 | +- Imports do not survive locally and have no durable provenance yet. That is a |
| 187 | + visible, recorded A-24/full-A-33 obligation, not an accidental omission. |
| 188 | + |
| 189 | +## Related |
| 190 | + |
| 191 | +- App requirements [§4.9](../specifications/App/specs.md#49-github-public-repo-import-libgithub_import--fr-import) |
| 192 | +- App design [§10](../specifications/App/TDD.md#10-github-import-design-libgithub_import) |
| 193 | +- [ADR-0009](0009-runtime-connection-manager.md) |
| 194 | +- [ADR-0010](0010-working-loop-in-memory-document.md) |
0 commit comments