Appearance
ADR-0084: A curated selection is superseded or retracted, never edited
Status: Accepted Date: 2026-09-20
Decisions
D-169 Correcting a curated selection is superseding the attachment row: one act, one statement, and the replaced row is kept
The unit of supersession is the attachment row — a CuratedFieldEntry, PpeItem, FirstAidMeasure, SpillResponseBranch or SpillResponseStep — and never a SourceSpan. A Source Span carries no position and two of those rows carry two spans apiece: a PPE item holds its passage and, optionally, its stated reason, and a Spill Response branch holds its stated condition. Correcting either of those second spans is a change to the row, so the row is what is replaced, at its own position, whole. A Source Span is never superseded, never rewritten and never darkened; it simply stops being pointed at.
A selection already in a Chemical Record is corrected by writing the replacement row and marking the replaced row in the same statement. The marker is two-sided, following the shape the finding register already uses (D-81): the replacement names what it replaced, and the replaced row names what replaced it. Neither side alone is sufficient.
A replacement states each of its spans one of two ways, and the choice is the Curator's per span. A passage that changed is stated as text and becomes a new Source Span, created under the row through the ordinary path with the whole-document gate run over it. A passage that did not change is referenced by id — and the only ids reachable are the spans the row being replaced was itself carrying.
That second key is narrow on purpose. select-span refuses a sourceSpanId outright, because "accepting a span id would let a selection reuse a span drawn from a document whose pages were never verified". That reason does not reach a supersession: the span named is one the predecessor already displayed, at this very position, in a document verified whole before anything was selected from it. A span from anywhere else in the document — let alone another document — is refused, so the key cannot become the general span-reuse key that file declines to add.
What the reference buys is the whole affordability of this ruling. A referenced span is the same row, so its Reconciliations are its own and nothing is re-judged. Correcting one word of a PPE item's stated reason therefore costs the judgement of that reason and nothing else, where replacing both spans would re-open the item's unchanged passage against every Restriction in the document.
A replacement that references every span and changes no other value is refused: a recorded correction that corrected nothing is noise in a chain whose whole purpose is to say what changed.
The replaced row is never deleted and never soft-deleted. Its content — its Source Span, its label, its curator, its timestamps — is untouched. It stops being displayed and nothing else about it changes.
Both causes are in scope: a Curator's error, where the wrong passage or the wrong boundaries were chosen, and a change of judgement, where the choice was defensible and is now judged wrong. A supplier issuing a new revision is not in scope and needs no ruling here: that is a new SDS and a second Chemical Record, which D-18 and D-45 already settle.
The act is the same whether or not the document has ever been published. What differs is reach, and ADR-0056 already governs reach: a published version serves the bytes it stored, so a correction reaches devices at the next publish and never rewrites a version already shipped.
D-170 A selection is removed by retracting it, and a retracted position stays vacant
Supersession requires a successor. A Curator who judges that a position should hold nothing retracts it instead, and retraction is a separate act with a separate name so that removing content can never be mistaken for replacing it.
A retracted position is not renumbered. A branch whose second step is retracted runs 1, 3, 4, and that is a legal Chemical Record. Renumbering would rewrite rows nobody judged, and the semantic order D-54 protects is about sequence, not about contiguity.
Retraction is the only act in this product that removes content from a Chemical Record. Its surface says so.
D-171 Nothing cascades from a superseded row, and the pairwise gate reopens the pair
Because supersession replaces a row and never a span, every Source Span in the document is left exactly as it was, and so is everything hanging off one: its Restriction, where it has one, its Curated Translation, and every Reconciliation naming it. Nothing is rewritten and nothing is darkened.
What changes is only which spans are displayed, and the pairwise rule is stated over displayed spans. The gap arises from a new span and from nothing else:
- A span the replacement references never left the displayed set. Nothing about it falls out, nothing about it reopens, and the Reconciliations already recorded about it are its own, because it is the same row. This is the whole value of the reference key (D-169).
- A span the superseded row carried and the replacement does not reference is no longer displayed, so its pairs fall out.
- A span the replacement creates is displayed and has been judged against nothing, so each of its pairs is a Reconciliation Gap, the gaps read shows it, and the
CORPUSgate refuses to publish until a named Curator resolves it.
That is the whole mechanism: no new state, no new gate, no clock — detect-restriction carries none by ruling, and this decision does not ask it to. A correction costs exactly the judgement of what it changed.
Two consequences of holding the line here rather than cascading:
- A Curated Translation does not carry to a new span, which must be authored again: a rendering reviewed against one passage says nothing about another, and carrying one forward would put a Thai passage behind an original it does not translate. A referenced span keeps the translation it already has, because nothing about it changed.
- Reconciliations already recorded against other displayed spans stand. Nothing about those spans changed, and un-judging them would be a false statement about what the Curator considered.
RESTRICTION_IS_THE_SPAN stays narrow and does not fire when the Restriction bearing on a replacement's new span is one the superseded row displayed. That refusal exists because resolving a Restriction against its own span "records that nothing was considered". Resolving a new span against the Restriction its predecessor carried considers something real and sharp — the prohibition was inside the displayed passage and is now outside it, and whether it still governs is exactly the question a correction raises.
The narrow reading holds in the one case that looks like it might break it: a replacement may reference a span that is a Restriction while also creating a new one beside it. The pair then put to the Curator names two different spans, so the refusal does not reach it, and the pair of that Restriction with itself is one the gate has always excluded — this ruling neither creates that case nor changes it.
D-172 The position is vacated, not the constraint narrowed
A superseded or retracted row's ordinal is set to NULL, which releases its position. NULLs are distinct in every one of the five uniques that carry an ordinal — CuratedFieldEntry, PpeItem, FirstAidMeasure, SpillResponseBranch and SpillResponseStep — so the replacement inserts at the same position with no existing constraint changed.
This works uniformly across the five precisely because D-169 puts the act on the row: every one of them carries an ordinal inside its unique, including the two that hold a second span, whose second span has no position of its own.
Releasing the position is bookkeeping about the row and never the row's content, the distinction D-83 already draws for the finding register's Coverage notes. The ordinal column becomes nullable on those five models; a live row always carries one, and only the superseding and retracting commands ever write the null.
D-173 Any Curator may correct any selection, and the act records who and why
The correcting act carries a curatorName and a reason, both required and neither blank. Both are plain strings, following Reconciliation.curatorName and Reconciliation.caveat: there are no accounts in this service (D-60), and the reason is a human's account of their own act that no supplier wrote, so faking a Source Span for it would put unauthored text behind an SDS citation.
This records who says they did it, not who did it. There is no authorization here and the ruling does not pretend otherwise.
Self-correction is permitted: the Curator who made an error is the one who finds it. No second person is required.
D-174 A superseded or retracted row never reaches a device, and a spec asserts it over the serialiser's source
The snapshot serialiser filters every read on the supersession marker, beside the deletedAt filter it already carries, and excludes those rows before ordering — an ordinal that has been released must never enter a sorted list.
A spec parses read-corpus-rows.util.ts as text and asserts that every read carries the filter. That is this repository's established shape for a rule a reader could omit: curation.spec.ts asserts a line over app.ts, detect-restriction.spec.ts scans its own source for a clock, and corpus.spec.ts parses the schema. Twelve reads and one omission would ship stale Safety-Critical Content with a correct-looking Source Span on it.
The payload's shape is unchanged, so the snapshot fixture that crosses to the frontend and to corpus_check.py does not move.
D-175 A retraction that empties a curated field requires the Curator to assert the document is silent
The seven curated fields are fixed (D-87), so a curated field holding no Source Spans renders as Not Stated — a statement that the supplier's document is silent. A retraction that leaves a field empty therefore makes the product assert something about a document, and only the Curator knows whether it is true: emptying a field wrongly filled from a document that says nothing is correct, and emptying a field the document does speak to is the product lying.
So the act states it. A retraction that would leave its field with no live Source Spans requires an explicit assertion that the document is silent on that field, and is refused without one. Supplying that assertion on a retraction that does not empty its field is refused as well.
That pairing is caveat's: required alongside one outcome, refused alongside the other, because the result reaches a device whatever the Curator intended. It is a rule of the domain rather than an absent input, so both refusals carry a code (D-153).
D-176 Supersession rides the selection command's refusal table, retraction has its own, and corrections chain linearly
Supersession is a field on the existing selection command, not a command of its own, so a replacement passes the same whole-document verification gate and the same Reconciliation gate as any other selection. Its refusals join that command's table — naming a row that is not there, one that is already superseded, a different position, a different document, a span the replaced row was not carrying, and a replacement that changes nothing. Retraction writes no Source Span, needs neither gate, and is its own command with its own table. Every code is derived from its model's table and retyped nowhere (D-136).
There is no un-supersede and no un-retract. Correcting a correction is another supersession of the row that is currently live, so corrections chain and the live row is the tip of the chain. Refusing to supersede a row that is already superseded is what keeps the chain linear: without it two replacements could name one predecessor, and a position would hold two live rows with nothing to arbitrate them. That refusal is the single-live-row invariant, not a duplicate guard.
D-177 Superseding a parent row re-parents its live children, and the set of mutations the Corpus permits is closed
A Spill Response branch is not only an attachment row; it is the parent its steps hang from through SpillResponseStep.branchId. Replacing a branch to correct its stated condition would otherwise leave the replacement holding no procedure and the steps hanging from a row nothing displays.
So a branch's supersession re-parents its live steps to the replacement, in the same statement. Each step keeps its row, its Source Span, its ordinal, its order and every Reconciliation recorded about it; only where it sits changes. The unique on a step's branch and ordinal cannot collide, because the replacement branch holds no steps at the moment they move. A step already superseded or retracted under the old branch stays where it is: it belongs to that branch's history, and carrying it forward would restore content a Curator removed.
Re-parenting is permitted for the same reason releasing an ordinal is: branchId records where a row sits and never what it says, which is the line D-83 draws between bookkeeping and the thing itself.
Retracting a branch retracts its live steps, in the same statement and by the same act. A branch removed from a Chemical Record takes its procedure with it, and the alternative — steps left parented to a row nothing displays — would leave their fate to whether a reader happens to reach them through their branch. The serialiser reads them nested and would likely drop them; the gaps read walks rows by a different path. A rule that holds only because of how one query is shaped is not a rule. Whether retracting a branch empties the Spill Response field is judged once, over the field, under D-175 — not once per step.
That line is not a general licence, so the list is closed. The Corpus permits exactly two mutations of a stored row — releasing an ordinal when the row is superseded or retracted, and re-pointing a branchId when the row's parent is superseded. Every other correction writes a new row. A third entry on this list requires its own ruling.
Context
ADR-0073 refused the second authoring of a Curated Explanation and wrote the resulting hole into its own text:
Nothing can correct a wrong Curated Explanation today. There is no replace, no retire and no edit. A term explained wrongly stays explained wrongly until a ruling says how it may be changed, and
term @uniquespanning soft-deleted rows means a soft delete would lock the term out for good.
The same hole runs through selections, and a Curator met it. The trigger is a Section 6 spill-response passage reading มาตรการทั่วไป : ย้ายแหล่งจุดติดไฟ. ใช้ความระมัดระวังเป็นพิเศษเพื่อหลีกเลี่ยงการเกิดไฟฟ้าสถิตย์. ห้ามมีเปลวไฟเปลือย, ห้ามสูบบุหรี่. which should display without its final sentence.
Three things about that case shaped this ruling.
The verbatim gate does not see it. The shortened passage is a prefix of the stored one, so it occurs in the page's verified text and PASSAGE_NOT_VERIFIED_TEXT passes. ADR-0015 described this class in its own Context — a selection that is "100% verbatim, fully cited, correctly sourced — and lethally wrong" — and the sentence being dropped is ห้าม language, which is a Restriction.
Every escape today is worse than the act. Re-admitting the document is refused by @@unique([productName, supplierName, revisionDate]), except for an undated sheet, where NULLs are distinct and it silently succeeds — producing two Chemical Records for one document that D-45 then forbids merging. So the one correction path that works is an accident of whether the supplier printed a date.
The refusal that created the hole is right about the act. D-150's ground is that "the act of correcting someone's reviewed judgement is a second act, and a create that quietly becomes an update records it as neither." This ADR does not weaken that. It gives the second act a name, a Curator and a reason, and keeps the first judgement intact and readable.
Decision
A Curator corrects a selection by superseding the attachment row that holds it — writing the replacement and marking the replaced row in one statement, with the replaced row kept whole and no longer displayed — or removes it by retracting it, which leaves its position vacant. A replacement states a changed passage as text, which becomes a new Source Span judged like any other, and names an unchanged one by id, which may only be a span the replaced row was already carrying, so nothing is re-judged that did not change. Superseding a Spill Response branch re-parents its live steps to the replacement. Both acts record a Curator and a reason, neither narrows an existing constraint, and both release their position by nulling an ordinal rather than by deleting a row. Nothing cascades: a Source Span and its Restriction are never touched, so the replacement's new spans face unjudged pairs and the CORPUS gate holds publication until a named Curator resolves them.
Scope, and what is deliberately left open
This rules selections only: CuratedFieldEntry, PpeItem, FirstAidMeasure, SpillResponseBranch and SpillResponseStep.
Correcting a Curated Explanation, a Curated Translation or an Extraction Verification is not ruled here, and the reason is mechanical rather than an oversight. Those are keyed by an identity rather than by a position — a term, a Source Span, a page number — and an identity cannot be vacated the way an ordinal can, so D-172 does not reach them. ADR-0073's hole stays open for explanations and D-152's refusal stands for verifications. Each needs its own ruling, and bundling them here would force the hardest member's mechanism onto the easiest.
Whether a published version carrying content later corrected must be signalled to devices is not ruled here. ADR-0056 says a shipped version keeps its bytes, and a correction rides the next publish; whether that is sufficient for safety content found wrong after shipping is a separate question, and answering it inside this ADR would settle by implementation the thing ADR-0073's supersession just showed is worth ruling on.
Rejected options
- Replacing the row in place, recording the new Curator and time — rejected. It is the option D-150 rejected on 2026-09-20, and nothing here changes its ground: "reviewed safety prose carrying a named reviewer and a review date is not silently overwritten by the next curator". A correcting act that overwrites records neither act.
- Withdrawing a selection, then selecting again, as two commands — rejected. It opens the window
spanCreateexists to close, where a span "cannot exist for a moment with an unresolved Restriction over it": between the two calls a field holds nothing, or a Restriction stands over nothing. It also soft-deletes, which is the trap ADR-0073 names by name — the withdrawn row keeps its position's key and locks that position permanently. - Narrowing the uniques to live rows with a partial index — rejected, and not on the precedent ground D-132 and D-150 used, since this ADR is the ruling those two deferred to. It loses because a partial unique index is not expressible in the Prisma schema, so it would live only in hand-written migration SQL, where
prisma migrate diff— which is how every migration in this project is produced, against no database — cannot see it, and wherecorpus.spec.tscannot see it either. That spec parses the schema as text and is the only gate this project has on its constraints. Moving a safety-critical uniqueness rule out of its sight to buy a tidier column is the wrong trade. - Superseding the Source Span rather than the attachment row — rejected, and it was the first shape this ruling took. A
SourceSpancarries no position, so there is nothing to release and the mechanism in D-172 has no anchor. Worse, it cannot express two of the five cases at all: a PPE item's stated reason and a Spill Response branch's stated condition are second spans on a row whose own position never changes, so "supersede the span" leaves the row holding one corrected span and one stale one, with no record that the row was touched. - Creating a fresh Source Span for every span the replacement carries, including unchanged ones — rejected, and it was this ruling's shape until the cost was worked through. It is honest: a duplicated span asserts no false verification, because
verifiedAtandverifiedByare derived in the write path from the page's verification record and cannot be stated by a caller. It loses on affordability. A new span has never been judged, so correcting one word of a PPE item's reason would re-open its unchanged passage against every Restriction in the document, and a correction path expensive enough to avoid does not close the hole ADR-0073 named. - Carrying a Reconciliation forward to a new span whose text is byte-identical — rejected. It is the cheap way to the same place and it is the one option here that decides a human judgement mechanically. D-44 puts Reconciliation with a human at Curation, and a carried row would re-affirm, under the original Curator's name, judgements the correcting Curator never saw. That is the rubber stamp
RESTRICTION_IS_THE_SPANalready exists to refuse. - Letting a replacement reference any live span in its document — rejected. It would not trip the reconciliation gate, and that is the objection rather than the defence: a pair is a Restriction and a displayed span with no position in it, so a span judged in one field stays judged when displayed in another. A Curator could move a First Aid passage into the PPE field and nothing would mark it as content nobody selected there. The reference is bounded to the replaced row's own spans, where "unchanged" is provable.
- Superseding a branch by writing its steps again as new rows — rejected. Under the reference key it costs no new spans and no re-judgement, so it is defensible, but it writes a procedure's worth of rows nobody asked for to correct one stated condition. Re-parenting moves the same rows and keeps every judgement attached to them.
- Refusing to supersede a Spill Response branch at all, so a condition is corrected by retracting the branch and selecting it again — rejected. It charges a whole procedure's Reconciliations for a typo in a threshold, which is the affordability objection that already decided the reference key.
- Appending the replacement at a new ordinal and leaving the replaced row in place — rejected. Order within a Spill Response branch is semantic (D-54), so a corrected second step landing at the end of the procedure is a different procedure.
- Soft-deleting superseded rows so the serialiser's existing
deletedAtfilter excludes them for free — rejected. It is mechanically safer, with no omission risk across twelve reads, and it still loses:deletedAtwould stop meaning deleted and start meaning not served, on models whose rows D-45 treats as evidence that is never quietly removed. The omission risk it avoids is answered instead by asserting the filter over the serialiser's source, which this repository already does for rules of this shape. - Requiring a second Curator to correct another's work — rejected. With no accounts (D-60) a second name is a second string typed by the same person. It buys the appearance of review and costs the ability to fix a typo, and on a single-Curator prototype it makes correction impossible outright.
- Refusing to supersede a span that bears a Restriction, or that a Reconciliation cites — rejected. It is loud and cheap, and it moves the hole rather than closing it: the trigger case is a span holding
ห้าม, so it would be refused on the first attempt and unblocking it would need a further ruling on retracting Restrictions. Leaving the Restriction live lets the gate that already exists do the work. - Widening
RESTRICTION_IS_THE_SPANto cover a superseded predecessor — rejected. It reads like the same rubber stamp and is not one, and it would make the pair permanently unjudgeable, so the gate would refuse publication forever and no trim of a prohibition-bearing passage could ever land. - Refusing a retraction that would empty a curated field — rejected. The same operation is correct or false depending on a fact only the Curator holds: whether the document is silent on that field. A blanket refusal forbids the correct case to prevent the false one.
- Renumbering the ordinals after a retraction — rejected. It rewrites rows no Curator judged, and D-54 protects the order of a branch, not the contiguity of its numbering.
- Ruling explanations, translations and verifications in the same ADR — rejected. Their keys are identities rather than positions, so the mechanism here does not reach them and serving them would force the partial index onto everything. Named as an open hole above instead, which is what ADR-0073 did well even as it did the deferral badly.
This ruling may not be re-decided
If a change contradicts this ADR: stop and raise it. Do not implement over it.
Specifically:
- Do not turn the selection command into an upsert, and do not add a plain edit or delete path for a curated selection.
- Do not soft-delete a superseded or retracted row to free its position. The position is freed by releasing the ordinal, and a soft delete would additionally mean the row was removed, which it was not.
- Do not narrow any unique in
corpus.prismato live rows. The ADR that could have authorised it is this one, and it declined. - Do not widen
RESTRICTION_IS_THE_SPANto cover a predecessor. It looks like an oversight and it is a ruling. - Do not renumber ordinals to close a vacancy. A gap in the sequence is a legal Chemical Record.
- Do not widen the replacement's span reference beyond the spans the replaced row was carrying. It looks like an arbitrary restriction and it is what keeps the key outside
select-span's stated objection to asourceSpanId. - Do not add a third row mutation to the closed list in D-177 without a ruling. Two are permitted because each records where a row sits; a mutation that changes what a row says is the overwrite this ADR exists to refuse.
- Do not drop the source-level assertion over the serialiser's reads on the grounds that the filters are obviously there. Twelve reads is exactly the count at which one stops being obvious.
Consequences
What becomes easy. A wrong selection can be corrected, by a named Curator, with a stated reason, without losing the judgement it replaces. Whole-document re-curation falls out as a sequence of supersessions with no batch mechanism and no window in which a position holds nothing — each act is one statement, so every position holds exactly one live row at every instant.
What becomes hard. A correction to a prohibition-bearing passage cannot be quiet. The Restriction stays live, the replacement faces an unjudged pair, and publication is refused until a named Curator writes the resolution. That is the point, and it is the most expensive path through this ruling.
What this costs.
ordinalbecomes nullable on five models, which makes "a live row with no position" expressible. Only the two commands write that null, and the schema gate can assert the column shape but not the invariant.- Ten refusal codes cross to the frontend and need Thai wording and at least one fixture case each — six joining the selection command's table and four on retraction. The one refusing an unasserted field emptying needs two sentences: what happened, and what the Curator must assert.
- A vacant ordinal reaches a device as a gap in a sequence. The Handler surface renders a vacant position as absent — never as an error and never as an empty step.
- A Curated Translation is authored again for every new span a replacement carries. A referenced span keeps the translation it already has, because it is the same row.
select-spangains a key its own documentation argues against, so that documentation is amended in the same change to say why a supersession sits outside the objection. A comment left saying "there is nosourceSpanId" beside a model that has one is the drift this project keeps catching.- The cost of a correction is now proportional to what actually changed, which is the property that makes the path usable. Correcting a PPE item's stated reason judges that reason. Correcting the passage judges the passage. Neither re-opens the other.
What is closed off. Editing a curated selection in place. Deleting one. Narrowing a Corpus unique to live rows. Branching a correction chain.
Coverage
No upstream finding lands here. This decision came from the owner's grilling of the hole ADR-0073 recorded, and from adversarial reading of the repository's own rulings against a live curation case — the same way ADR-0036, ADR-0021 and ADR-0018 arose. Recorded plainly rather than attached to a finding it does not follow from (D-104).
| Upstream | Landed in | Evidence | Note |
|---|