ADR-0008: What portalr reads of a native file: its words, its own check-out, its limit, and a proposal¶
Status: Accepted Date: 2026-10-17 Deciders: Alex Nodeland Amends: ADR-0007, whose routes these join
Context¶
ADR-0007 gave files_router a file's status, check-out, extension, check-in, and a version's bytes and pages. portalr's screens for phase 4 of lattice RFC-0003 need four things those routes don't give:
- A revision's words. "A revision diff shows the changed text, from the text blob." The manifest names the text blob by its digest, and no route serves it.
- The reader's own check-out. A check-in and an extension name the check-out, but the status names only whose it is. A person who checked a file out in one tab, or before a reload, has no way back to its id.
- The limit. The router refuses a check-in past
limit, but the page can't say so before the upload starts. - A proposal's file. A stale check-in is kept as a proposal, and "the reviewer sees both versions' pages". A proposal's content is named only in its patch, and the routes read versions alone.
Decision¶
GET /workspaces/{w}/files/{a}/versions/{v}/text: a version's words, as plain text, asNativeFiles.read(part="text")reads them, with the headers a version's bytes have:nosniff,Content-Security-Policy: sandbox,private, no-store, but no attachment. A version with no text isContentUnavailable, 409, as unavailable content is everywhere.Lease.checkout_id: the check-out's id, given only to the person whose check-out it is, read from the lease's holder,checkout:<id>:<participant>, when its participant is the caller's. Anyone else, and an agent's edit, gets None: the id is the only handle on a check-in, andNativeFilesrefuses it from anyone else regardless.FileStatus.limit: the router's ownlimit, in bytes, the largest check-in it takes.NativeFiles.proposed(ws, proposal_id, part=)andrender_proposed(ws, proposal_id, page), and two routes over them,GET /workspaces/{w}/proposals/{p}/textandGET /workspaces/{w}/proposals/{p}/pages/{n}. The proposal is read through the workspace (Workspace.proposal, which hides what the actor may not read), then its file (Workspace.get(NativeFile, ...)), so a hidden one of either is a missing one, 404. The blob is the one the proposal's JSON Patch last replaces the part with, as JSON Patch applies its operations in order, and asrevisebuilds every native revision; a proposal that replaces none, or clears it, such as an archive or a hand-written patch, isContentUnavailable, 409. The bytes are read under the proposal's artifact's key, so a proposal can name no other artifact's bytes. Each route still names a workspace and a proposal, never a bare digest.- The OpenAPI document,
schemas/artifactr-files.openapi.json, is regenerated, and portalr's client is typed from it.
Options considered¶
Option A: Reads beside the routes there are, each through the workspace (chosen)¶
| Dimension | Assessment |
|---|---|
| Fit | Each answers one need of the screens, through the same access checks as every other route |
| Size | Two methods on NativeFiles, three routes, two fields |
| Risk | A proposal's bytes are read only as its artifact's |
Option B: A route for a blob by its digest¶
Cons: RFC-0003 rules it out: every route names an artifact and a revision, so that a digest learned elsewhere opens nothing.
Option C: Keep the check-out's id in the browser¶
Cons: lost with the tab, or on another device; the server already knows whose the check-out is.
Trade-off analysis¶
The four reads are small, and each is resolved as the routes before them are. The alternative for each either breaks the RFC's rule on digests or leaves the screens unable to do what the RFC asks of them.
Consequences¶
- Easier: portalr compares a revision's words, takes a person back to their check-out, refuses a file too large before sending it, and shows a stale check-in's pages beside the head's.
- Harder: a proposal's pages are rendered on demand, a render each, as a version's are.
- Revisit: a cache for renders, keyed as RFC-0003 has it, once a deployment renders often.
Action items¶
- The text route, the check-out's id and the limit, and the proposal's reads, with their tests and the regenerated document.
- portalr's screens that read them (lattice RFC-0003, phase 4).