A Forced Poll replaces the Cover; an ordinary pass still only fills a blank one #153

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

Parent

Spec #135.

What to build

Asking for a Forced Poll is asking to accept the Series' page as it now stands, so a forced pass takes the replace path and an ordinary pass keeps filling only a blank Cover. That is the whole control: no dedicated Cover-refetch button, no second column, no second verb.

Rejected: a force_cover_at column — two columns, two verbs, two controls and another field on the admin projection, for a rare action. Rejected: a synchronous fetch inside the admin request — it contradicts the standing decision that intervention reaches the poller through the database, and it would block an owner request on the browser sidecar.

The pass already holds the Cover it read when it decides whether to fill a blank one, so a forced pass costs no extra page read. Accepted consequence, stated rather than hidden: the owner cannot refresh a Cover without also re-reading chapters. The two are one act.

Identical artwork is an honest no-op the caller can distinguish, because the address now comes from the bytes — the owner learns the Site has not re-arted, rather than watching a control do nothing.

A Series whose Cover file went missing is repaired by one Check now — same bytes means the same address and the file re-linked, new bytes means a new address and the row repointed. The replace path has no empty-address predicate, which is exactly what makes this true.

Fetch path: the existing one, with its existing fetcher routing, so the gated cover host and the browser-only Sites keep going through the sidecar and everything else goes over plain TLS — one routing rule for Acquisition, Poll and this. No pre-flight refusal when the sidecar is down: the Lane skips its pass, the request marker ages, and an ageing marker is already the evidence that a Lane is stuck. No new failure mode and no new notification.

No confirmation, no danger accent, never ember. A Cover that disagrees with the Site is the state that confuses the Reader; the change is the remedy, not the risk, and it pulls no Series out of a list.

Acceptance criteria

  • A forced pass replaces an existing Cover; an unforced pass leaves it alone
  • A forced pass over identical bytes is a no-op the caller can distinguish from a replacement
  • A forced pass on a Series whose Cover file was unlinked re-links the file or repoints the row
  • The Cover fetch routes through the same fetcher selection as the page read
  • With the sidecar unreachable, the pass degrades exactly as it already does — skipped and logged, no new surface
  • No new column, no new control, no confirmation step

Blocked by

  • #150 — needs the byte-derived addresses and the replacing store method.
## Parent Spec #135. ## What to build Asking for a Forced Poll *is* asking to accept the Series' page as it now stands, so a forced pass takes the replace path and an ordinary pass keeps filling only a blank Cover. That is the whole control: **no dedicated Cover-refetch button, no second column, no second verb.** **Rejected: a `force_cover_at` column** — two columns, two verbs, two controls and another field on the admin projection, for a rare action. **Rejected: a synchronous fetch inside the admin request** — it contradicts the standing decision that intervention reaches the poller through the database, and it would block an owner request on the browser sidecar. The pass already holds the Cover it read when it decides whether to fill a blank one, so a forced pass costs no extra page read. **Accepted consequence, stated rather than hidden**: the owner cannot refresh a Cover without also re-reading chapters. The two are one act. **Identical artwork is an honest no-op the caller can distinguish**, because the address now comes from the bytes — the owner learns the Site has not re-arted, rather than watching a control do nothing. **A Series whose Cover file went missing is repaired by one *Check now*** — same bytes means the same address and the file re-linked, new bytes means a new address and the row repointed. The replace path has no empty-address predicate, which is exactly what makes this true. **Fetch path: the existing one**, with its existing fetcher routing, so the gated cover host and the browser-only Sites keep going through the sidecar and everything else goes over plain TLS — one routing rule for Acquisition, Poll and this. **No pre-flight refusal when the sidecar is down**: the Lane skips its pass, the request marker ages, and an ageing marker is already the evidence that a Lane is stuck. No new failure mode and no new notification. **No confirmation, no danger accent, never ember.** A Cover that disagrees with the Site is the state that confuses the Reader; the change is the remedy, not the risk, and it pulls no Series out of a list. ## Acceptance criteria - [ ] A forced pass replaces an existing Cover; an unforced pass leaves it alone - [ ] A forced pass over identical bytes is a no-op the caller can distinguish from a replacement - [ ] A forced pass on a Series whose Cover file was unlinked re-links the file or repoints the row - [ ] The Cover fetch routes through the same fetcher selection as the page read - [ ] With the sidecar unreachable, the pass degrades exactly as it already does — skipped and logged, no new surface - [ ] No new column, no new control, no confirmation step ## Blocked by - #150 — needs the byte-derived addresses and the replacing store method.
sulthan added the ready-for-agent label 2026-08-22 08:38:29 +07:00
sulthan self-assigned this 2026-08-22 09:09:55 +07:00
Author
Owner

