steid

@jamesgill /

docs: fold the blame handover into plans

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGBYVHV86DDvJCBKsDFHT6
JamesPatrickGill authored 15 hours agoparente141fa6Browse files691bf9881d2fda6c26a653b74d57afcca98a1f42

4 files changed+48 −146

plans/current.md+12 −0View file
@@ -184,6 +184,18 @@ instance, and Steid's own source is pushed to it and browsable there.
184184 - **The compare form's `<datalist>` costs a `list_refs`** rendered inline on every visit.
185185 - ~~Nothing links to a commit by its sha yet~~ — done.
186186
187+- **The blame page has no revision switcher**, to save a `list_refs` and a
188+ `Switch::Blame` arm in `web/browse.rs` while five agents edited it. About four lines
189+ now the branches are merged.
190+- **`BlameCommit::boundary` and `author_name` reach only the tooltip.** The row is sha,
191+ summary, date and must stay one line. On a multi-user instance the author is the first
192+ thing to revisit.
193+- **Blame is the most expensive read in `GitQuery`** and the likeliest to meet the 20 s
194+ timeout. Nothing but this page calls it.
195+- **Cross-branch links were verified only after the merge**: blame's sha links to the
196+ commit page, search's line links to `blob#L<n>`, and the branches page's Compare link
197+ each landed on a different branch. See the integration check below.
198+
187199 ### Carried over — small, unblocked
188200
189201 - **A client that disappears mid-request leaves the body-copy task waiting.** The copy
plans/handover-blame.md+0 −145
@@ -1,145 +0,0 @@
1# Handover — `blame` (branch `feat/blame`)
2
3For the orchestrator to fold into `progress.md`, `current.md` and `ui.md`. Written
4instead of editing those files directly, because five agents appending to them at once
5would conflict every time.
6
7## What shipped
8
9`GET /{handle}/repos/{name}/blame/{rev}/-/{*path}` — the same file as the blob page,
10read by who last changed each line. `Code · Blame · Raw` in the file header of **both**
11views, so the blob page gained an entry point rather than blame being reachable only by
12typing a URL. That is the `ui.md` "Where the next features go" row for Blame, filled in
13as written: *a toggle in the blob's own header, beside Raw*.
14
15Files: `src/application/blame.rs` (new), `src/infrastructure/web/blame.rs` (new),
16`GitQuery::blame` on `application/port.rs`, its two implementations, and three
17visibility widenings plus a header swap in `web/browse.rs`.
18
19## For `progress.md` — decisions worth not rediscovering
20
21- **The porcelain parser is pure and lives in the application layer.** All the fiddly
22 knowledge about `git blame --porcelain` is about the *format*, not about the binary:
23 the commit header block is written **only the first time a commit appears**, so every
24 later run of the same commit is a bare sha line and its details have to be looked up
25 from an accumulated map. A line of code can look exactly like a header line — a file
26 full of shas would reparse itself into nonsense — and **the leading tab is the only
27 thing that tells them apart**. Both are pinned by tests.
28- **`parse_blame` never fails.** A record git spells in a shape it does not recognise is
29 skipped. Blame renders fine missing one line's attribution; refusing the page would
30 turn a future git version into an outage.
31- **`filename` is per commit, not per hunk.** git emits it inside the first header block
32 for a commit and not again, so it is carried on `BlameCommit`. That is what makes the
33 "moved from" marker work at all — measured against real output, not assumed.
34- **The use case reads the blob before blaming.** Binary, too-large and not-there are
35 then decided by exactly the rules the blob page uses, from the same `MAX_BLOB_BYTES`,
36 rather than by a second set free to disagree. It costs one `cat-file`. The alternative
37 — deriving those states from blame's own output — **cannot work**: the porcelain
38 stream carries the bytes without saying what they are, so a binary file blames into
39 garbage rather than reporting itself.
40- **A timeout is a page, and it does not matter which read timed out.** The page catches
41 `Error::GitQuery(e) if e.is_timeout()` around the whole use case, so a timeout in the
42 blob read renders the same "Blame took too long for this file" as one in blame itself.
43 Verified by temporarily setting `GIT_TIMEOUT` to 1ns (screenshotted, then restored).
44- **The commit cell is one line tall, on purpose.** Two lines and the blame and the blob
45 disagree about where line 400 is, which is the one thing that would stop them reading
46 as two views of one file. Row height was measured at 19.5px in both, matching.
47- **The age tint is linear in time, not in rank.** `AGE_STEPS = 5`, scaled between the
48 file's own oldest and newest commit. Consequence worth knowing: **a file whose history
49 is bunched shows only the end steps.** `src/infrastructure/git_query.rs` on this branch
50 has four commits at two moments a week apart, so it renders as steps 0 and 4 only,
51 and looks like a two-value marker rather than five. `plans/progress.md`, whose history
52 is spread over five weeks, uses the middle steps and is the file to look at to judge
53 the effect. Rank-based quintiles would always use all five and always look richer —
54 and would lie about the gaps. Linear was kept; if it is ever changed, that is the
55 trade being made.
56- **A file written in one commit takes the *top* step**, not the bottom. There is
57 nothing older to contrast it with, and rendering it as uniformly ancient would be a
58 claim the data does not support.
59- **The tint is plain CSS in `styles.css`**, five `.blame-age-N` rules built from
60 `color-mix(in oklab, var(--primary) N%, transparent)`. Not a Tailwind utility, because
61 the colour is a token at a fraction of an alpha; `color-mix` keeps it a token, so it
62 follows a palette change and both colour schemes without a `.dark` variant. It rides
63 on the leftmost `<td>` of **every** row in a run, so consecutive rows draw one
64 unbroken edge under `border-collapse`.
65- **Chrome headless writes screenshots to files; the puppeteer MCP tool does not.**
66 `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless
67 --hide-scrollbars --window-size=W,H --screenshot=path URL` is how the PNGs in this
68 handover were made, with the MCP browser used for measuring computed styles.
69- **`topcoat dev` does not always pick up a rebuild**, and its symptom is silent: it
70 keeps printing `no changes; application up to date` while serving the previous binary.
71 A `pkill -f "topcoat dev"`, wait for the port to free, then restart is the fix. Two
72 restarts in a row failed with `AddrInUse` because the old process had not yet exited.
73
74## For `current.md` — holes opened, shortcuts taken
75
76- **`/{handle}/repos/{name}/commits/{sha}` is linked and does not exist on this branch.**
77 Every sha in the blame's commit column points at it. This is the URL contract with the
78 `commit-page` agent; if that branch does not land, every blame row has a dead link and
79 the fix is one function (`commit_url` in `web/blame.rs`).
80- **Line-number links point at `blob#L<n>`.** The anchors are another agent's work. Until
81 that lands the link navigates to the blob and does not scroll.
82- **The blame page has no revision switcher**, deliberately: it would cost a whole `git`
83 process (`list_refs`, ~14ms) and would need a `Switch::Blame` variant in
84 `web/browse.rs` — a shared file five agents were editing. Reconsider once the branches
85 are merged; a `Switch::Blame(&path)` arm is about four lines.
86- **`BlameCommit::boundary` is parsed and tested but only reaches the tooltip.** A
87 boundary commit is where git stopped walking, so its lines may be older than it is.
88 There is nowhere to say that on a row that must stay one line tall. If it matters
89 later, the place for it is a marker beside the sha.
90- **`BlameCommit::author_name` is shown only in the tooltip** for the same reason. The
91 row is sha, summary, date; the author did not fit, and on a personal instance every
92 commit has the same author anyway. On a multi-user instance this is the first thing to
93 revisit.
94- **The blame page makes 4 `git` processes** for a text file: `cat-file --batch-check` +
95 `cat-file blob` for the blob read, then `cat-file --batch-check` + `git blame` for the
96 blame. Two of those are the same existence question asked twice — once by the use case
97 through `read_blob`, once inside the adapter's `blame` because the port must be
98 self-contained. **A kept-alive `cat-file --batch` collapses both**, which is the same
99 upgrade 0006 defers for the per-file last-commit column. Until then it is the price of
100 the port not assuming its caller checked.
101- **Blame is the most expensive read in `GitQuery`** and is the read most likely to meet
102 the 20s timeout. Nothing but this page calls it.
103
104## For `ui.md`
105
106The Blame row in "Where the next features go" is done; it can move out of that table
107into the page's own description. Suggested text for **The repository page**:
108
109- **A file has two views and one header.** `Code · Blame · Raw` sits where Raw alone
110 used to, with the active view in the primary colour — the same rule the tab strip
111 follows, so the primary colour still means position and nothing else. Raw is never
112 "active": it downloads rather than displays.
113- **Blame's rows are the blob's rows.** Same type, same leading, same height, so the two
114 views of a file are two readings of one thing rather than two pages. The commit is
115 shown once per run of lines and is held to a single line to keep that true.
116- **The age tint is texture, not a heat map.** A hairline down the left of each run,
117 five steps of the primary colour at 5–30% alpha, scaled between the file's own oldest
118 and newest commit. It should tell you which end of a file is moving and nothing more
119 precise; if it ever reads as a value to look up, it is too strong.
120- The page's cost: **4 `git` processes**, up from the blob page's 4 (3 + the switcher).
121
122## Verified
123
124- `cargo test` — **494 passed, 0 failed** (21 of them new). `cargo clippy --all-targets`
125 — zero warnings. `cargo fmt` clean.
126- Run on port 3105 from this worktree against a claimed instance holding this
127 repository's own history, `v0.1.0` and `v0.2.0` annotated tags, and a `fixtures`
128 branch carrying a 1.7 MB text file, a binary file and a one-commit file.
129- Looked at in a browser in **both colour modes** and at **800px**: no horizontal page
130 overflow (`document.scrollWidth == innerWidth`), the table scrolls inside its own
131 container, and the commit column narrows to `w-56` below `lg` so the code keeps the
132 room.
133- Every state screenshotted and looked at: many commits, one commit, a spread of tint
134 steps, a renamed file (the `↳` marker beside the sha, verified end to end against a
135 real `git mv`), an empty file, binary, too large, timeout, the blob header's new
136 toggle, and 800px.
137
138## Not verified
139
140- **The commit link's destination**, because the page does not exist on this branch.
141- **A real timeout.** The state was provoked by lowering `GIT_TIMEOUT` to 1ns, not by a
142 repository large enough to take 20 seconds.
143- **A file with a run of lines carried through a rename in *this repository's* own
144 history.** The marker was verified against a `git mv` pushed to the fixture branch, and
145 the parse against captured porcelain; no existing Steid source file has been renamed.
plans/progress.md+27 −0View file
@@ -109,6 +109,33 @@ latest-commit bar links to the commit page.
109109 - **The file header is `sticky`, which is why the diff panel is not `overflow-hidden`**:
110110 a hidden overflow makes an ancestor a scroll container and sticky sticks to nothing.
111111
112+**Blame (merged 2026-09-05).** `/blame/{rev}/-/{path}`, with `Code · Blame · Raw` in the
113+file header of both views.
114+
115+- **The porcelain parser is pure, in `application/blame.rs`.** `git blame --porcelain`
116+ writes a commit's header block only the *first* time the commit appears; later runs are
117+ a bare sha line looked up from an accumulated map. A code line can look exactly like a
118+ header line, and **the leading tab is the only thing that tells them apart**. Both are
119+ pinned by tests. `parse_blame` never fails: an unrecognised record is skipped.
120+- **`filename` is per commit, not per hunk**, carried on `BlameCommit`; that is what makes
121+ the "moved from" marker work.
122+- **The use case reads the blob before blaming**, so binary, too-large and not-there are
123+ decided by the blob page's rules from the same cap. Blame's own output cannot tell: a
124+ binary file blames into garbage rather than reporting itself. Costs one `cat-file`.
125+- **The commit cell is one line tall, on purpose**: two lines and blame and blob disagree
126+ about where line 400 is. Row height measured at 19.5px in both.
127+- **The age tint is linear in time, not rank**, five steps between the file's oldest and
128+ newest commit, as `color-mix` of the primary in plain CSS. A file whose history is
129+ bunched shows only the end steps; rank quintiles would always look richer and lie about
130+ the gaps. A file written in one commit takes the top step.
131+- **Blame is 4 processes** (two `batch-check`s asking the same existence question, one
132+ from the use case and one inside the adapter). A kept-alive `cat-file --batch`
133+ collapses both — the same upgrade 0006 defers.
134+- **Headless Chrome writes screenshots to files; the puppeteer MCP tool does not.**
135+ `--headless --hide-scrollbars --window-size=W,H --screenshot=path URL`. And `topcoat
136+ dev` sometimes keeps printing "no changes; application up to date" while serving the
137+ previous binary; `pkill -f "topcoat dev"`, wait for the port, restart.
138+
112139 ### Milestone 0 — Skeleton · done
113140
114141 Topcoat 0.5 app serving pages, `AppConfig` from `STEID_*` env, SQLite pool in app
plans/ui.md+9 −1View file
@@ -154,6 +154,14 @@ to say, pushing the code below the fold.
154154 colour is used only for links out of a dead end ("View the whole file", "Compare them
155155 the other way round").
156156
157+- **A file has two views and one header.** `Code · Blame · Raw` sits where Raw alone
158+ used to, the active view in the primary colour, the same rule as the tab strip. Raw is
159+ never active: it downloads. **Blame's rows are the blob's rows** — same type, leading
160+ and height, so they read as two readings of one thing; the commit is shown once per run
161+ and held to a single line. **The age tint is texture, not a heat map**: a hairline down
162+ the left of each run at 5–30% of the primary. If it ever reads as a value to look up, it
163+ is too strong.
164+
157165 ### Where the next features go
158166
159167 The slots matter as much as what fills them today. Written down so the next feature
@@ -165,7 +173,7 @@ lands in the frame rather than beside it:
165173 | A commit page | done — the sha in the latest-commit bar and in the log |
166174 | Branches, Tags | done — the counts beside the switcher and in the sidebar |
167175 | Compare | done — a branch row on the branches page, or `/compare` directly |
168| Blame | a toggle in the blob's own header, beside Raw |
176+| Blame | done — the toggle in the file header, both views |
169177 | Archive download | done — two small links under the clone URL |
170178 | Code search | done — the box on the right of the toolbar row |
171179 | Per-file last commit | a column in the listing, once `cat-file --batch` is kept alive |