Cover addresses derived from the bytes, and a store surface that replaces a Cover #150

Closed
opened 2026-08-22 08:37:45 +07:00 by sulthan · 1 comment
Owner

Parent

Spec #135.

What to build

A Cover address is currently the hash of the source URL, and the store only ever fills a blank one. So a Site that re-arts a Series behind the same URL is invisible twice over: the file is not written, the row is not updated, and every Reader holds the old bytes for a week under an immutable cache header. Nothing in the system can tell.

This ticket changes the derivation and adds the surface that uses it. New Cover writes are addressed by the SHA-256 of the bytes. That is what makes a re-art visible at all — the address itself changes, so the wire URL changes, so the Reader's cache is bypassed without touching a cache header — and it makes "the Site is serving the same artwork" an honest, detectable no-op instead of a silent double failure.

Legacy rows are not rehashed, ever. Existing URL-derived addresses stay as they are; the two derivations coexist, and a legacy row heals the first time its Cover is replaced.

Alongside the existing fill-if-blank setter, add a replacing store method taking the same arguments. Both go through the same cover installer and differ only in the update predicate; the existing method keeps its empty-address guard and the existing "a Cover is not overwritten" test must keep passing unchanged — it is the guard proving this change did not leak into the routine path. Rejected: a force bool parameter — three existing callers would pass false for ever, and the first caller passing true moves the fill-versus-overwrite decision out of the store and into a call site. The forced update writes the source URL as well as the address, because a re-art may sit behind a new URL.

Write the ADR with this implementation. The rule is hard to reverse once data exists under both derivations, and it contradicts the URL-hash statements in the cover-address migration and in the store's own doc comment, which this change must rewrite.

Acceptance criteria

  • The same bytes yield the same address; different bytes yield a different one
  • The replacing method overwrites a non-empty cover address and writes the source URL too
  • The existing fill-if-blank setter still refuses to overwrite, with its existing test unchanged and green
  • Legacy URL-derived addresses keep resolving; nothing rehashes them
  • The cover-address migration comment and the store doc comment no longer claim addresses are URL-derived
  • An ADR records the byte-derived rule, what it costs, and why it is hard to reverse

Blocked by

  • None — can start immediately.
## Parent Spec #135. ## What to build A Cover address is currently the hash of the *source URL*, and the store only ever fills a blank one. So a Site that re-arts a Series behind the same URL is invisible twice over: the file is not written, the row is not updated, and every Reader holds the old bytes for a week under an immutable cache header. Nothing in the system can tell. This ticket changes the derivation and adds the surface that uses it. **New Cover writes are addressed by the SHA-256 of the bytes.** That is what makes a re-art visible at all — the address itself changes, so the wire URL changes, so the Reader's cache is bypassed without touching a cache header — and it makes "the Site is serving the same artwork" an honest, detectable no-op instead of a silent double failure. **Legacy rows are not rehashed, ever.** Existing URL-derived addresses stay as they are; the two derivations coexist, and a legacy row heals the first time its Cover is replaced. Alongside the existing fill-if-blank setter, add a replacing store method taking the same arguments. Both go through the same cover installer and differ **only in the update predicate**; the existing method keeps its empty-address guard and the existing "a Cover is not overwritten" test must keep passing unchanged — it is the guard proving this change did not leak into the routine path. **Rejected: a `force bool` parameter** — three existing callers would pass `false` for ever, and the first caller passing `true` moves the fill-versus-overwrite decision out of the store and into a call site. The forced update writes the source URL as well as the address, because a re-art may sit behind a new URL. **Write the ADR with this implementation.** The rule is hard to reverse once data exists under both derivations, and it contradicts the URL-hash statements in the cover-address migration and in the store's own doc comment, which this change must rewrite. ## Acceptance criteria - [ ] The same bytes yield the same address; different bytes yield a different one - [ ] The replacing method overwrites a non-empty cover address and writes the source URL too - [ ] The existing fill-if-blank setter still refuses to overwrite, with its existing test unchanged and green - [ ] Legacy URL-derived addresses keep resolving; nothing rehashes them - [ ] The cover-address migration comment and the store doc comment no longer claim addresses are URL-derived - [ ] An ADR records the byte-derived rule, what it costs, and why it is hard to reverse ## Blocked by - None — can start immediately.
sulthan added the ready-for-agent label 2026-08-22 08:37:45 +07:00
sulthan self-assigned this 2026-08-22 08:55:03 +07:00
Author
Owner

Landed on spec-135 as merge commit for ticket/150-cover-bytes-addressing (b9fc832).

Implemented: putCover now addresses by the SHA-256 of the bytes and returns that address; SetSeriesCover keeps its cover_address = '' predicate and is otherwise unchanged; ReplaceSeriesCover(site, seriesID, sourceURL, body, contentType) (previous, current string, err error) differs from it only in that predicate and writes the source URL too; exported CoverAddress(sourceURL) is gone, replaced by CoverAddressForBytes(body); unexported coverSourceAddress stays so legacy URL-derived rows keep resolving through GetCover and nothing rehashes them; the 0009_series_cover_address.sql comment and the store doc comments no longer claim URL derivation; ADR-0014 records the rule, its cost and why it is hard to reverse.

The coverAddressRe path-traversal guard is byte-identical (^[0-9a-f]{64}$) — a byte-derived address has the same shape, so it was not widened.

One noted deviation: the pre-existing "a Cover is not overwritten" test could not stay byte-for-byte identical, because deleting the exported CoverAddress forces one identifier swap in it. The guard it proves is unchanged and the assertion is the same; the two instructions in the brief were literally incompatible. Judged non-blocking.

Superseded cover bytes are not reclaimed here — that is #154, and it is documented as out of scope in ADR-0014.

Backend suite green on the merged base: 562 tests, 10 packages.

Landed on `spec-135` as merge commit for `ticket/150-cover-bytes-addressing` (b9fc832). Implemented: `putCover` now addresses by the SHA-256 of the bytes and returns that address; `SetSeriesCover` keeps its `cover_address = ''` predicate and is otherwise unchanged; `ReplaceSeriesCover(site, seriesID, sourceURL, body, contentType) (previous, current string, err error)` differs from it only in that predicate and writes the source URL too; exported `CoverAddress(sourceURL)` is gone, replaced by `CoverAddressForBytes(body)`; unexported `coverSourceAddress` stays so legacy URL-derived rows keep resolving through `GetCover` and nothing rehashes them; the `0009_series_cover_address.sql` comment and the store doc comments no longer claim URL derivation; ADR-0014 records the rule, its cost and why it is hard to reverse. The `coverAddressRe` path-traversal guard is byte-identical (`^[0-9a-f]{64}$`) — a byte-derived address has the same shape, so it was not widened. One noted deviation: the pre-existing "a Cover is not overwritten" test could not stay byte-for-byte identical, because deleting the exported `CoverAddress` forces one identifier swap in it. The guard it proves is unchanged and the assertion is the same; the two instructions in the brief were literally incompatible. Judged non-blocking. Superseded cover bytes are not reclaimed here — that is #154, and it is documented as out of scope in ADR-0014. Backend suite green on the merged base: 562 tests, 10 packages.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#150