Completion-marker hint: storage, presentation, and false-positive containment #130
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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_atis owner-written only, and no adapter, Readeror 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:
series(observed value plus when), nothing at all (re-derived oneach read), or a value only #117's Lane Pass log carries.
latest_checked_atis stampedbefore 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.
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?
from completed. If they are not distinguishable on some Site, that Site probably contributes
no hint rather than a weak one.
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.
finished_atby hand, so the hintat 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.
readSeriesPagealready returns. Every Site's adapter reading a new field is per-Site workagainst markup nobody controls, and #129's stability findings decide whether that is worth
paying for on all six or only some.
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 toseries. The two writes share the position incheckOne(after the read, sincelatest_checked_atis 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_foundsplit out oferrors), and arefusedorunreachablePoll 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.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 readthat 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). #129proved the six vocabularies are not comparable — demonicscans and novelfull are binary and cannot
express hiatus at all, asurascans'
droppedand kagane'supload_statusare scanlation-editorialrather 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:
status == "completed"(itsdroppedis editorial:demon-kingstops at ch. 14 while the work continues)Completedstatus == "finished"(on_hiatus,discontinued,not_yet_releasedare distinct)publication_status == "Completed"only —upload_statusis the release's state, and the two diverge ("'Cause Calypso Can": publication Ongoing, upload Hiatus)CompletedcreativeWorkStatus == CompletedActionStatusA 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-datadetail entry, kagane's API body, lnw's head JSON-LD); demonicscans (infoblock
<li>pair) and novelfull (<a href="/status/…">) each need one new selector against markupnobody 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
Completedis the only signal theSite 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
checkOneafter the read — #127 established that itspoll_failureswrite shares only the position, so the cost cannot hide there — andcheckOne'sSetLatestChapteris skipped on the unchanged-number path (poller.go:474), so nothing existing cancarry it.
site_completed_atjoins the due-query projection (store.Series, already read byDueForLatestCheck), and the statement fires only when the answer would change. The ordinaryPoll 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 tailbeside #124's
finished; one figure in the landing stats block linking to it, printing an unlinkeddigit 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 itsown 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 hygienefilters 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_atfreezes at whatever the last Pollsaw — 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:
is asleep or unreachable — the same degradation their Covers already have, and no new notification.
docs/adrstops 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 columnnor #124's
finished_atis in the code yet; the term lands with the implementation, not with thespec.