ba679223b2
Closes #103. A userscript PUT already carries the Latest Chapter the Reader's own browser read off the Series page. It may now stand in for a Poll, under one restriction and one ceiling: - **Solitary Series only** — a Series two Readers share is Polled on schedule however recently it was sighted, so one Reader's mistake can never reach another's list. - **One rest of standing**, and a **six-rest ceiling** (`sightingCeilingRests`, counted in the Site's own Rest): however many Sightings arrive, an unpolled Series is Polled. Both live in the due query's HAVING clause (`Store.DueForLatestCheck`) — the same place the schedule has always been decided, so no timer and no second code path can disagree with it. No new query per scheduler round. Judgement costs no extra request. `Poller.checkOne` already compares what the Site publishes against what is stored: a lower number contradicts the Sighting (Reader and both numbers logged), the same number confirms it, a higher number is the Site publishing and clears the attribution instead. Three contradictions stop that Reader deferring — their reports still write the Latest Chapter — and twenty consecutive confirmations forgive them, as does the owner's clear-marks control from #102. One client change was required: both userscripts skipped the PUT when the number had not moved, so the case the whole mechanism exists for — visiting a Series with nothing new — never reached the backend. `reportLatestChapter` sends it, skipping only the local write and the re-render. A numberless PUT (favourite toggle, progress from a chapter page) is no Sighting and defers nothing. Schema: migration `0011_series_sightings.sql` adds `series.latest_sighted_at` and `series.latest_raised_by`. Trust model, thresholds, and rejected alternatives with their citations: `docs/adr/0011-sighting-deferral-trust-model.md`. Reviewed on both axes (spec against #103, standards against the repo's rules); the blocker — attribution surviving a Poll that overtook the report — is fixed and has a test that fails without the fix. Verification: `go test ./...` green (needs Docker), `node --test userscript/test/*.test.js` 66 pass. Reviewed-on: #108 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
140 lines
7.9 KiB
Markdown
140 lines
7.9 KiB
Markdown
# ADR-0011: Sightings — a Reader report defers a Poll where being wrong hurts only them
|
|
|
|
Date: 2026-08-16
|
|
Status: accepted
|
|
|
|
## Decision
|
|
|
|
A **Sighting** is the Latest Chapter the Reader's own browser read off the
|
|
Series page and PUT to the backend. It is now allowed to stand in for a Poll,
|
|
under one restriction and one ceiling:
|
|
|
|
- **Solitary Series only.** A Sighting defers the Poll of a Series exactly one
|
|
Bookmark points at. A Series two Readers share is Polled on schedule no matter
|
|
how recently it was sighted.
|
|
- **One rest of standing.** A Sighting postpones Polls for one Rest
|
|
(`defaultRest`, an hour), not forever: a Series nobody visits again returns to
|
|
the normal schedule by itself.
|
|
- **Six-rest ceiling.** `sightingCeilingRests = 6`, counted in the Site's own
|
|
Rest — six hours everywhere today. However many Sightings arrive, a Series
|
|
unpolled that long is Polled.
|
|
|
|
Both live in the due query's HAVING clause (`store.DueForLatestCheck`), beside
|
|
the Rest cutoff — the same place the schedule has always been decided, so no
|
|
timer and no second code path can disagree with it.
|
|
|
|
Attribution and judgement:
|
|
|
|
- `Store.RecordSighting` runs *before* the Upsert that stores the reported
|
|
value, because the raise test needs the row as it stands. A report that raises
|
|
the stored Latest Chapter names its Reader in `series.latest_raised_by`.
|
|
- The Poll is the oracle. `Poller.checkOne` already compares what the Site
|
|
publishes against what is stored, so judgement costs no extra request: a lower
|
|
number contradicts the Sighting (`sighting_disagreements + 1`, both numbers and
|
|
the Reader logged), the same number confirms it (`sighting_agreements + 1`), a
|
|
**higher** number is the Site publishing and means nothing either way — but it
|
|
does clear the attribution (`Store.ClearSightingAttribution`), because the
|
|
value stored afterwards is the Poll's own and nobody must answer for it.
|
|
- At `SightingDisagreementLimit` (3) that Reader's Sightings stop deferring
|
|
anything. They still write the Latest Chapter — the penalty removes a
|
|
privilege, it does not silence anyone.
|
|
- `SightingAgreementsToClear` (20) consecutive confirmations forgive the
|
|
disagreements. A disagreement resets the run to zero.
|
|
- The owner clears marks from the administration page (issue #102, shipped
|
|
first precisely so a false mark has a remedy the day the mechanism lands).
|
|
|
|
One client change was required, and only one. Both userscripts stopped short of
|
|
PUTting a read whose number had not moved (`applyLatestChapterIfChanged`), so
|
|
the case this whole mechanism exists for — visiting a Series with nothing new —
|
|
never reached the backend. `reportLatestChapter` now sends it, skipping only the
|
|
local write and the re-render. A numberless PUT (favourite toggle, progress from
|
|
a chapter page) is not a Sighting and defers nothing: nobody read the Series
|
|
page, so there would be nothing to judge later.
|
|
|
|
## Why
|
|
|
|
Most of the backend's work was redundant. The userscript reads the Latest
|
|
Chapter on every Series page visit; minutes later the Poll Lane fetches the same
|
|
page for the same number. Deferring on a report converts a visit into a Poll
|
|
saved, which is Lane capacity handed back to Series nobody is
|
|
reading.
|
|
|
|
The restriction is the whole safety argument, and it is about **blast radius**,
|
|
not about trust arithmetic:
|
|
|
|
- On a solitary Series, a wrong report can only mislead the Reader who made it.
|
|
There is nobody else's ember to falsify.
|
|
- On a shared Series it could mislead someone else, so a report never postpones
|
|
anything there.
|
|
|
|
The ceiling bounds the damage in time: a false value dies within six hours
|
|
whatever happens, because the Poll that finds it is guaranteed. That is also
|
|
what makes lying pointless — the six-hour audit is certain, not sampled, so a
|
|
determined attacker buys at most three ceilings' worth of a wrong number on
|
|
their own Series and then loses deferral entirely.
|
|
|
|
The cost of recovery is deliberate. An agreement is only recorded when a later
|
|
Poll confirms a Sighting, so twenty agreements are twenty Polls of Series that
|
|
Reader bookmarks — hours to days of real time, not twenty page views. Waiting is
|
|
therefore not a strategy, and credit cannot be banked in advance.
|
|
|
|
## Tradeoffs and rejections
|
|
|
|
- **Trusting a Sighting on a shared Series** rejected: it is the only case where
|
|
one Reader's mistake reaches another Reader's list, and no amount of
|
|
reputation makes that recoverable within the six-hour window.
|
|
- **Cross-Reader agreement, voting, weighting, consensus scoring** rejected on
|
|
evidence: every truth-discovery method estimates source reliability by
|
|
comparing sources on the same object, and the standard survey states outright
|
|
that an object provided by very few sources cannot have its confidence
|
|
evaluated — Li, Gao, Meng, Li, Su, Zhao, Fan, Han, *A Survey on Truth
|
|
Discovery*, SIGMOD Record 45(1), 2016 (arXiv:1505.02463), §"Challenges" on
|
|
sparse sources. With the two Readers this backend actually has, a
|
|
disagreement is a coin flip. The Poll is an authoritative oracle, so it is
|
|
the only judge.
|
|
- **A randomised audit** (Poll a fraction of deferred Series) rejected in favour
|
|
of the fixed ceiling. Sampling an oracle against untrusted reports is the
|
|
gold-question technique from crowdsourcing quality control — Le, Edmonds,
|
|
Hester, Biewald, *Ensuring quality in crowdsourced search relevance
|
|
evaluation: the effects of training question distribution*, SIGIR 2010
|
|
Workshop on Crowdsourcing for Search Evaluation, which inserts known answers
|
|
sporadically and adjusts each worker's trust from them. The ceiling is the
|
|
same idea made deterministic: sampling prices an attack in expectation, a
|
|
guaranteed six-hour audit prices it as a certainty, which is what makes the
|
|
solitary-Series rule defensible in one sentence.
|
|
- **A trust *ratio*** (agreements over judgements, as that same gold-question
|
|
scheme uses) rejected for two thresholds: a ratio lets an attacker bank
|
|
credit first and spend it on lies later, and it needs the owner watching a
|
|
score to act. Three-and-twenty is a threshold both ways — a disagreement
|
|
resets the run to zero, so credit cannot be pre-bought, and recovery happens
|
|
without the owner in the loop.
|
|
- **Blocking a marked Reader's writes** rejected: the Latest Chapter they report
|
|
is still the best available value, and their Sightings must keep being judged
|
|
or they could never earn the privilege back.
|
|
- **Per-Series flagging** rejected in favour of per-Reader marks: a Series is
|
|
not the thing that can be wrong. Naming the Reader and logging both numbers is
|
|
also what distinguishes a broken Site adapter (every Reader of that Site
|
|
contradicted at once) from one bad actor.
|
|
- **Timers or a background reputation job** rejected: deferral is recomputed
|
|
from live facts every round — Bookmark count and sighting timestamp — so a
|
|
Series that gains a second Bookmark stops deferring at once, with nothing to
|
|
invalidate. The Reader's marks are the one input read earlier, when the
|
|
Sighting is recorded rather than when the round runs: a Reader who crosses
|
|
the threshold, or has their marks cleared, changes behaviour from their next
|
|
Sighting on, and the standing they already bought lasts out its rest. That is
|
|
bounded by one rest and costs one subselect instead of joining `readers` into
|
|
the due query on every round.
|
|
|
|
## Constraints preserved
|
|
|
|
- A Sighting is not Progress: it may move the Latest Chapter and nothing else.
|
|
`updated_at` never moves, so a report cannot reorder the list (ADR-0004).
|
|
- The Latest Chapter is a Series-level fact (ADR-0003): a Sighting writes the
|
|
shared row, so every Reader of a shared Series sees it immediately — deferral
|
|
is the only thing the solitary rule withholds.
|
|
- Ember means new chapter only (`docs/design-system.md`): a marked Reader
|
|
renders no differently in their own list, and nothing about the trust model
|
|
reaches the Series list's colour.
|
|
- The Poll remains authoritative. Where a Sighting and a Poll disagree, the
|
|
Poll's value is what gets stored.
|