# Handover — `blame` (branch `feat/blame`)

For the orchestrator to fold into `progress.md`, `current.md` and `ui.md`. Written
instead of editing those files directly, because five agents appending to them at once
would conflict every time.

## What shipped

`GET /{handle}/repos/{name}/blame/{rev}/-/{*path}` — the same file as the blob page,
read by who last changed each line. `Code · Blame · Raw` in the file header of **both**
views, so the blob page gained an entry point rather than blame being reachable only by
typing a URL. That is the `ui.md` "Where the next features go" row for Blame, filled in
as written: *a toggle in the blob's own header, beside Raw*.

Files: `src/application/blame.rs` (new), `src/infrastructure/web/blame.rs` (new),
`GitQuery::blame` on `application/port.rs`, its two implementations, and three
visibility widenings plus a header swap in `web/browse.rs`.

## For `progress.md` — decisions worth not rediscovering

- **The porcelain parser is pure and lives in the application layer.** All the fiddly
  knowledge about `git blame --porcelain` is about the *format*, not about the binary:
  the commit header block is written **only the first time a commit appears**, so every
  later run of the same commit is a bare sha line and its details have to be looked up
  from an accumulated map. A line of code can look exactly like a header line — a file
  full of shas would reparse itself into nonsense — and **the leading tab is the only
  thing that tells them apart**. Both are pinned by tests.
- **`parse_blame` never fails.** A record git spells in a shape it does not recognise is
  skipped. Blame renders fine missing one line's attribution; refusing the page would
  turn a future git version into an outage.
- **`filename` is per commit, not per hunk.** git emits it inside the first header block
  for a commit and not again, so it is carried on `BlameCommit`. That is what makes the
  "moved from" marker work at all — measured against real output, not assumed.
- **The use case reads the blob before blaming.** Binary, too-large and not-there are
  then decided by exactly the rules the blob page uses, from the same `MAX_BLOB_BYTES`,
  rather than by a second set free to disagree. It costs one `cat-file`. The alternative
  — deriving those states from blame's own output — **cannot work**: the porcelain
  stream carries the bytes without saying what they are, so a binary file blames into
  garbage rather than reporting itself.
- **A timeout is a page, and it does not matter which read timed out.** The page catches
  `Error::GitQuery(e) if e.is_timeout()` around the whole use case, so a timeout in the
  blob read renders the same "Blame took too long for this file" as one in blame itself.
  Verified by temporarily setting `GIT_TIMEOUT` to 1ns (screenshotted, then restored).
- **The commit cell is one line tall, on purpose.** Two lines and the blame and the blob
  disagree about where line 400 is, which is the one thing that would stop them reading
  as two views of one file. Row height was measured at 19.5px in both, matching.
- **The age tint is linear in time, not in rank.** `AGE_STEPS = 5`, scaled between the
  file's own oldest and newest commit. Consequence worth knowing: **a file whose history
  is bunched shows only the end steps.** `src/infrastructure/git_query.rs` on this branch
  has four commits at two moments a week apart, so it renders as steps 0 and 4 only,
  and looks like a two-value marker rather than five. `plans/progress.md`, whose history
  is spread over five weeks, uses the middle steps and is the file to look at to judge
  the effect. Rank-based quintiles would always use all five and always look richer —
  and would lie about the gaps. Linear was kept; if it is ever changed, that is the
  trade being made.
- **A file written in one commit takes the *top* step**, not the bottom. There is
  nothing older to contrast it with, and rendering it as uniformly ancient would be a
  claim the data does not support.
- **The tint is plain CSS in `styles.css`**, five `.blame-age-N` rules built from
  `color-mix(in oklab, var(--primary) N%, transparent)`. Not a Tailwind utility, because
  the colour is a token at a fraction of an alpha; `color-mix` keeps it a token, so it
  follows a palette change and both colour schemes without a `.dark` variant. It rides
  on the leftmost `<td>` of **every** row in a run, so consecutive rows draw one
  unbroken edge under `border-collapse`.
- **Chrome headless writes screenshots to files; the puppeteer MCP tool does not.**
  `"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless
  --hide-scrollbars --window-size=W,H --screenshot=path URL` is how the PNGs in this
  handover were made, with the MCP browser used for measuring computed styles.
