Skip to content

Commit d2c1fbe

Browse files
committed
[docs] Freeze SHA-pinned connected GitHub import
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent fa535dd commit d2c1fbe

4 files changed

Lines changed: 647 additions & 23 deletions

File tree

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
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)

‎docs/decisions/README.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,5 +43,6 @@ Each ADR captures one significant decision: its context, the choice, and the con
4343
| [0037](0037-fixed-profile-reset-and-transfer-product-slos.md) | Use fixed ESP/Pico reset ceilings and one product-wide 64 KiB PUT/GET floor for replacement-v0.6.0 qualification | Accepted |
4444
| [0038](0038-replace-unpublished-v060-candidate-in-place.md) | Replace the unpublished local v0.6.0 candidate in place while routing qualification by source era rather than SemVer alone | Accepted |
4545
| [0039](0039-admit-waveshare-single-page-largest-block-transient.md) | Admit the characterized Waveshare single-page largest-block transient with a fixed 98304 floor in a V5 source era, with byte-identity-conditioned evidence carry-forward | Accepted |
46+
| [0040](0040-sha-pinned-connected-github-import.md) | Ship GitHub import first as a SHA-pinned, bounded connected-board subset while durable local files and provenance remain deferred | Accepted |
4647

47-
Decided 2026-06-30 (0001–0005); 0006 accepted 2026-07-01; 0007, 0008, 0009 and 0010 accepted 2026-07-02; 0011 accepted 2026-07-03; 0012 accepted 2026-07-25; 0013 accepted 2026-07-27; 0014, 0015, 0016, and 0017 accepted 2026-07-28; 0018–0022 accepted 2026-07-29; 0023 and 0024 accepted 2026-08-01; 0025 and 0026 accepted 2026-08-02; 0027–0029 accepted 2026-08-03; 0030 accepted 2026-08-11; 0031–0033 accepted 2026-08-12; 0034 accepted 2026-08-13; 0035 and 0036 accepted 2026-08-16; 0037 and 0038 accepted 2026-08-20; 0039 accepted 2026-08-21.
48+
Decided 2026-06-30 (0001–0005); 0006 accepted 2026-07-01; 0007, 0008, 0009 and 0010 accepted 2026-07-02; 0011 accepted 2026-07-03; 0012 accepted 2026-07-25; 0013 accepted 2026-07-27; 0014, 0015, 0016, and 0017 accepted 2026-07-28; 0018–0022 accepted 2026-07-29; 0023 and 0024 accepted 2026-08-01; 0025 and 0026 accepted 2026-08-02; 0027–0029 accepted 2026-08-03; 0030 accepted 2026-08-11; 0031–0033 accepted 2026-08-12; 0034 accepted 2026-08-13; 0035 and 0036 accepted 2026-08-16; 0037 and 0038 accepted 2026-08-20; 0039 accepted 2026-08-21; 0040 accepted 2026-08-22.

0 commit comments

Comments
 (0)