Appearance
ADR-0080: The verification surface states what the server states, and a refused write never reads as an unverified page
Status: Accepted Date: 2026-09-20
Decisions
D-161 Verifying a page that is already verified is a coded refusal, and the client words it
POST /v1/curation/verifications answers a second verification of the same document page with a 409 carrying errorCode: PAGE_ALREADY_VERIFIED, joining select-span, reconcile-restriction and publish-snapshot in curation-refusals.golden.json. message stays English developer text. The curator's Thai sentence is written by the client from the code, never from the status number and never from the server's prose.
D-162 Opening a document shows the verifications it already has, read-only
openDocument() reads GET /documents/:sdsId/verified-pages alongside the stored copy and seeds every page that has a verification: the text the Corpus holds, whether it was corrected, the curator who verified it, and no form. The outstanding count is then true across a reload instead of true only within one sitting.
A corrected page's raw extraction stays unserved. It is the text the correction replaced, and that ruling is unchanged by this one — the panel shows what selection will use, and says that a correction happened, and does not show what was wrong.
D-163 "Done" is derived from the server's own gate and writes nothing
The control that closes a document is enabled only when verified-pages answers selectable: true, and pressing it clears the document id field for the next document. It records nothing: there is no "this document is verified" row, because the gate already answers that question and a stored flag beside it is a second answer that can disagree. Where selectable is false the control is disabled and the gate's own refusal text is shown verbatim.
D-164 A curation stage links forward carrying the document id, and every stage stays addressable
Verification links to span selection, span selection to reconciliation, each carrying ?sdsId=, so a curator types a document id once. A forward control is enabled from the server's state for that document and, when disabled, shows that refusal verbatim.
Nothing is locked. Every curation page remains independently addressable, because the API is what enforces stage order — span selection already refuses unverified text — and a UI that enforced it again would be a second gate that can disagree with the first, and would block a curator returning to fix one span on an otherwise finished document.
D-165 A recorded verification is a toast; a refused one is a notice that stays
Success raises a toast: it is the feedback a curator is missing, and it is safe to lose. Every refusal — already verified, refused by the rules, server unreachable — keeps the persistent panel notice this surface already had, and may raise a toast as well. A refusal a curator scrolled past must not be recoverable only from memory, and "unreachable" that has faded is indistinguishable from a submission never made.
handleLoading contributes the blocking overlay from the existing Loading store for the whole of an in-flight write, and contributes nothing else here: its error path reads an axios-shaped e.response.data, and this surface's resources use plain fetch and return a discriminated outcome rather than throwing. They are not to be made to throw in order to reach that path — returning the outcome is what keeps every failure reported to the caller and none swallowed, which is CurationVerification.ts's own stated rule. The refusal wording stays with the surface, so a coded refusal is never rendered as a generic failure.
Context
A curator testing the prototype re-verified a page that was already verified and was told:
บันทึกผลการตรวจสอบไม่สำเร็จ: เซิร์ฟเวอร์ตอบกลับ 409 หน้านี้ยังนับว่ายังไม่ได้ตรวจสอบ
Two statements, both defective. The server had said this page of this document has already been verified, and a verification is not overwritten; the client threw that away and printed the status number, because recordExtractionVerification collapses every non-ok response to เซิร์ฟเวอร์ตอบกลับ ${status}. And the page then appended its standing sentence — that the page still counts as unverified — which is true of every other failure on this surface and is the exact opposite of the truth here. The page is verified. The curator was told to go and do the one thing that cannot succeed.
The surface also forgets. Every reload starts every page Unverified, so the outstanding count counts the session rather than the document, and a curator returning to a half-verified document cannot see which pages they did.
verify-extraction is the only refusing curation route with no errorCode in the golden set, which is why its client had nothing but a status number to print.
Decision
The refusal becomes coded, the client words it, the panel it belongs to flips to verified by reading the record that already exists, and the surface loads the judgements it already holds. The stages link forward, a derived control closes a document, and success — and only success — is allowed to fade.
Rejected options
- Special-case HTTP 409 in the frontend without an
errorCode— rejected: this repository's rule is that a client words a refusal from a code, and 409 on this route will mean something else the day a second conflict exists. - Correct the sentence and leave the panel unverified — rejected: the screen would still show "not yet verified" for a page that is verified, which is the same confusion in a quieter voice.
- Serve the raw extraction of a corrected page so a curator can audit the correction — rejected for now: it reverses a decision recorded in
find-verified-pages' own contract, and the request that prompted it was to see what the Corpus holds, not to review a colleague's judgement. It needs its own ruling if it is wanted. - Have "Done" write a completion record naming the curator who closed the document — rejected: a stored flag beside a computed gate is two answers to one question, and this repository has ruled against that shape every time it has come up.
- A locked three-stage wizard — rejected: the API already enforces order, and a UI that enforces it again blocks the legitimate case of returning to a finished document.
- One feedback channel, toasts for everything — rejected: a refusal that auto-dismisses leaves the panel as its only trace, and for "unreachable" that trace is identical to having submitted nothing.
This ruling may not be re-decided
If your change contradicts an Accepted ADR: stop and raise it. Do not implement over it. Superseding requires a new ADR that says so; never edit an Accepted one.
Consequences
A curator can leave a document and come back to it, which is what makes a day-per-document (D-58) workload survivable. The gate a curator sees is the gate the server runs, in the server's own words, in three places.
What becomes hard: the surface now makes two reads where it made one, and a document whose stored copy opens but whose verification read fails must say so rather than silently showing every page as unverified — which is a new failure state, and by this repository's discipline it stays distinct from the other four.
Coverage
No upstream finding lands here. The ruling came from a curator's prototype test on 2026-09-20 and from reading the defect out of CurationVerification.ts and CurationVerifyExtractionPage.vue (D-104).