Skip to content

fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) - #19928

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-19927-readonlywhen-exact-drop-set
Sep 24, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-19927-readonlywhen-exact-drop-set

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #19927

Clause-②: no

What a caller saw before, and what happens now

Before. When readonlyWhen locks read each other in a chain, an update could ignore an edit whose own lock was FALSE on the row the update stored. The card's cascade: c locked by previous.c == 'L', x by record.c == 'open', y by record.x == 'xv'; row { c: 'L', x: 'old', y: 'old' }; update({ c: 'open', x: 'xv', y: 'yv' }). The row keeps c: 'L', so x's lock is FALSE there, yet all three fields were dropped, and a strictReadonlyWrites refusal named x. The settlement that PR #19923 added released every over-locked key once and, when that one step did not settle the set, kept the fixpoint's larger drop set.

Now. The release repeats. The stored row for the card's update is { c: 'L', x: 'xv', y: 'old' }, one readonly_when event [c, y], by id, on bulk (multi: true) updates and for isSystem callers; under strictReadonlyWrites the refusal names [c, y]. No caller field is written while its readonlyWhen is TRUE on the row the update stores (0 opened in 3016 engine runs and 5662 strip runs, below). A cycle with no exact drop set keeps the fail-safe drop.

The change

A1: the card reproduced on origin/main ae0c90c133, before any edit

Real ObjectQL engine, the #19911 suite's in-memory driver, two rows { c: 'L', x: 'old', y: 'old' }:

write stored report
by id r1 { L, old, old } event [c, x, y]
bulk (where: { tag: 't' }, multi: true) r1, r2 { L, old, old } event [c, x, y]
by id, strictReadonlyWrites untouched refused ERR_READONLY_FIELD_REJECTED, fields: [c, x, y]
bulk, strictReadonlyWrites untouched refused, fields: [c, x, y]

Brute-force enumeration of the 8 drop sets: the only exact one is {c, y}. Reproduced.

A2: the search, its bound, and its answers against a brute-force oracle

Why this search. Every step judges every key against one set, so no order enters the answer: field declaration order and payload key order cannot move it (measured below). A one-key-at-a-time release needs an order: either a read graph built from the CEL source, which the strip does not have, or declaration order, which would make the answer order-dependent on a cycle. It was not built.

