# 33. Who made a page, and where a text revision came from, are stored Date: 2026-10-26 ## Status Accepted ## Context scrap never stored who made a page. Each feature worked it out from the page's captures and the topic table (#280): Written by Claude from the first capture's source, `is:claude` from any capture's, a `scrap gc` review page from a `gc` capture. Step 2 of #280 gathered those tests into `store/roles.py`; they gave the same answer for every page, but by different routes, and each read the capture table. A text revision's author was also its source (#153). MCP writes as `claude`, and an author of `claude` was what made a revision Claude's: `is:claude-edited`, and whether an MCP edit may fold into the revision before it. A web edit's author is the identity header's value, so a request naming `claude` in that header made a web edit look like Claude's. ## Decision - `page.made_by` is `human`, `claude`, `topic` or `gc` (a `CHECK`). It is set once, when the page is made, from how it is made: an MCP `write` is `claude`, `scrap gc`'s review page `gc`, a topic page `topic`; a PWA or share capture, an import and the Pinned page `human`. A trigger refuses any later change, so editing, re-kinding, adding photos, appending and the trash never touch it. - `is:claude`, MCP `read`'s `written_by_claude`, the PWA's Written by Claude badge, `Store.append`'s check and `REVIEW` (gc's review page, wherever health checks, retention and `scrap gc --report` use it) read the column. - The topic test stays the `topic` table, not `made_by`: the table holds the key that keeps Claude from making a topic again, and the topic's sources. Both are written in one transaction and never change, so they agree. - `page_rev.source` is `web` or `mcp`, as `page_meta_rev.source` is. `mcp` is Claude's text: MCP's edits, reverts and appends, and the task boxes Claude adds for photo ingest and enrich. Every other revision is `web`, including scrap's own (an import, `scrap gc`, a new topic page). `is:claude-edited`, folding a burst (same author *and* source) and putting back the kind of a todo whose boxes Claude took off read it, not the author. - The identity header's value `claude` (in any case, spaces aside) counts as no header: the author is `tagged`. No request header can name MCP's author. - Migration 16 adds both columns. `made_by` is backfilled with what `roles` said before (`INFERRED_MADE_BY`: a topic, else a `gc` capture, else a first capture from MCP, else `human`); `source` is `mcp` where the author is `claude`, as #153 asked. On the live data (376 pages, 2026-10-26) the backfill gives `claude` to 23 of the 292 pages out of the trash and 26 of the 84 in it: in each, exactly the pages the old `is:claude` found. - The Python helpers (`roles.made_by`, `edited_by_claude`, so `Store.page`) still answer on a database not yet migrated, as before. The SQL fragments read the columns, so `scrap gc`, which uses `REVIEW`, expects the database the service has already migrated (the service migrates at start, before the nightly gc runs). ## Consequences - Who made a page is one column read, and no longer depends on the captures a page has later. - The task boxes Claude adds for ingest and enrich are `mcp` too, so the backfill and new revisions agree, and `is:claude-edited` finds both: every text change Claude made, over MCP or not. That is the owner's decision; telling them apart would need a third source. - Behind an authenticating proxy the identity header must still be set (`identityHeader`, `SCRAP_IDENTITY_HEADER`), or every web change is `tagged`. - An older scrap cannot run on a migrated database even with `user_version` set back: it writes text revisions without naming their columns, which the extra `page_rev` column refuses. Going back past migration 16 needs the pre-upgrade backup, as the upgrade guide says. - `made_by` is part of the baseline at the 1.0 squash (#267).