Handle $ARGUMENTS. check, or no argument, runs the gate bound. deep runs --marks=3 and --blocks=3 and compares their failures and coupled sets with Findings. lower runs --lower after a fix removes a known finding or a coupled set. update brings the verification in line with changed code, as Coverage record says.
The property. For every document in the bound, deciding its pending changes one at a time, each accepted or rejected in any admissible order, gives the same children and the same readable landed writes as deciding the same verdicts in one call. A step may wait only on another change that is still pending, and so may deciding the same verdicts at once. A set of verdicts that settles only when decided at once, because every order of single steps waits before reaching it, is a coupled set, and the list of coupled sets only shrinks.
The harm. A reviewer who accepts and rejects suggestions one by one gets a different document than a reviewer who decides the same suggestions together, so text moves or disappears depending on click order. A new coupled set makes a reviewer decide two suggestions in one call, and it hides any decision-order bug in the orders it blocks.
What it checks
packages/plitejs/scripts/authored-decision-order.ts runs the real resolver on every document it can build within the bound. It is bounded exhaustive checking of the TypeScript code, not a model; no model checker or verifier runs on this resolver, so the script enumerates the inputs instead of sampling them.
- It builds every document of one or two top-level blocks, each a paragraph or a quote of one or two paragraphs, with each paragraph holding one letter (
a,b, and so on). - It places one or two pending marks from changes
s0ands1at every place recording writes one. A block's boundary with the block before it takesjoinorsplit. A block's whole content takesinsertordelete, spread into its children as recording writes it. A leaf takes its owninsertordelete. A quote takeswraporunwrap, and analignformat tag with or without the storedalign: 'center'. The script tries a split between two top-level paragraphs with and without the cut link that Enter inside a link writes. - It normalizes each document through
quotesEditorandexpandContexts, as the sampled law does. A document the editor rejects, or one that normalizes to a document another one gives, stops the run, as Read a failure says. - One decision step calls
resolveWithVerdictswith one change's verdict and the earlier verdicts as records, thensettleAsAdmission. The script runs every order of(change, verdict)steps.admissionOfjudges each resolve the waydecideOninauthored.tsdoes. It drops a blocker that names a change the call itself decides, then reads the other blockers before placement. A step whose otherblockersname only pending changes waits, so that order stops there. A step whoseblockersname a change decided earlier or one the document lacks, or that isunplaceable, is a failure. For such a blockerdecideOnreturnsblockedwith a change the reviewer can no longer decide, so the script counts it as a failure, not a wait. From the second step on, the script compares the document with deciding the same verdicts at once, through the same two calls; the first step makes the same calls as deciding its verdict alone. The at-once call follows the same rule. When it waits on another pending change, the script skips that comparison, and any other refusal is a failure. When a step settles and accepts only changes that insert or delete,loadWithVerdictsalso loads the document before the step with every verdict so far, through the editor's own settle, and a loaded document that differs from the step's result is a failure. - Then it tries every set of verdicts that no order of single steps reached, smallest sets first, each as one call from the largest set an order did reach inside it, adding at least two verdicts. A call that settles makes a coupled set, printed as the reached verdicts, then the ones decided together in parentheses, so each coupled set it reports is a minimal one. That call is a step too: the walk goes on from its result and compares each later step with deciding everything so far at once, so a bug behind a coupled set still shows. A set no call reaches and the at-once call refuses is a failure.
- It sorts the failures and the coupled sets and compares them with
packages/plitejs/scripts/authored-decision-order-baseline.jsonthroughtooling/scripts/finding-baseline.mjs, the same shrink-only baseline the lint baseline uses. Failures are thedecision-orderrule and coupled sets thedecided-togetherrule.
The helpers, admissionOf included, come from packages/plitejs/test/support/authored-laws.ts. The sampled fast-check law in packages/plitejs/test/authored-tree-resolve-laws.spec.ts imports them too, so both run the same decision step and settle, and the plitejs test typecheck covers the helpers through the spec. The sampled law skips a step the resolver blocks or cannot place, where the script judges it.
Three reductions keep the run short. Change names are labels, so a document uses s1 only after s0; a run with every labeling, at commit da4bc3e523 on the generator before format tags on deleted quotes, found exactly twice the documents, orders and failures. The first step is not compared, since both sides make the same calls. A failure is reported once. The orders that extend a failed order only mark which verdicts one at a time reaches and are not compared, so a second failure further along a failed order stays hidden until the first one is fixed.
The give-back family
The documents above never build a rejected cut's give-back, so a second family does. It records each script through a real authored editor on a links schema whose link ref drops on a split, starting from "" + link ab ref r1 + "". Change s0 splits the link after a, exactly once. Every other operation sets or unsets ref on the whole link before the split or on either half after it, and at most one operation removes the left half. Changes take s1, s2 and so on by first use; s0 may also set or unset either half but never removes, and --giveback-edits=1 adds one Editing author, which no review decides. An unset runs only where a ref is stored, and each set writes a fresh value (v1, v2 and so on). An operation that throws or changes nothing stops the run as unchecked.
Every step decides through the public decide of authoredTestApi, on an editor loaded from the step before, so this family covers the decide path the quote documents leave out. A blocked result that names only changes still pending waits; any other refusal is a failure. It compares what a reader sees: the children with every authored mark removed. Tag ids are deterministic, so a recording gives the same ids every run, and each worker caches recorded script prefixes. A script with two changes and nothing compared counts as idle rather than stopping the run, because two changes can each wait on the other. Under any generated-id --schema, the family records and decides the same scripts on a twin links schema that generates an id on every element, with the paragraph stated as p1 and the link as l1, and a step also fails when its decision calls the id generator or builds an element that finds no spare. At three operations no decision builds an element, so that count stays 0 even with resolution's generation unmasked, and the variant runs outside the gate. Its rules are giveback-decision-order and giveback-decided-together, and a failure prints the script with its order, as in s0 splits, s1 removes left, s1 sets right v1, s2 sets right v2: (reject s0, accept s1), reject s2.
The lift family
The give-back family cuts a link once, never unwraps or empties a piece, and removes only the left half, so a third family does all three. It records each script on the same links schema, which also holds a quote, a link url, a metadata note and a paragraph align, starting from "" + link amb ref r1 + "". Change s0 splits the link once or twice, inline or, with --lift-enter=paragraph, as an Enter. The other operations unwrap, empty or remove a piece or write its key, by changes labeled by first use. Flags add a paragraph before or after the link's, a second cut link, a quote around the link's paragraph, typed letters, Editing writes and a paragraph joined across the cut.
Every step decides through the public decide on an editor loaded from the step before, as in the give-back family, and also reads which changes stay pending, so a step that leaves a change in review in only one order fails. The family also decides each order on one editor that keeps deciding, and compares what that editor reads with the loaded one after every step; the summary counts those as continued. A failure prints its script and order as a give-back failure does, and no baseline lists lift failures, so a run exits 1 while any remains. --lift-script='<script>' runs one script as a failure prints it, and --lift-shard=<part>/<parts> splits a run past the time budget.
The generated-id family
--schema=generated runs the quote documents under five schemas whose element id generates, and --schema=generated-<places> runs one of them. elements puts a unique id on every element, as Plate's ElementIdPlugin does. blocks puts one on blocks only, mixed on blocks beside an unused element whose content admits text, paragraphs and links, and paragraphs on paragraphs only. Under placed, paragraphs generate their ids, a heading in a quote states one that does not start with p, and any other heading takes any string. A block can also be a card whose body root holds one line. The documents add a move whose copy shows the original id through an as member, after the last block or into the last quote, another change that retypes that copy to a heading or joins it, a quote retyped from a section, and a quote deletion that keeps a spare holding the moved line's id.
Each step loads the value the step before committed and decides through the editor's public decide, through decideIn in packages/plitejs/test/support/authored-laws.ts, so admission settles shown values as it does for a reviewer. A step fails when its decision calls the id generator, adds a spare or builds a line that finds no spare. After a step that only accepts insertions and deletions and keeps every root, a peer loads the value the step started from with the verdict records the step stored. The step fails unless the load's children and roots equal the decision's with held and spare members left out, and the load stores every spare the decision stores, under the same container or among the root records, as many times. Any other spare the load keeps must match a copy that the step started with and the decision drops, one kept spare per copy. It also must not be an unbuilt spare whose line the load shows. Settle leaves a container it does not change as stored, spares included, because a Yjs room's editors still build lines from them, so a spare the decision drops may stay there. The comparisons ignore decision tokens. A coupled set fails unless the document's plain twin, the same schema's elements with no id, couples it too, so ids and their settlement add no set a reviewer must decide together. A mutant that drops the ancestor type tag term from isPendingHolder fails generated-placed with 35 failures.
The gate runs one variant at one block, --schema=generated-elements --family=quotes --blocks=1, after the plain bound. It builds 514 documents and makes 1,171 comparisons in about 17 s with the load average near 45. Those documents hold a card whose body root a decision drops with the card. Phase B's diff panel finding 1 added two drops of that root's spare records, one in resolve.ts and one in admission.ts. With both reverted, the run fails 16 documents. With settle's spare rule from before the merge panel on phase B, it fails 10 documents through the settle comparison. Every other variant and bound runs outside the gate and its baseline, and exits 1 on any failure. At two blocks each variant builds 10,079 documents and makes 21,660 comparisons in 168 to 258 s, about 17 minutes for all five with the load average near 50, and finds 296 coupled sets, each one its twin couples. Run it after a change to settlement, holders or spares.
Run it
Run every command from the repository root.
pnpm check:authored-orderchecks the gate bound: two blocks, two marks, two changes. It builds 4,467 documents and makes 17,472 comparisons in 4 to 14 s on an 18-core M5 Max, slower while other sessions load the machine, and prints one JSON summary line, then each failure and coupled set the baseline does not list.pnpm check authored-orderruns it, then the one-blockgenerated-elementsrun, through the check runner, andpnpm check --affectedruns both for any change underpackages/plitejs/or totooling/scripts/finding-baseline.mjs.- A flag raises one part of the bound and leaves the rest at the gate's. On the same machine,
pnpm check:authored-order --marks=3builds 54,451 documents and makes 303,134 comparisons in 51 to 105 s. It is the cheapest bound that caught the container-boundary and rejected-wrap bugs in What a pass proves.--blocks=3builds 34,961 documents and makes 137,656 comparisons in 21 to 79 s.--marks=3 --changes=3builds 71,203 documents and makes 1,300,316 comparisons in 90 to 167 s. A bound other than the gate's skips the baseline and exits 1 on any failure, because the known findings repeat there in more documents and orders. It prints the coupled sets only as the summary'stogethercount. Compare its failures and that count with Findings. Run--marks=3and--blocks=3after a change to the resolver's join, split or enclosure logic. - The gate also runs the give-back family at
--giveback-ops=3 --giveback-changes=2 --giveback-edits=0: 890 scripts and 9,603 comparisons in about 21 s at commit55deb94a19, with the machine's load average between 26 and 74. Under the same load,--giveback-changes=1 --giveback-edits=1ran 1,738 scripts in 19 s,--giveback-edits=12,415 in 48 s,--giveback-changes=3972 in 34 s, and--giveback-ops=4 --giveback-changes=311,253 in 465 s.--giveback-script='<script>'runs one script written as a failure prints it, which is how a script past the time budget runs:--giveback-ops=5 --giveback-changes=3holds 137,610 scripts, about two hours.--family=quotesor--family=givebackruns one family. pnpm check:authored-order --family=liftruns the lift family at its default bound: one inline split bys0, two operations, two changes and the url key. At commit4784406a84it records 402 scripts and makes 5,292 comparisons, and 10,022 more on the continued editor, in 55 s with the load average near 20. It exits 1 on the 12 failures in Findings. Each--lift-*flag raises one part of that bound; the comment aboveliftScriptsin the script names each one.--jobs=<n>sets the number of worker processes; it defaults to the machine's cores.--together=<file>writes every coupled set to that file, sorted, one per line: its identity, then=>and what a reader sees after deciding it at once: the document and readable landed writes for a quote document, the children for a give-back script. A bound flag and--togethertake the--marks=3form, and any other argument exits 2.- After a fix removes a known finding or a coupled set, the gate fails until you run
pnpm check:authored-order --lower, which drops the entries the tree no longer holds. The baseline never grows:--lowerrefuses a new entry, andfinding-baseline.mjsrefuses an entry its copy at the base commit lacks.
Read a failure
Each failure prints the document, the order of steps, and both results. This one is the bug Findings describes, before its fix:
quote(paragraph{delete s0}("a"{delete s0})) paragraph{split s1}("b"): accept s0, reject s1
one at a time: quote(paragraph("b")) landed []
all at once: quote(paragraph("")) paragraph("b") landed []
A refused step prints the last step is unplaceable or the last step is waiting on s0, which is not another pending change in place of the two results, and a refused at-once call prints the same reason after all at once:. A set of verdicts that no order reaches and the at-once call refuses prints the verdicts in change order, then at once, then the reason and and no order reaches these verdicts. A new coupled set prints the document and its verdicts in change order, then settles when decided together, while every order of single steps waits.
A node is type(children) and a leaf is its text in quotes. {delete s0} lists the node's marks by kind and change; a format tag shows its key, and a cut shows its side. [align center] is a stored property. landed [...] lists the landed writes that some remaining tag still links, the only ones a reader can see.
When the run says that some generated documents fail to load, did not come back from the workers, normalize to a document another one gives, or hold two or more changes with no order the script could compare, it exits 1 at every bound, --lower included, before it prints failures or reads the baseline. Those documents went unchecked. Find what changed in the editor, the resolver or the generator before anything else.
Tell a real bug from a modeling error before you change code:
- Real bug. The document is one recording writes and the two results differ in children or readable landed writes. Recording writes one mark of a change per node, spreads a whole-block insert or delete into every node inside the block, puts boundary marks only on a block that follows another, and never tags a node with a format change that also inserted it.
formattedPropsinpackages/plitejs/src/authored/tree/record.tsowns that last rule. It writes no tag on a node that already holds a delete mark and none inside the writer's own insertion, so a tag written before its change deletes the node stays beside the delete mark. Fix the resolver, then run--lower. - Modeling error. The document breaks one of those recording shapes, or the difference is in something no user reads. Fix
slotsOforbuildin the script, or the shared helper, and fix the sampled law's generators in the spec the same way. - Intended change. A change that alters what a decision produces changes both sides alike and keeps passing. A step may wait on another pending change only while every set of verdicts that settles at once stays reachable one at a time or is already a coupled set. A new coupled set fails the gate, because it makes a reviewer decide those suggestions in one call and can hide a decision-order failure, as What a pass proves shows. Whether that trade is worth it is the owner's call, and accepting it changes this check, not the baseline. Never weaken the property or add a baseline entry to get green.
Findings
No bound of the quote documents or the give-back family finds a failure. The gate bound found two and the deeper bounds 22 at --marks=3, 14 at --blocks=3 and 78 at --marks=3 --changes=3 until the fix for suggested task t_6e758b7d. All had one shape, a paragraph with a pending split right after a quote whose paragraphs a decision removes. For quote(paragraph{delete s0}("a"{delete s0})) paragraph{split s1}("b"), accepting s0 and then rejecting s1 gave quote(paragraph("b")), while deciding both at once gave quote(paragraph("")) paragraph("b"). With a paragraph x before the quote, deciding both at once rejoined b into x instead.
Deciding the removal first leaves the empty paragraph that completion builds, because a quote needs one, and the rejected split then rejoins it. Deciding both at once, phase one of resolveAuthoredChildren looked for the nearest preceding text block the decision keeps before completion ran, so it passed the quote. Now the split looks back for the first container that the decision keeps but empties, or whose children it lifts out by accepting an unwrap or rejecting a wrap. Only that container can take the split. The split's block takes the removed block's place there only when the decision does not lift the container's children, the container has no prefix slot, completion would rebuild it as one empty line with exactly the split block's properties, and the split block carries no format tag. Deciding the removal first and then rejoining the split into that line gives the same document. Otherwise the split looks further back as it did before the fix, and no container further back can take it. It waits on what removing that block, emptying the container and building the line wait on. Through the public decide of authoredTestApi in packages/plitejs/test/support/authored.ts, both orders now give quote(paragraph("b")), and rejecting s1 first still returns blocked. Every coupled set at every bound below is the same set as before the fix, compared one by one. Decided at once, the gate's and --blocks=3's give the same documents as before, while 12 of those at --marks=3 and 16 at --marks=3 --changes=3 give another one. Each of those has a split right after a quote the decision empties, and the split's piece now takes the place of the quote's paragraph instead of following the quote. The script does not compare these documents; a one-off dump of each set with its document did.
The gate bound has 66 coupled sets. Each merges two paragraphs, by accepting a join or rejecting a split, while the other change's verdict removes the content of one of them. In paragraph{delete s0}("a"{delete s0}) paragraph{join s1}("b") with both accepted, accepting s0 first waits on the join and accepting s1 first waits on the deletion, while deciding both at once gives paragraph("b"). Through the public decide, accepting either change first returns blocked with the other, and deciding both together applies. --blocks=3 has 372 coupled sets, --marks=3 has 2,362 and --marks=3 --changes=3 has 5,334. A deeper bound does not check these counts, so compare them by hand.
At --marks=3 --changes=3, measured before the generator added format tags on deleted quotes, 84 orders decide two changes one at a time where deciding the same two together waits on a third change, a split that is still pending. One is paragraph("a") quote{unwrap s0}(paragraph{delete s1}("b"{delete s1}) paragraph{split s2}("c")) with s0 and s1 accepted. Lifting a quote's children, by accepting an unwrap or rejecting a wrap, waits on the join boundary of its first child, as the comment beside that wait in resolve.ts says. Decided together, that child is the split paragraph, so the resolver waits on s2. One at a time, the first child is still b when the unwrap lands. The script skips those comparisons. Nothing shows that the wait loses content, but it makes deciding together stricter than deciding one at a time.
The give-back family finds no failure at any bound run. Its gate has 1,105 coupled sets, --giveback-edits=1 1,550, --giveback-changes=1 --giveback-edits=1 672, --giveback-changes=3 1,303 and --giveback-ops=4 --giveback-changes=3 29,937, and the five-operation script in What a pass proves gives 3. Each is the same set as before the split copied what it drops, compared one by one. Before that copy, the gate found 12 failures, each rejecting a cut whose left half stays. In s0 splits, s0 sets right v1, s1 sets left v2, s2 unsets left: accept s1, reject s0, accept s2, one at a time read link("ab"), and at once read link[ref v2]("ab"). --giveback-edits=1 found 65 failures, --giveback-changes=1 --giveback-edits=1 33, --giveback-changes=3 24 and --giveback-ops=4 --giveback-changes=3 773.
The lift family fails, so it runs outside the gate. At its default bound on commit 4784406a84, it fails 12 orders and has 96 coupled sets. In each failure, s0 cuts the link amb twice and two other changes each remove a different piece. Rejecting s0 and then deciding one removal leaves the right piece as a link of its own without the ref, while deciding both at once rejoins every piece: s0 splits after a, s0 splits after m, s1 removes middle, s2 removes right: reject s0, accept s1 reads link[ref r1]("a") link("b") one at a time and link[ref r1]("a" "b") at once, with s2 still pending. Deeper starts failed more at commit 8733523c81. Two cut links one paragraph apart, --lift-pair=1, failed 36 orders with 1,084 coupled sets. A quote around the link's paragraph, --lift-quote=1, failed 148 with 602. A paragraph joined to the link's across the cut, --lift-bridge=1, failed 180 with 3,232. A paragraph after a quoted link, --lift-quote=1 --lift-outside=1, failed none with 186. Eight iterations of the cut-join subject, from docs/plans/2026-10-24-split-rejoin-order.md to docs/plans/2026-10-27-split-rejoin-order-8.md, tried to fix these, and the last one says what is left.
What a pass proves
The check catches a defect that makes the result depend on decision order, that refuses a step no other pending change holds, or that couples a set of verdicts the baseline lacks. At commit da4bc3e523, bugs planted in resolveAuthoredChildren showed where that holds:
- Letting a rejected split rejoin into a block the decision removes, by dropping the
!survives(candidate.node) && !rejoins(candidate.node)test, gives six failures at the gate bound that the baseline does not list. - Letting a join leave the quote that holds it, by dropping the
boundarytest, passes the gate bound and--blocks=3, and gives six new failures at--marks=3and fourteen at--marks=3 --changes=3. One isquote(paragraph{delete s0}("a"{delete s0})) quote(paragraph{delete s0}("b"{delete s0}) paragraph{split s1}("c"))withs0accepted, thens1rejected. - Keeping a rejected wrap passes the gate bound and
--blocks=3, and gives one new failure at--marks=3and three at--marks=3 --changes=3. The one at--marks=3isquote{wrap s0}(paragraph{insert s0}("a"{insert s0})) paragraph{split s1}("b")withs0rejected, thens1rejected. - Marking a step unplaceable where the rejoin target was removed, so the resolver refuses the known failing orders instead of fixing them, gives 94 new failures at the gate bound.
- Making a pending split wait on a decision that removes the block before it, the mirror of the wait a rejected split already has, turns both known failures into coupled sets, 38 new ones in all at the gate bound. The gate fails, and so does
--lower, which leaves the baseline unchanged. Planted together with the rejoin-target bug above, it still adds 38 and fails. Making every removal wait on every other pending change adds 348. - Letting a join land in a block an earlier join emptied, counting a deletion made after a join, and ignoring earlier verdict records passed the gate bound,
--marks=3,--blocks=3and--marks=3 --changes=3. Either each changes both paths alike or it needs a larger document.
The fix for t_6e758b7d adds a path where a rejected split takes the place of a removed block in a container the decision empties. Bugs planted in that path gave these results:
- Turning that path off gives back the two known failures at the gate bound.
- Taking that place while the container keeps other content, by dropping the test that nothing in it survives, gives two failures at the gate bound. One is
quote(paragraph("a") paragraph{delete s0}("b"{delete s0})) paragraph{split s1}("c")withs0accepted, thens1rejected. - Leaving the container's own pending marks out of what the split waits on passes the gate bound. At
--marks=3 --changes=3it gives two failures, adds 12 coupled sets and drops 2, each on a quote that holds a pending format tag, insert, delete, wrap or unwrap. - No bound can catch a looser test on the line completion would build, because in the quotes schema completion always rebuilds an emptied quote as one empty paragraph and the generator gives a split paragraph no property and no format tag. Dropping the whole completion check passed every bound. The decide-completion test
keeps a rejected split in place when the first container the decision empties or lifts before it is not one completion rebuilds, outside any prefix slot, as one empty line with exactly the split block’s properties, or when the split block carries a format tagcatches each of nine such bugs: taking a line of another type, comparing only the type of the line and the split block, taking the place while the split block carries a format tag, taking a line in a prefix slot, taking one of several lines, taking a line that holds an element, taking a place in a container the decision lifts, looking past such a container, and looking past the first emptied container that fails these tests. Each makes deciding the changes together returninvalidor give another document than before the fix. - Taking that place in a quote the decision unwraps passes every bound. The smallest such document is a paragraph, a quote that one change unwraps and empties, and a paragraph whose split another change rejects. That needs three blocks and three marks, and no bound raises both. The decide-completion test
rejoins a rejected split past a quote whose unwrap it decides with the deletion of the quote’s paragraph, as deciding the two in turn doescatches it.
The give-back family caught each of three reverted give-back builds that stacked both halves' pending tags in one key history. At 20e93253ef the gate finds 3 failures and 15 coupled sets the base lacks, among them s0 splits, s1 removes left, s1 sets right v1, s2 sets right v2: (reject s0, accept s1), reject s2, which reads r1 one at a time and v1 at once; --giveback-edits=1 adds the Editing class. At 23b376a9db, --giveback-changes=3 finds 1,350 coupled sets against the base's 1,303, among them s0 splits, s0 sets left v1, s1 sets left v2, s2 removes left: reject s1, then (reject s0, accept s2). At 7ec61b62cf, the script s0 splits, s1 sets left v1, s1 sets right v2, s2 sets left v3, s2 sets right v4, s3 removes left gives 5 coupled sets against the base's 3; only --giveback-script reaches it inside the time budget. A fourth build, reverted at c1b3b46d56, kept each key history to one half's pending tags and passed the family at every bound tried, so its bugs were all outside the family, as What it does not cover lists.
A settle that always kept the accepted delete claim on an element still holding live content failed the quotes family through the settle leg in the build of docs/plans/2026-10-26-settle-repair-identity.md.
The sampled laws in packages/plitejs/test/authored-tree-resolve-laws.spec.ts and the resolver's other tests own what each decision produces.
Coverage record
Synced at commit b2228e62ef. The verification describes these files and functions as they stand there:
| File | What the check exercises |
|---|---|
packages/plitejs/src/authored/tree/resolve.ts |
resolveAuthoredChildren for the main root, its structural landing, enclosure lifts and canonical completion |
packages/plitejs/src/authored/facts/facts.ts |
factsOf, the review facts each step reads |
packages/plitejs/src/authored/facts/records.ts |
verdict and landed records, through reviewRecords in test/support/authored.ts |
packages/plitejs/src/authored/facts/compact.ts |
expandContexts on the normalized document |
packages/plitejs/src/authored/resolve-schema.ts |
authoredResolveSchemaOf for the quotes schema |
packages/plitejs/src/authored/authored.ts |
createAuthoredPlugin normalization of the initial value, which blockers decideOn drops, and the order in which it reads blockers and placement |
packages/plitejs/test/support/authored-laws.ts |
the step, the settle, admissionOf, loadWithVerdicts and the comparison |
packages/plitejs/src/authored/facts/settle.ts |
settle on load, through loadWithVerdicts under the plain schema and through the generated family's checkSettle, for steps that accept only inserts and deletes and keep every root; synced at feat(plitejs): store spare blocks and unique ids so suggestions never generate them |
packages/plitejs/src/authored/tree/record.ts |
not run; slotsOf and build copy where recording puts marks and format tags |
The give-back family is synced at fix(plitejs): keep the right half's own write when a rejected split's left half is gone. It runs recording and the public decide whole, so it describes these files as they stand there:
| File | What the family exercises |
|---|---|
packages/plitejs/src/authored/tree/record.ts |
splitHistoryTags, which copies the left half's tags on the dropped key onto the right half under the cut's own tag |
packages/plitejs/src/authored/tree/cuts.ts |
joinRejectedCuts, which joins a rejected cut's halves or keeps them apart, and then drops the right half's copies |
packages/plitejs/src/authored/tree/resolve.ts |
in resolveIn, the walk to the fragment before a removed one, the met halves, the waits of a pending cut's removed left fragment, and apartFromRemoved, which keeps a right half apart from a left half its own removal takes |
packages/plitejs/src/authored/tree/halves.ts |
leftSupplies and the halves comparison, which read the dropped key from the left half while the right half holds only the split's output |
packages/plitejs/src/authored/tree/layers.ts |
decideKey and keySteps on the dropped key, which read a rejected split's copies, splitCopiesOf and copyKeyOf, which key each copy by key and write id, and recordFormats while recording |
packages/plitejs/src/authored/authored.ts |
decide through authoredTestApi, with its blockers, placement and settle |
packages/plitejs/test/support/authored-laws.ts |
the links schema, the recorded scripts and the reader's view the family compares |
The lift family is synced at 4784406a84, the commit it was added on, outside the gate. It runs the same files as the give-back family, with liftStart, recordLift and the pending read in authored-laws.ts.
To see whether the verification needs another look, run git log --oneline b2228e62ef..HEAD -- packages/plitejs/src/authored packages/plitejs/test/support/authored-laws.ts packages/plitejs/scripts/authored-decision-order.ts. The commit that added this check, test(plitejs): check every decision order of the authored review tree up to a bound, is in that range; it is the check itself. For each other commit it lists, read the diff and confirm three things. One call of resolveWithVerdicts plus settleAsAdmission still matches the resolve call in decideOn in authored.ts, apart from what What it does not cover lists. admissionOf still drops the blockers a call decides and reads the rest before placement, as decideOn does. Recording writes no mark kind or place the script lacks. Move the sync commit in this file forward only after that check and a run of pnpm check:authored-order. When recording gains a mark kind or a place, add it to slotsOf and build and to the spec's generators in the same change.
What it does not cover
- For the quote documents under the plain schema, the public
decidepath inauthored.ts: revision checks andstale, cards, decision tokens,decideOn'sstores: trueresolve, withdrawals of held members, thetakencheck, the second blocker check for insertions inside removed content, schema validation of the decided document and undo.settleAsAdmissionstands in for the run-scopedresolveOwninauthoredAdmission. - Readable landed writes. The generated format tags link no earlier write through
over, so no document in any bound tried has one, and only the children half of the comparison can fail. - A known failure whose results change. A baseline entry names the document and the order, not the results, so a known failing order that gets worse stays green.
- What deciding a coupled set produces. No order of single steps reaches a coupled set, so nothing compares its at-once result. A planted bug that lets an accepted join merge into a block that another change in the same call removes passed the gate bound and changed 24 of the 68 coupled-set results. Other authored tests caught it with 34 failures, among them the sampled law
accepting every change at once equals accepting admissible batches in turnand the preview reads of a block a pending join passes. To see which coupled-set results a resolver change moves, run the same bound with--together=<file>before and after it and diff the two files. - Coupled sets beyond the gate bound. Only the gate's are in the baseline; a deeper bound prints their count, which Findings records.
- Splits inside a quote with a cut link. The script tries cut links only between top-level paragraphs.
- The script's own code. It is in no typecheck program, so keep the code that reads resolver results in the shared helper, which the plitejs test typecheck covers.
- Named roots and orphaned roots, which
resolveAuthoredValueadds aroundresolveAuthoredChildren, except a card's body root of one line, which the generated-id variants build and the gate builds only underelementsat one block. - Shapes no bound builds, where the split is decided as it was before the fix. In one, the first container the split meets has a prefix slot, the decision lifts its children, or completion would rebuild it with no line, with more than one line, with a line that holds an element or with a line whose properties differ from the split block's. In another, the split block carries a format tag, pending or decided in the same call. Removing everything in such a container and rejecting the split right after it can still give a different document one at a time than at once, or refuse the second step as
invalidwhile deciding both together applies. Suggested task t_674a5226 tracks it. - For settle on load, any step that rejects, decides a join, split, wrap, unwrap or format tag, calls the id generator or drops a root. That leaves out a line deleted with its boundary, whose accepted join settle keeps unlanded, which suggested task t_e3dcf88b owns. Next's load of a verdict that deletes a card throws
Composing overlapping lifecycle changes for root rN requires the source value, so the generated family skips a step whose decision drops a root until the construction-values plan's Open work item on that throw lands. The comparison loads through a fresh editor, so it never drivessettleBlocks, which a peer's collaboration commit runs, and it leaves out the formats and contexts a settle lands. It lets a load keep a spare the decision drops when the step started with it, because settle keeps a spare while the build that would replace its block waits. So a settle that never drops a root record or a container's spare still passes. It cannot see a stale spare that the decision keeps too, such as the spare decide keeps for a join on a block the same decision removes. That plan's Open work lists both. - Bold or other leaf formats, text longer than one letter, more than two blocks, marks or changes at the gate bound, schemas other than the plain one and the five generated-id ones, the generated-id family at any bound past the gate's, corrections that derive values, and collaboration.
- A block whose unique value an unbuilt spare also holds, such as one an Editing write stores after the suggestion. While that block is stored, the decision that builds the spare renames the built block, so the stored ids depend on decision order, and deciding a pending removal of that block before or with the build keeps the spare's value (construction-values plan Open work).
- Sizes beyond the bound. A pass covers only the documents the bound builds.
- The lift family, in the gate. A resolver change that fixes a lift failure or adds one shows only when someone runs
--family=lift. - An edit between two decisions. Each family records its whole script before the first decision, and the continued walk only decides, so no family edits a document that an earlier decision changed.
- For the give-back family, everything past one link, one dropped key with no fill or generated value, one split and one change that removes the left half. That leaves out a chain of cuts, two changes that remove the left half, a removal of the left half's paragraph, a write to another key of either half, to another node or to another root, a change that removes a node holding another change's write, a structural wait such as a second split, five operations after the split, a proposed Enter that cuts a block, collaboration and recordings with random tag ids. Three push panel rounds on the reverted fourth build found its bugs by reading the source: a chain of cuts within a block and across blocks, a removal of the left half's paragraph, writes to another key, node or root, a removal of a node holding another change's write, a structural wait, a key whose fill differs by place, and five operations after the split. The decision log of
docs/plans/2026-10-23-giveback-order.mdholds each case. With random ids the same verdicts on two recordings of one script could settle differently, which the family no longer sees.
/pstack:maintain-formal-verification-skill brings this verification back in line after changes that did not recheck it.