Completion-marker hint: storage, presentation, and false-positive containment #130

Closed
opened 2026-08-19 19:01:32 +07:00 by sulthan · 2 comments
Owner

Part of #114
Blocked by: #129

Question

How does a Site's own completion marker reach the owner as a suggestion, without ever
becoming a decision?

#124 fixed the boundary: series.finished_at is owner-written only, and no adapter, Reader
or Poll may write it. The owner still wants the dashboard to say "this one looks finished",
and was explicit that a false positive is the failure mode to design against — a Series
wrongly hinted is noise, but a hint that gets acted on wrongly silences a live Series.
#129 establishes which Sites publish a signal at all and how ambiguous each one is.

To decide:

  • Storage. A column on series (observed value plus when), nothing at all (re-derived on
    each read), or a value only #117's Lane Pass log carries. latest_checked_at is stamped
    before the fetch, so the hint cannot ride along with it — a second write per Poll is on the
    table, and #127 faces the same choice for its outcome columns. Whether the two share one
    write is a real question, not a micro-optimisation.
  • Decay. A marker that disappears again (a "completed" Series that resumes, a scrape that
    broke) must not leave a stale hint sitting on the row for ever. Does the hint expire, get
    overwritten on every Poll, or require N consecutive observations before it shows at all?
  • Which markers count. #129 will report whether hiatus and dropped are distinguishable
    from completed. If they are not distinguishable on some Site, that Site probably contributes
    no hint rather than a weak one.
  • Presentation. A row field, a stats-block figure, or the eleventh filter name — noting
    that #118 closed its vocabulary on the argument that every hygiene figure must be
    individually actionable, #127 owns the ninth (failing) and #124 already took the tenth
    (finished). Whether a hint sits on the Series detail page beside the Finish action (where
    #124 put the only control) or on the list is part of this.
  • What acting on it looks like. The owner still writes finished_at by hand, so the hint
    at most pre-fills or shortens that path. Whether a hinted Series' Finish action changes at
    all — and whether dismissing a hint is itself a stored decision — is open.
  • Whether the adapters extract this at all, or whether the hint is derived from what
    readSeriesPage already returns. Every Site's adapter reading a new field is per-Site work
    against markup nobody controls, and #129's stability findings decide whether that is worth
    paying for on all six or only some.
Part of #114 Blocked by: #129 ## Question How does a Site's own completion marker reach the owner as a suggestion, without ever becoming a decision? #124 fixed the boundary: `series.finished_at` is owner-written only, and no adapter, Reader or Poll may write it. The owner still wants the dashboard to say "this one looks finished", and was explicit that **a false positive is the failure mode to design against** — a Series wrongly hinted is noise, but a hint that gets acted on wrongly silences a live Series. #129 establishes which Sites publish a signal at all and how ambiguous each one is. To decide: - **Storage.** A column on `series` (observed value plus when), nothing at all (re-derived on each read), or a value only #117's Lane Pass log carries. `latest_checked_at` is stamped before the fetch, so the hint cannot ride along with it — a second write per Poll is on the table, and #127 faces the same choice for its outcome columns. Whether the two share one write is a real question, not a micro-optimisation. - **Decay.** A marker that disappears again (a "completed" Series that resumes, a scrape that broke) must not leave a stale hint sitting on the row for ever. Does the hint expire, get overwritten on every Poll, or require N consecutive observations before it shows at all? - **Which markers count.** #129 will report whether hiatus and dropped are distinguishable from completed. If they are not distinguishable on some Site, that Site probably contributes no hint rather than a weak one. - **Presentation.** A row field, a stats-block figure, or the eleventh filter name — noting that #118 closed its vocabulary on the argument that every hygiene figure must be individually actionable, #127 owns the ninth (`failing`) and #124 already took the tenth (`finished`). Whether a hint sits on the Series detail page beside the Finish action (where #124 put the only control) or on the list is part of this. - **What acting on it looks like.** The owner still writes `finished_at` by hand, so the hint at most pre-fills or shortens that path. Whether a hinted Series' Finish action changes at all — and whether dismissing a hint is itself a stored decision — is open. - Whether the adapters extract this at all, or whether the hint is derived from what `readSeriesPage` already returns. Every Site's adapter reading a new field is per-Site work against markup nobody controls, and #129's stability findings decide whether that is worth paying for on all six or only some.
sulthan added the wayfinder:grilling label 2026-08-19 19:01:32 +07:00
Author
Owner

Constraint from #127 (closed):

The one-write-for-both merge flagged in #124 is not available. #127's post-read write targets a new table, poll_failures, and only when a Poll failed: an insert keyed on the failure, or a delete when the read succeeded. A completion-marker hint is a fact learned from a successful read and belongs to series. The two writes share the position in checkOne (after the read, since latest_checked_at is stamped before it) and nothing else — so a hint needs its own statement, and its cost cannot be hidden inside #127's.

Also settled there, and relevant to containment: the outcome vocabulary now has six words (not_found split out of errors), and a refused or unreachable Poll writes no per-Series state at all, on the ground that neither is evidence about the Series. If a hint is only extractable from a page the Poll can read, the same rule applies to it for free.

Constraint from #127 (closed): **The one-write-for-both merge flagged in #124 is not available.** #127's post-read write targets a new table, `poll_failures`, and only when a Poll *failed*: an insert keyed on the failure, or a delete when the read succeeded. A completion-marker hint is a fact learned from a **successful** read and belongs to `series`. The two writes share the position in `checkOne` (after the read, since `latest_checked_at` is stamped before it) and nothing else — so a hint needs its own statement, and its cost cannot be hidden inside #127's. Also settled there, and relevant to containment: the outcome vocabulary now has six words (`not_found` split out of `errors`), and a `refused` or `unreachable` Poll writes **no** per-Series state at all, on the ground that neither is evidence about the Series. If a hint is only extractable from a page the Poll can read, the same rule applies to it for free.
sulthan self-assigned this 2026-08-20 21:21:31 +07:00
Author
Owner

Answer

The false-positive containment this ticket was named for is not built, because #129 removed the
threat.
If the Poll reads only each Site's completed value, no Site has a false-positive path; the
residual error is a false negative (a finished Series that gets no hint), and we accept it. So: no
N-consecutive-observation gate, no expiry, no confirmation counter, no suspicion damping of any kind.

Storage — one column. series.site_completed_at bigint NOT NULL DEFAULT 0, epoch-ms of the read
that saw the Site's completed value; zero means the last successful read did not see it. Decay is
therefore free: every successful read rewrites the answer, so a stale hint cannot survive one Poll.
The timestamp is the age the hint renders with, not a history — same role as failing_since.

Rejected: a text column holding a normalised word (completed/ongoing/hiatus/dropped). #129
proved the six vocabularies are not comparable — demonicscans and novelfull are binary and cannot
express hiatus at all, asurascans' dropped and kagane's upload_status are scanlation-editorial
rather than completion state, and only kagane separates the work's status from the translation's. A
shared word would be a different fact per Site, and nothing on this map acts on any value but
completed. Also rejected: storing nothing and re-deriving on read — the marker only exists in a
fetched page, and the admin surface never fetches (every intervention on this map goes through the
database).

Which value counts — the completed value only, one predicate per Site, no mapping table:

Site Reads
asurascans status == "completed" (its dropped is editorial: demon-king stops at ch. 14 while the work continues)
demonicscans Completed
comix status == "finished" (on_hiatus, discontinued, not_yet_released are distinct)
kagane publication_status == "Completed" only — upload_status is the release's state, and the two diverge ("'Cause Calypso Can": publication Ongoing, upload Hiatus)
novelfull Completed
lightnovelworld creativeWorkStatus == CompletedActionStatus

A failed extraction is not-completed, never unknown-as-suspicion. And per #127's rule, inherited for
free: a refused or unreachable Poll makes no statement at all — the column keeps its previous
value rather than zeroing, because a challenge is not evidence about the Series.

All six adapters extract it. Four ride a payload the adapter already parses (asura's astro-island
props, comix's initial-data detail entry, kagane's API body, lnw's head JSON-LD); demonicscans (info
block <li> pair) and novelfull (<a href="/status/…">) each need one new selector against markup
nobody controls. Paid anyway, on two grounds: a broken selector degrades to a missing hint, which is
the error class we already accept; and on exactly those two Sites Completed is the only signal the
Site can ever emit, so omitting them makes an absent hint unreadable — the owner could not tell "not
finished" from "we never look".

The write. Its own statement in checkOne after the read — #127 established that its
poll_failures write shares only the position, so the cost cannot hide there — and checkOne's
SetLatestChapter is skipped on the unchanged-number path (poller.go:474), so nothing existing can
carry it. site_completed_at joins the due-query projection (store.Series, already read by
DueForLatestCheck), and the statement fires only when the answer would change. The ordinary
Poll of an ongoing Series writes nothing, exactly as a healthy Poll writes nothing in #127.

Presentation. A twelfth filter name, site-completed, ordered with the informational tail
beside #124's finished; one figure in the landing stats block linking to it, printing an unlinked
digit at zero per #118; and one line beside the Finish control on the detail page. No field on the
list row
— #122 gives a row two lines, and this fact is true of few Series, so a per-row field
spends space on every row for a rare signal. The filter is the work list; the figure is how the owner
sees the work without opening it.

The Finish control does not change. Still one control, still confirm-gated to finish (#124). The
hint is text next to it: it may not pre-fill, add a second button, or remove the confirm step. A hint
that shortens the path is the Site deciding the Lifecycle, which is the exact boundary #124 drew.

No dismissal column. The owner's answers are Finish or ignore. A stored dismissal would be a
third opinion on the row next to the Site's marker and the owner's finished_at, and would need its
own rule for the day the Site's marker changes again. An ignored Series stays in a 50-row filter;
if that noise turns out to be real, the column can be added then.

After a finish. The filter carries AND finished_at = 0, the same guard the other hygiene
filters gained in #124, and the detail page hides the hint on a finished Series. #124 removes a
finished Series from the Poll query, so its site_completed_at freezes at whatever the last Poll
saw — old and unrefreshable, therefore not work. Un-finishing puts the Series back in the Poll query
and the next Poll writes a fresh answer.

Two consequences worth stating rather than rediscovering:

  • The hint is only as fresh as the last Poll, so comix and kagane hints age while the browser sidecar
    is asleep or unreachable — the same degradation their Covers already have, and no new notification.
  • Paper migration numbering: everything this map has specified (0012–0017) is unbuilt — docs/adr
    stops at 0011 and so do the migrations, including the ADR-0012 #124's resolution named — so this
    column takes 0018 in map order, not in tree order.

No fog graduated and no new ticket: the answer is self-contained, and #131 (Latest Chapter
provenance) is untouched by it.

Nothing added to CONTEXT.md. The glossary describes the system that exists, and neither this column
nor #124's finished_at is in the code yet; the term lands with the implementation, not with the
spec.

## Answer **The false-positive containment this ticket was named for is not built, because #129 removed the threat.** If the Poll reads only each Site's completed value, no Site has a false-positive path; the residual error is a *false negative* (a finished Series that gets no hint), and we accept it. So: no N-consecutive-observation gate, no expiry, no confirmation counter, no suspicion damping of any kind. **Storage — one column.** `series.site_completed_at bigint NOT NULL DEFAULT 0`, epoch-ms of the read that saw the Site's completed value; zero means the last successful read did not see it. Decay is therefore free: every successful read rewrites the answer, so a stale hint cannot survive one Poll. The timestamp is the age the hint renders with, not a history — same role as `failing_since`. Rejected: a text column holding a normalised word (`completed`/`ongoing`/`hiatus`/`dropped`). #129 proved the six vocabularies are not comparable — demonicscans and novelfull are binary and cannot express hiatus at all, asurascans' `dropped` and kagane's `upload_status` are scanlation-editorial rather than completion state, and only kagane separates the work's status from the translation's. A shared word would be a different fact per Site, and nothing on this map acts on any value but completed. Also rejected: storing nothing and re-deriving on read — the marker only exists in a fetched page, and the admin surface never fetches (every intervention on this map goes through the database). **Which value counts — the completed value only,** one predicate per Site, no mapping table: | Site | Reads | |---|---| | asurascans | `status == "completed"` (its `dropped` is editorial: `demon-king` stops at ch. 14 while the work continues) | | demonicscans | `Completed` | | comix | `status == "finished"` (`on_hiatus`, `discontinued`, `not_yet_released` are distinct) | | kagane | `publication_status == "Completed"` **only** — `upload_status` is the release's state, and the two diverge ("'Cause Calypso Can": publication Ongoing, upload Hiatus) | | novelfull | `Completed` | | lightnovelworld | `creativeWorkStatus == CompletedActionStatus` | A failed extraction is not-completed, never unknown-as-suspicion. And per #127's rule, inherited for free: a **refused or unreachable Poll makes no statement at all** — the column keeps its previous value rather than zeroing, because a challenge is not evidence about the Series. **All six adapters extract it.** Four ride a payload the adapter already parses (asura's astro-island props, comix's `initial-data` detail entry, kagane's API body, lnw's head JSON-LD); demonicscans (info block `<li>` pair) and novelfull (`<a href="/status/…">`) each need one new selector against markup nobody controls. Paid anyway, on two grounds: a broken selector degrades to a missing hint, which is the error class we already accept; and on exactly those two Sites `Completed` is the only signal the Site can ever emit, so omitting them makes an absent hint unreadable — the owner could not tell "not finished" from "we never look". **The write.** Its own statement in `checkOne` after the read — #127 established that its `poll_failures` write shares only the position, so the cost cannot hide there — and `checkOne`'s `SetLatestChapter` is skipped on the unchanged-number path (`poller.go:474`), so nothing existing can carry it. `site_completed_at` joins the due-query projection (`store.Series`, already read by `DueForLatestCheck`), and the statement fires **only when the answer would change**. The ordinary Poll of an ongoing Series writes nothing, exactly as a healthy Poll writes nothing in #127. **Presentation.** A twelfth filter name, **`site-completed`**, ordered with the informational tail beside #124's `finished`; one figure in the landing stats block linking to it, printing an unlinked digit at zero per #118; and one line beside the Finish control on the detail page. **No field on the list row** — #122 gives a row two lines, and this fact is true of few Series, so a per-row field spends space on every row for a rare signal. The filter is the work list; the figure is how the owner sees the work without opening it. **The Finish control does not change.** Still one control, still confirm-gated to finish (#124). The hint is text next to it: it may not pre-fill, add a second button, or remove the confirm step. A hint that shortens the path is the Site deciding the Lifecycle, which is the exact boundary #124 drew. **No dismissal column.** The owner's answers are Finish or ignore. A stored dismissal would be a third opinion on the row next to the Site's marker and the owner's `finished_at`, and would need its own rule for the day the Site's marker changes again. An ignored Series stays in a 50-row filter; if that noise turns out to be real, the column can be added then. **After a finish.** The filter carries `AND finished_at = 0`, the same guard the other hygiene filters gained in #124, and the detail page hides the hint on a finished Series. #124 removes a finished Series from the Poll query, so its `site_completed_at` freezes at whatever the last Poll saw — old and unrefreshable, therefore not work. Un-finishing puts the Series back in the Poll query and the next Poll writes a fresh answer. Two consequences worth stating rather than rediscovering: - The hint is only as fresh as the last Poll, so comix and kagane hints age while the browser sidecar is asleep or unreachable — the same degradation their Covers already have, and no new notification. - Paper migration numbering: everything this map has specified (0012–0017) is unbuilt — `docs/adr` stops at 0011 and so do the migrations, including the ADR-0012 #124's resolution named — so this column takes **0018** in map order, not in tree order. No fog graduated and no new ticket: the answer is self-contained, and #131 (Latest Chapter provenance) is untouched by it. Nothing added to `CONTEXT.md`. The glossary describes the system that exists, and neither this column nor #124's `finished_at` is in the code yet; the term lands with the implementation, not with the spec.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#130