Files
mangaBookmark/docs/adr/0011-sighting-deferral-trust-model.md
sulthan ba679223b2 Sightings: a Reader report defers a Poll of a solitary Series (#103) (#108)
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>
2026-08-16 20:10:59 +07:00

7.9 KiB

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.