C Attachments,\swritten\swhere\sthey\sare\sread D 2026-09-11T20:39:24.343 E 2026-09-11T15:40:00 b7190dfc66f324281aae7254e391644fb2452be7 N text/x-markdown U dmestas W 4666 # Attachments, written where they are read zit has been able to *read* an attachment for a long time. The A card folds, `zit artifact` describes it, a clone carries it, and a fossil-hosted project's screenshots and patches come across a sync intact. What zit could not do was write one — and every other thing quarry authors goes through a zit verb. `ticket new`, `wiki commit`, `forum reply`, `technote add`: the artifact format is written in one place, by one writer, and read by fossil on the other side. Attachments were the exception, and an exception in a format writer is where the second implementation starts. 0.3.13 closes it. ## The verbs ``` zit attach add TARGET FILE [--name NAME] [--comment TEXT] [--mimetype TYPE] [--user NAME] [-R repo] zit attach rm TARGET NAME [--user NAME] [-R repo] zit attach list [TARGET] [-R repo] ``` `add` stores the file as a blob, then writes the attachment artifact — one `A NAME TARGET SOURCE` card, a comment if you gave one, the writer clock, a mimetype if you gave one, the author, the checksum — and prints the artifact's hash and the blob's on two lines. The two writes happen in **one commit**, which is the detail worth noticing: there is no moment at which the store holds an attachment naming bytes it does not have, and no crash that leaves one behind. `TARGET` is what fossil accepts: a ticket, technote, or forum post's hash, or a wiki page's name. It is checked for shape and nothing more, because that is what the format says — an unresolved target is recorded as a generic reference, and a verb that refused to attach to a ticket it could not find would be inventing a rule fossil does not have. `rm` writes an A card with no source. Nothing leaves the store; the association ends under the format's latest-wins rule, and `list` — which folds the newest card per name — stops showing it. `list` prints `NAME SIZE MIMETYPE BLOB DATE USER`, with the target as a first column when you ask for the whole repository, which is what an export or an audit walks. ## The cap, and why it is lower than fossil's Fossil has a setting called `attachment-size-limit`: a byte count checked before the blob is stored, `0` meaning unlimited, and unlimited is its default. zit honours the same setting the same way, with one deliberate difference — **unset, the limit is 1 MiB.** A blob in the DAG travels to every clone, for ever. Fossil's default is tuned for a project whose attachments are screenshots and patches, uploaded by people who know what a repository is. quarry is a forge, and a forge's first-time user will drag a 400 MB screen recording onto a ticket. The right default is the one that says no to that, names the size and the limit, and tells you how to lift it: `zit settings attachment-size-limit 0`. The setting lives in the repository's config frame under a new key. A zit that predates it reads the repository without complaint and simply sees the setting as unset. ## What quarry attaches instead quarry will never write a file's bytes into a hosted repository through this verb. It attaches a **stub** — a small text blob that says where the bytes are: ``` quarry-attachment 1 sha256 4f9a…e01c size 18349022 type video/mp4 name screen-recording.mp4 ``` No URL. The bytes are found by content hash in whatever storage the repository's steward bound in quarry; the A card stays a valid fossil artifact; the store stays small; nothing is a phantom on sync. zit now recognises the shape: `attach list` shows such an attachment as `external:sha256:…` with the size and type it points at, and `zit artifact` on the blob says `attachment-stub`. A client that does not recognise it sees a small text file under the attachment's name — degraded, but honest. The size cap applies to what is actually stored, so a stub always passes. Nothing on the write side changed; quarry writes the stub through the same `attach add`, from stdin. ## Proven against fossil The claim that matters is not that zit writes an A card, but that fossil reads it as an attachment. So the smoke suite now authors one with zit, pushes it to a real `fossil server`, and asks fossil's own `attachment` table: one live row for the target and name zit recorded, with zit's blob as its source, and the timeline entry fossil renders — *Add attachment [note.txt] to wiki page [Home]*. That check runs on every qualification. The first run of it failed, which is worth saying because of *how*: the row was there, exactly right, and my query had forgotten to name the repository. The interop was never in doubt once the data was looked at. It was the harness that needed the fix. Z e84fcafc1c5df8d19582c56ab21a6305