Termination and cost. ① makes at most n(n + 1) / 2 key judgements; ② at most n + n² (the first release judges ①'s dropped keys once; each of at most n further sets judges every key once). Worst case 3n(n + 1) / 2 key judgements; a bulk write evaluates each over its matched rows. Measured below as CEL evaluations: the highest ratio to that bound was 1.0 (9 of 9 at n = 2, the two-lock cycle with no exact set); at n = 7 the highest was 70 of 84 by id and 144 of 252 on a 3-row bulk write. On every run where ① or its first release settled the set, the evaluation count equals the base's exactly (5535 of 5535 runs); on the 127 runs where it did not, head evaluates at least as many.

Exactness without a cycle. A key's verdict depends only on the judged keys its record reads name. After t sets, every key at depth below t in that read graph holds its final verdict, so the n-th set is exact and the (n + 1)-th gives it back. Measured: every acyclic run reached its exact set (below).

Probe. The real stripReadonlyWhenFields / stripReadonlyWhenFieldsMulti of the base tree (ae0c90c133) and of this head, over 9 hand-built shapes plus 3000 seeded random ones: 2 to 7 text fields, predicates of one or two atoms joined by && / ||, sometimes negated, over record.* (self-reads included), previous.*, parent.status (bound, or unbound for 10% of rows) and true / false; 1 to 3 prior rows; by id and bulk. 178 shapes judged fewer than two keys and were skipped: 5662 runs. The oracle: a key's lock on a drop set D is the one-key strip (only that key carries its readonlyWhen) over the payload with D minus that key removed, i.e. the same evaluator, bindings and fault rules with no settlement. Every one of the 2^n drop sets was enumerated. A shape is cyclic when the record.* reads among its judged keys form a cycle.

class (runs) exact sets (enumerated) head reaches one base reaches one
acyclic (4720) exactly 1 in every run 4720 4673
cyclic, one exact set (846) 1 846 823
cyclic, two exact sets (64) 2 39 (the same answer as base in all 64) 39
cyclic, no exact set (32) 0 n/a: fail-safe drop, equal to base in 32 of 32 n/a
  • Locks opened: head 0, base 0.
  • Order: 3 permutations of field declaration order and payload key order per run, 16986 permutation runs, 0 changed the head's answer.
  • Two exact sets (64 runs): head's answer equals base's in all 64: one of the exact sets in 39 (① or its first release had already reached it), the fail-safe drop in 25. Pinned: a locked by record.b == 'new_b', b by record.a == 'new_a' has {a} and {b}; ② alternates between {} and {a, b} and ①'s {a, b} stands. a locked by record.b == 'old', b by record.a == 'old' has {} and {a, b}; ①'s first pass locks nothing, so {} is the answer and both edits land.
  • Movements against base: 70 runs. In every one, head's answer is exact and each key base dropped but head keeps lands with its lock FALSE on the row head stores. In 4 of them (hand-built, by id and bulk) head also drops a key base had written: that key's lock is TRUE on the row head stores (the knock-on class below).

A3: the same through the real engine, base and head

The #19911 contract review's probe style: 8 hand-built shapes plus 1500 seeded random ones with 2 to 5 fields, record / previous / parent roots, self-reading locks, a static readonly field (35% of shapes) whose value the caller forges in half of them, a master_detail FK with its own lock (45%), isSystem (25%). Each ran by id and bulk over 1 to 3 rows with random values, plain and under strictReadonlyWrites, at base and at head: 3016 plain and 3016 strict runs per tree, plus 2 permutations per plain run at head. An independent oracle re-evaluated every caller-supplied field's predicate on the row the driver holds after the write (the FK's own lock against the header it names, #4889).

  • Locks opened: head 0, base 0.
  • Order: 6032 permutation runs at head, 0 differ in stored rows or reported drops.
  • Strict: the refusal went refuse to accept or accept to refuse in 0 of 3016 pairs; its fields moved in 20 (refused before and after).
  • Movements: 20 plain runs (12 hand-built, 8 random). 14 only release keys; 6 also drop a knock-on key. Every released key lands with its lock FALSE on the stored row; every knock-on key is locked on it. Two random runs (seed 413, by id and bulk) release a master-detail FK: the line moves to the header the update names, as in the settlement row below.
  • Exactness at head: 3002 of 3016 runs store a row on which every dropped key is locked and every kept key unlocked (base: 2982). The 14 others are unmoved from base: 13 have a cycle among record reads (4 hand-built: the two-exact-set pair and the no-exact-set cycle, by id and bulk); 1 (seed 371, bulk) has its only cycle through parent: the FK's lock reads record.f1 and f1's lock reads parent, which the FK decides. Enumerating its 4 sets of {inv, f1} by hand, none is exact, and the fail-safe drop stands.

A4: the FK settlement

The search applies there: both judgeFkLock closures call the same strips (only: fk), which call settleReadonlyWhenDrops, and a landing FK is re-judged by the strip that follows over the same header. Pinned by id, bulk and strict with line_cascade below. The rule for an FK that does not land is unchanged (its verdict is final, #19853), and the new docblock says the settlement's claims are about the views it is handed for that reason.

Every movement (base ae0c90c133 to head d9af551bb5)

Measured through the real engine at both trees. The new test file pins each row by id, on bulk and by id under strictReadonlyWrites, except the six-lock cascade (by id only); the card is also pinned under strictReadonlyWrites on bulk and for isSystem.

write before now
the card by id, bulk (2 rows) and isSystem every row { c: L, x: old, y: old }; event [c, x, y] every row { c: L, x: xv, y: old }; event [c, y]
the card under strictReadonlyWrites, by id and bulk refused, fields: [c, x, y], nothing lands refused, fields: [c, y], nothing lands
six-lock cascade (c by previous.c == 'L', each xN by the previous one being 'v', all set to 'v'), by id, bulk, isSystem all six dropped x1, x3, x5 land; event [c, x2, x4]
the same under strictReadonlyWrites fields: [c, x1, x2, x3, x4, x5] fields: [c, x2, x4]
KNOCK-ON: p by previous.p == 'L', m by record.p == 'L', j by record.m == 'new', k by record.p == 'L' && record.j == 'new'; all set to 'new' on { p: L, m: old, j: old, k: old }, by id, bulk, isSystem k: 'new' stored; event [p, m, j] j: 'new' stored, k kept old; event [p, m, k]
the same under strictReadonlyWrites refused, fields: [p, m, j] refused, fields: [p, m, k]
SETTLEMENT: c by previous.c == 'L', FK invoice by record.c == 'open', y by record.invoice == 'h_open', amt by parent.status == 'paid'; line { c: L, invoice: h_paid, y: old, amt: a0 }; update { c: open, invoice: h_open, y: yv, amt: a1 }, by id, bulk, isSystem line stays under h_paid, y: yv stored, amt stays a0; event [c, invoice, amt]; by-id header reads [h_open, h_paid] line moves to h_open, amt: a1 stored, y stays old; event [c, y]; by-id header reads [h_open, h_open] (the named header, then the repoint's reference check)
the same under strictReadonlyWrites refused, fields: [c, invoice, amt] refused, fields: [c, y]; the line stays under h_paid

Unmoved (pinned): the two-lock cycle with no exact set by id and bulk ({a, b} dropped); the pair with exact sets {a} and {b} ({a, b} dropped); the pair with exact sets {} and {a, b} (both land); only: y over the card's cascade. PR #19923's 35 pins, PR #19905's 18 and PR #19877's 38 are green.

Warn lines: a released key loses its drop line and a knock-on key gains one; the card by id now warns for c and y only.

Declaration note for the contract review

The claim carries Clause-②: no, copied above as given. No authorable key, export or error code moves; the function is not exported (index.ts and core.ts export neither it nor the strips).

  • Accept direction (a dropped edit now lands). content/docs/data-modeling/fields.mdx, "Conditional Logic": "The server enforces requiredWhen on submit and ignores writes to fields whose readonlyWhen predicate is TRUE", and the table row "CEL predicate; field is read-only when TRUE". content/docs/data-modeling/formulas.mdx binds record to "the row being evaluated". Every released key's predicate is FALSE on the row the update stores (checked on every moved run of A2 and A3), so the old drop is one the text negates. content/docs/kernel/contracts/data-engine.mdx lists the readonly_when strip as "A TRUE readonlyWhen predicate" and strictReadonlyWrites as "refuse instead of stripping": the old refusal named x, whose predicate was FALSE on the stored row.
  • Drop direction (the knock-on class: a written field is now ignored). On the row the update now stores, that field's predicate is TRUE, so ignoring it is what the same sentence prescribes. The base wrote it onto a different row, one where another field was dropped with its own predicate FALSE; only the new answer agrees with every field's lock on the row it stores. This moves a write to a drop and does not move any refusal.
  • Settlement. A master-detail repoint the chain used to hold now lands where its lock is FALSE on the stored row, and the other fields are then judged under the header it lands on (amt unlocked, y locked): the same "stays to moves" class PR fix(objectql): judge each readonlyWhen lock against the row the update stores, not a value another lock drops (#19911) #19923 declared, reached now through a chain.
  • strictReadonlyWrites. Whether a write is refused does not move: the answer is empty only when ①'s first pass locks nothing, exactly as before, and a set that gives back itself is never empty otherwise (0 of 3016 A3 pairs moved). Only fields / drops move.
  • Validation. requiredWhen and validation rules run on the stripped payload, as before, so a field that now lands or is now ignored reaches them as stored. Declared by class; no shape here carries a requiredWhen.
  • Cycles. Where no exact set exists the docs are silent and the fail-safe drop stands, as before (binding per the dispatch).

Tests

New packages/objectql/src/engine-readonly-when-exact-drop-set.test.ts, 19 cases: the card by id, bulk, strict by id, strict bulk and isSystem (stored rows, the one event, the warn lines, code ERR_READONLY_FIELD_REJECTED + fields + drops); the six-lock cascade; the knock-on by id, bulk and strict; the no-exact-set cycle by id and bulk; the two two-exact-set cycles; the settlement by id (with header reads), bulk and strict; three unit cases on stripReadonlyWhenFields (the cascade, only: x, only: y).

On 2c9cbe94eb (the patch round changed only the two changesets; the code is d9af551bb5's):

Gates on 2c9cbe94eb

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: the same 63 commands as at d9af551bb5, each run with its exit code captured before any pipe: 62 exit 0, and node scripts/check-empty-changeset.mjs --base origin/main exits 1 on the A record-scoped readonlyWhen reading a field that another readonlyWhen drops in the same pass is judged against the dropped value, so a closed row locked amount is rewritten #19911 note this PR corrects (the refusal's DELIBERATE CORRECTION class, declared below). --ran: 63 derived famil(ies) accounted for — 63 run, 0 NOT-MEASURED (a DERIVED zero — all 63 recorded an exit code and none of them is 3), exit 0. The three dist-reading gates ran after pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*' --concurrency=2 (72/72, all cached) and exit 0; git status stayed clean.
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0. pnpm check:nul-bytes: exit 0. node scripts/check-changeset-fixed.mjs: exit 0.
  • node scripts/check-issue-citations.mjs (live, the verdict CI blocks on): exit 0 at 2c9cbe94eb, 6 citations judged, all resolve. pnpm check:authz-resolver, pnpm check:filter-alias-parity and the narrowed eslint run below were measured at d9af551bb5; the patch round touched only the two changesets, which eslint's config ignores.
  • node scripts/check-system-context-census.mjs: exit 0. pnpm check:query-options-erasure: exit 0.
  • The three roster gates the derivation marks as keeping a roster under this diff's directories: node scripts/check-changeset-fixed.mjs, pnpm check:authz-resolver, pnpm check:filter-alias-parity, exit 0 each.
  • Lint, narrowed and proven. eslint's own config (ESLint.isPathIgnored / calculateConfigForFile) ignores the changeset and lints the three TypeScript files with typescript-eslint/parser and no parserOptions.project / projectService. eslint --no-inline-config --format json over them gives 3 results, 0 errors, 0 warnings. The config enables no type-aware linting, and its only file reads are two baselines this diff does not touch, so this diff cannot move a verdict on any untouched file. pnpm lint itself is CI's.

Neighbour PR #19728

Textually disjoint, not merged in, not waited on. Driver-free bare probe (git clone --bare --shared, no merge.* config): merge-tree --write-tree --name-only of 2c9cbe94eb (and before it d9af551bb5 and b5e3de9a35) against its head 3b9c5f2fca exits 0, and against origin/main fdeeea0cc9 exits 0. The merged tree carries both changes (that loop-bound marker 1 in rule-validator.ts; #19728's RelatedRecordBinding 5 there and 2 in engine.ts).

Acceptance notes

  • The pending A record-scoped readonlyWhen reading a field that another readonlyWhen drops in the same pass is judged against the dropped value, so a closed row locked amount is rewritten #19911 release note is corrected in place. .changeset/19911-readonlywhen-interdependent-locks.md is unreleased and compiles into the same CHANGELOG as this PR's entry. This PR made two of its passages false, so both are rewritten in that file and nothing else there moves: the "What happens now" sentences on the release (it now repeats round after round until the dropped fields are exactly the locked ones, and the first, larger set stands after one round more than the caller's readonlyWhen fields), and the residue bullet that said "The release is one step, not a search" with the three-lock cascade as its example. That bullet now says a field whose lock is FALSE on the stored row can still be dropped only where locks read each other in a cycle (a parent-scoped lock counting as a read of the master-detail field), gives the two cycle shapes this PR pins, and says some other updates with two agreeing drop sets store one of them. This PR's own entry no longer repeats what that entry states (the cycle residue, validation on the stripped update, the no-lock-opens rule) and no longer points at the old text. The seat chose this correction in the patch-round order (option A). node scripts/check-empty-changeset.mjs --base origin/main exits 1 on it by design: "Correcting a pending release note is a decision about a release rather than a refactor -- say so on the PR, naming the note and what changed under it, and get it confirmed." So Check Changeset stays red, skip-changeset is not applied, and the seat holds this PR's landing for the maintainer's confirmation.
  • Cycles. A cycle can have no exact set, one, or several. With none, the fail-safe drop stands; with several, the answer is the one the iteration reaches, or the fail-safe drop (measured: equal to base on all 64 such runs).
  • In-tree usage. PR fix(objectql): judge each readonlyWhen lock against the row the update stores, not a value another lock drops (#19911) #19923 scanned examples and platform objects and found no readonlyWhen reading another readonlyWhen field; this change moves nothing where no such chain exists (on every run where ① or its first release settled, head's answer and evaluation count equal base's).

Generated by Claude Code

…il the set gives back itself

The conditional strip's release step ran once. In a cascade of three
locks, releasing one key moved another's verdict, the check failed, and
the fixpoint's larger drop set stood: a key whose own lock was FALSE on
the stored row was dropped. The release now repeats (every key judged
against the previous set) until a set gives back itself, at most n + 1
sets; otherwise the fixpoint's fail-safe set stands as before.

Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 23, 2026
@github-actions

github-actions Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json fdeeea0cc9183f9c80343a552ef6523b1a53f99b → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5d1a90f6c93125b12b93c0d8853ef240f5cc5fd1 — the merge of head 2c9cbe94eb2dc223cfc01ff12cd7176a5590da62 into base fdeeea0cc9183f9c80343a552ef6523b1a53f99b, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 5d1a90f6c93125b12b93c0d8853ef240f5cc5fd1 && git checkout 5d1a90f6c93125b12b93c0d8853ef240f5cc5fd1
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fdeeea0cc9183f9c80343a552ef6523b1a53f99b 2c9cbe94eb2dc223cfc01ff12cd7176a5590da62 && git checkout -B drift-repro fdeeea0cc9183f9c80343a552ef6523b1a53f99b && git merge --no-ff 2c9cbe94eb2dc223cfc01ff12cd7176a5590da62

node scripts/docs-audit/affected-docs.mjs --json fdeeea0cc9183f9c80343a552ef6523b1a53f99b

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…elease; trim the #19927 entry

The #19911 entry described the release as one step and listed the
three-lock cascade as dropping all three fields; after the repeated
release neither holds. Its residue bullet now says what a caller can
still see: an over-lock only where locks read each other in a cycle.
The #19927 entry drops its pointer at the old text and the facts the
corrected #19911 entry already states, and states the knock-on drop and
the strict fields that move with it.

Claude-Session: https://claude.ai/code/session_01TEhopqrWQYBycZzyJHpAZr
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Confirmation needed: this PR corrects a pending release note

domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-23T23:45Z.

Check Changeset is red on purpose. scripts/check-empty-changeset.mjs classes this PR's edit as a DELIBERATE CORRECTION: "Correcting a pending release note is a decision about a release rather than a refactor -- say so on the PR, naming the note and what changed under it, and get it confirmed ... this gate stays red either way". Everything else is green (the seven required contexts included), and the contract review passed on this head (5804807706 on #19927).

The note: .changeset/19911-readonlywhen-interdependent-locks.md, from PR #19923 (#19911), merged and not yet released.

What changed under it: PR #19923 settled interdependent readonlyWhen locks with ONE release step. This PR makes that step iterate until the dropped fields are exactly the locked ones, capped at one round more than the caller's readonlyWhen fields, then the fail-safe drop set. Two passages of the note became false:

  • the "What happens now" sentences describing the release, which now describe the repeated release and its cap;
  • the bullet "The release is one step, not a search", whose three-lock example now saves x. It is replaced by the residue that remains: a field whose own lock is FALSE on the stored row can still be dropped only where locks read each other in a cycle, with the two cycle shapes this PR pins.

Every rewritten sentence was checked on measurement by the contract reviewer. The rest of that note is unchanged. Restoring the old text would publish sentences this PR makes false; leaving it and correcting it in this PR's own entry would ship a false entry beside its erratum.

The one action asked of the maintainer: confirm the in-place correction (a reply on this PR is enough). On confirmation the seat marks the PR ready and queues it, with Check Changeset red for exactly this reason. ⛔ No skip-changeset label is applied.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Confirmed by the maintainer — landing released

domain:engine#1, session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-24T01:29Z.

Whose confirmation, verbatim, where: the maintainer (huangyiirene), in this seat's session session_01TEhopqrWQYBycZzyJHpAZr (chat), answering the request above (5804825048): 「确认 #19928」.

That is the confirmation check-empty-changeset's DELIBERATE CORRECTION class asks for: the in-place correction of the pending .changeset/19911-readonlywhen-interdependent-locks.md stands. Check Changeset stays red for that reason alone; every other check on 2c9cbe94eb is green, the seven required contexts included. ⛔ No skip-changeset. The seat now marks this PR ready and arms auto-merge on this head.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 24, 2026 01:30
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 24, 2026
Merged via the queue into main with commit 0b866bf Sep 24, 2026
41 of 43 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19927-readonlywhen-exact-drop-set branch September 24, 2026 01:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A three-lock readonlyWhen cascade drops a write whose own lock is FALSE on the stored row: a legitimate edit is silently ignored

1 participant