- **`topcoat dev` does not always pick up a rebuild**, and its symptom is silent: it
  keeps printing `no changes; application up to date` while serving the previous binary.
  A `pkill -f "topcoat dev"`, wait for the port to free, then restart is the fix. Two
  restarts in a row failed with `AddrInUse` because the old process had not yet exited.

## For `current.md` — holes opened, shortcuts taken

- **`/{handle}/repos/{name}/commits/{sha}` is linked and does not exist on this branch.**
  Every sha in the blame's commit column points at it. This is the URL contract with the
  `commit-page` agent; if that branch does not land, every blame row has a dead link and
  the fix is one function (`commit_url` in `web/blame.rs`).
- **Line-number links point at `blob#L<n>`.** The anchors are another agent's work. Until
  that lands the link navigates to the blob and does not scroll.
- **The blame page has no revision switcher**, deliberately: it would cost a whole `git`
  process (`list_refs`, ~14ms) and would need a `Switch::Blame` variant in
  `web/browse.rs` — a shared file five agents were editing. Reconsider once the branches
  are merged; a `Switch::Blame(&path)` arm is about four lines.
- **`BlameCommit::boundary` is parsed and tested but only reaches the tooltip.** A
  boundary commit is where git stopped walking, so its lines may be older than it is.
  There is nowhere to say that on a row that must stay one line tall. If it matters
  later, the place for it is a marker beside the sha.
- **`BlameCommit::author_name` is shown only in the tooltip** for the same reason. The
  row is sha, summary, date; the author did not fit, and on a personal instance every
  commit has the same author anyway. On a multi-user instance this is the first thing to
  revisit.
- **The blame page makes 4 `git` processes** for a text file: `cat-file --batch-check` +
  `cat-file blob` for the blob read, then `cat-file --batch-check` + `git blame` for the
  blame. Two of those are the same existence question asked twice — once by the use case
  through `read_blob`, once inside the adapter's `blame` because the port must be
  self-contained. **A kept-alive `cat-file --batch` collapses both**, which is the same
  upgrade 0006 defers for the per-file last-commit column. Until then it is the price of
  the port not assuming its caller checked.
- **Blame is the most expensive read in `GitQuery`** and is the read most likely to meet
  the 20s timeout. Nothing but this page calls it.

## For `ui.md`

The Blame row in "Where the next features go" is done; it can move out of that table
into the page's own description. Suggested text for **The repository page**:

- **A file has two views and one header.** `Code · Blame · Raw` sits where Raw alone
  used to, with the active view in the primary colour — the same rule the tab strip
  follows, so the primary colour still means position and nothing else. Raw is never
  "active": it downloads rather than displays.
- **Blame's rows are the blob's rows.** Same type, same leading, same height, so the two
  views of a file are two readings of one thing rather than two pages. The commit is
  shown once per run of lines and is held to a single line to keep that true.
- **The age tint is texture, not a heat map.** A hairline down the left of each run,
  five steps of the primary colour at 5–30% alpha, scaled between the file's own oldest
  and newest commit. It should tell you which end of a file is moving and nothing more
  precise; if it ever reads as a value to look up, it is too strong.
- The page's cost: **4 `git` processes**, up from the blob page's 4 (3 + the switcher).

## Verified

- `cargo test` — **494 passed, 0 failed** (21 of them new). `cargo clippy --all-targets`
  — zero warnings. `cargo fmt` clean.
- Run on port 3105 from this worktree against a claimed instance holding this
  repository's own history, `v0.1.0` and `v0.2.0` annotated tags, and a `fixtures`
  branch carrying a 1.7 MB text file, a binary file and a one-commit file.
- Looked at in a browser in **both colour modes** and at **800px**: no horizontal page
  overflow (`document.scrollWidth == innerWidth`), the table scrolls inside its own
  container, and the commit column narrows to `w-56` below `lg` so the code keeps the
  room.
- Every state screenshotted and looked at: many commits, one commit, a spread of tint
  steps, a renamed file (the `↳` marker beside the sha, verified end to end against a
  real `git mv`), an empty file, binary, too large, timeout, the blob header's new
  toggle, and 800px.

## Not verified

- **The commit link's destination**, because the page does not exist on this branch.
- **A real timeout.** The state was provoked by lowering `GIT_TIMEOUT` to 1ns, not by a
  repository large enough to take 20 seconds.
- **A file with a run of lines carried through a rename in *this repository's* own
  history.** The marker was verified against a `git mv` pushed to the fixture branch, and
  the parse against captured porcelain; no existing Steid source file has been renamed.