Landed on spec-135 as a --no-ff merge of ticket/153-forced-poll-replaces-cover (7e1cbdd, 35a86f5).

Implemented in internal/latest/poller.go only: checkOne dispatches on the store.Series.Forced flag the pass already carries - a Forced Poll calls the new replaceCover, an ordinary pass keeps calling fillBlankCover, whose ADR-0007 blank-only guard and doc comment are byte-for-byte the base's. replaceCover is that function's sibling minus the guard, sharing the same coverWG backgrounding and the same storeCover fetch, so the fetcher selection stays at exactly one call site. storeCover calls ReplaceSeriesCover on a forced pass and SetSeriesCover otherwise, holding previous in one named local at the single replace call site and logging the three outcomes distinctly: filled a blank, unchanged because the Site re-serves the same bytes, or replaced old -> new. Refreshing the Cover and re-reading the chapters are one act - there is no Cover-only refetch, and that consequence is stated in the doc comment rather than worked around.

Nothing was reclaimed and no TODO was left: the stranded address is #154's line next wave.

No new column, no new route, no confirmation step, no pre-flight refusal, no new failure surface, and no web or template change at all. The sidecar-down path is exactly as it was - skipped and logged. The fetch gate at the file's tail belongs to #151 and was not touched.

Four tests appended to poller_test.go: forced replaces while unforced does not; identical bytes log the no-op and never the replacement; a forced pass re-links a cover file unlinked out from under a filled row (re-link, not repoint); and a forced pass with no cover fetcher still records the chapter and leaves the existing Cover alone. The pre-existing no-overwrite guard test and all four sidecar-down regression tests are unchanged and green.

The full backend suite is green on the merged base: 571 tests, 10 packages.

No concerns handed back.

Landed on `spec-135` as a `--no-ff` merge of `ticket/153-forced-poll-replaces-cover` (7e1cbdd, 35a86f5). Implemented in `internal/latest/poller.go` only: `checkOne` dispatches on the `store.Series.Forced` flag the pass already carries - a Forced Poll calls the new `replaceCover`, an ordinary pass keeps calling `fillBlankCover`, whose ADR-0007 blank-only guard and doc comment are byte-for-byte the base's. `replaceCover` is that function's sibling minus the guard, sharing the same `coverWG` backgrounding and the same `storeCover` fetch, so the fetcher selection stays at exactly one call site. `storeCover` calls `ReplaceSeriesCover` on a forced pass and `SetSeriesCover` otherwise, holding `previous` in one named local at the single replace call site and logging the three outcomes distinctly: filled a blank, unchanged because the Site re-serves the same bytes, or replaced old -> new. Refreshing the Cover and re-reading the chapters are one act - there is no Cover-only refetch, and that consequence is stated in the doc comment rather than worked around. Nothing was reclaimed and no TODO was left: the stranded address is #154's line next wave. No new column, no new route, no confirmation step, no pre-flight refusal, no new failure surface, and no web or template change at all. The sidecar-down path is exactly as it was - skipped and logged. The fetch gate at the file's tail belongs to #151 and was not touched. Four tests appended to `poller_test.go`: forced replaces while unforced does not; identical bytes log the no-op and never the replacement; a forced pass re-links a cover file unlinked out from under a filled row (re-link, not repoint); and a forced pass with no cover fetcher still records the chapter and leaves the existing Cover alone. The pre-existing no-overwrite guard test and all four sidecar-down regression tests are unchanged and green. The full backend suite is green on the merged base: 571 tests, 10 packages. No concerns handed back.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#153