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_byishuman,claude,topicorgc(aCHECK). It is set once, when the page is made, from how it is made: an MCPwriteisclaude,scrap gc’s review pagegc, a topic pagetopic; a PWA or share capture, an import and the Pinned pagehuman. A trigger refuses any later change, so editing, re-kinding, adding photos, appending and the trash never touch it.is:claude, MCPread’swritten_by_claude, the PWA’s Written by Claude badge,Store.append’s check andREVIEW(gc’s review page, wherever health checks, retention andscrap gc --reportuse it) read the column.The topic test stays the
topictable, notmade_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.sourceiswebormcp, aspage_meta_rev.sourceis.mcpis Claude’s text: MCP’s edits, reverts and appends, and the task boxes Claude adds for photo ingest and enrich. Every other revision isweb, 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 istagged. No request header can name MCP’s author.Migration 16 adds both columns.
made_byis backfilled with whatrolessaid before (INFERRED_MADE_BY: a topic, else agccapture, else a first capture from MCP, elsehuman);sourceismcpwhere the author isclaude, as #153 asked. On the live data (376 pages, 2026-10-26) the backfill givesclaudeto 23 of the 292 pages out of the trash and 26 of the 84 in it: in each, exactly the pages the oldis:claudefound.The Python helpers (
roles.made_by,edited_by_claude, soStore.page) still answer on a database not yet migrated, as before. The SQL fragments read the columns, soscrap gc, which usesREVIEW, 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
mcptoo, so the backfill and new revisions agree, andis:claude-editedfinds 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 istagged.An older scrap cannot run on a migrated database even with
user_versionset back: it writes text revisions without naming their columns, which the extrapage_revcolumn refuses. Going back past migration 16 needs the pre-upgrade backup, as the upgrade guide says.made_byis part of the baseline at the 1.0 squash (#267).