Repository navigation
A readonlyWhen cycle anywhere in an update over-locks an unrelated acyclic chain in the same update: the fail-safe fallback is global, not per component #19929
Description
Activity
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsBlocked-by: #19927
定级
pm:blocked·priority:p3·bug·domain:engine—— 一处锁的环,会让同一次更新里无关的锁链也被整体多锁;排在在飞的 PR #19928 之后Path: records-forms
Triage: lands in
packages/objectql/src/validation/rule-validator.ts(settleReadonlyWhenDrops' fail-safe fallback, applied to the whole update rather than per connected component) ⇒domain:engine; rationale: a conditional-lock cycle anywhere in an update makes the server ignore a legitimate write to an unrelated acyclic chain whose own lock is FALSE on the stored row; fail-safe direction (no lock opens), and the reviewer's scan of the 51 files that declarereadonlyWhenfound no lock reading another lock's field, so no shipped metadata reaches it ⇒ p3; blocked because PR #19928 (#19927) is editing the same function.分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-24T00:20Z。本席读完了卡面(本卡尚无评论)。本席的读数(
origin/mainfdeeea0cc9)- PR fix(objectql): judge each readonlyWhen lock against the row the update stores, not a value another lock drops (#19911) #19923(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)已落地;PR fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) #19928(A three-lock readonlyWhen cascade drops a write whose own lock is FALSE on the stored row: a legitimate edit is silently ignored #19927)尚未落地。本卡要改的正是 fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) #19928 在改的函数。
- 两组锁同时出现在一次更新里的复现,是合约复核跑的,本席读的是卡面与代码路径。
判定
- 业务后果:一次更新里,如果有两个字段的锁互相引用(成环),那么同一次更新里另一组毫不相干、本来能正常保存的字段,也会被一起丢掉,没有提示。
p3:方向是「该写的没写」,⛔ 不是越权。仓库里现有的元数据都走不到这一步,只有作者写出「锁读另一把锁的字段」时才会碰到。- 修法方向(⛔ 不是裁定,卡面已写):按锁依赖图的连通分量分别求解,只对没有收敛的那个分量用安全兜底。
⚠️ 卡面提到: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 / A three-lock readonlyWhen cascade drops a write whose own lock is FALSE on the stored row: a legitimate edit is silently ignored #19927 待发布的 changeset 写的是「只在锁互相成环的地方」,这句话对整次更新成立,对单个字段不成立。接手时顺手把 changeset 的说法改准。Blocked-by: #19927(本条第一行,供解锁扫描):等 PR fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) #19928 落地再做,⛔ 不并入在飞的 PR。
Generated by Claude Code
- addedbugSomething isn't workingSomething isn't workingand removed
on Sep 24, 2026 objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsUnlocked →
pm:queuedomain:engine#1,session_01TEhopqrWQYBycZzyJHpAZr, written 2026-09-24T01:56Z.Blocked-by: #19927is satisfied: #19927 closed completed when PR #19928 landed (squash0b866bf191, verified onorigin/main).settleReadonlyWhenDropsonmainnow iterates the release. This card's residue, the fallback applying to the whole update rather than per component, is unchanged by that landing: the PR #19928 contract review measured it at the head (5804807706, flag ③-4). The premise stands on the merged ref.Returned to
pm:queuein the same act (label replace). ⛔ Not dispatched: this seat is standing down on the maintainer's order; the nextdomain:engineseat takes it by the full order. Serial note: PR #19728 (#18682, spec lane, draft) still editsrule-validator.ts; stay merge-tree disjoint.
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsClaim: PM loop round 22
Session:session_01Bvd69VPa6puiNzzPUroDBx
Branch:claude/issue-19929-readonlywhen-per-component-fallback
Worktree:objectstack-issue-19929
Domain:domain:engine
Seat:domain:engine#1
File surface:packages/objectql/src/validation/rule-validator.ts(settleReadonlyWhenDropsand its fallback),packages/objectql/src/engine.tsonly if the caller's settlement comment must follow, objectql tests,.changeset/19929-*.md, and the wording of the pending.changeset/19911-readonlywhen-interdependent-locks.md(triage's ask on this card). Stop on breach; explain in the report.
Container & model:M,mode:subagent,model: opus(dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
Clause-②: no
Thread-read: 5806084369
Serial constraints cleared: at 2026-09-24T12:55Z, the census of all 32 open PRs finds one on these files: draft PR #19728 (#18682, spec lane) editsrule-validator.tsandengine.tsin other regions (needsPriorRecord,evaluateRule,unevaluableRuleError; engine read paths). This card stays merge-tree disjoint from it at region level (settleReadonlyWhenDrops), never merges its branch in and never waits on it. #19629's claim (spec lane) namesrecord-validator.ts, a different file. The last landing on the function is PR #19928 (0b866bf191).
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 19929,
"status": "done",
"branch": "claude/issue-19929-readonlywhen-per-component-fallback",
"pr": "#19979",
"session": "session_01Bvd69VPa6puiNzzPUroDBx — this run's harness-stamped id (mode:subagent, the parent's id, as the claim 5814534750 names)",
"premise_still_valid": true,
"summary": "settleReadonlyWhenDrops (packages/objectql/src/validation/rule-validator.ts) now settles the judged keys group by group: the strongly connected components of 'the predicate of k reads j through record', taken in dependency order. A key in no cycle gets one judgement after the keys it reads. A cycle keeps the old fixpoint, release rounds and fail-safe fallback, bounded by the cycle's size and applied to that cycle's keys only. Reads come from the parseCelToAst AST: only a '.' or '.?' select on the bare record root names a field, and any other use of record, a non-CEL predicate or one that does not parse counts as reading every judged key. H1 and H2 hold on main 2c1011b: all five fields dropped in both card shapes (x stored 'old', strict refusal named c,x,y,a,b). H3 holds as a measured invariant. H4: engine.ts needed no change; its master-detail settlement now lands the FK beside a cycle with unchanged header reads. I chose SCC over the suggested connected components on measurement: over 16,500 random systems, CC left 87 over-locked keys outside any cycle, SCC left 0, and only SCC makes the 19911 per-field sentence true. The pending 19911 changeset's mechanism and cycle sentences were rewritten to match the shipped code. The 19927 changeset has no such sentence and is untouched. Neither was consumed by a release at base. The assignee was already set (os-sales) and I did not write it. Known delta, in PR acceptance notes: in 4 of 16,500 probe systems, a cycle with two agreeing sets that base settled by its whole-update trajectory now keeps its fail-safe drops (cycle keys only, fail-safe direction, no pin flipped).",
"tests": "Head 2d75a59:pnpm --filter @objectstack/objectql typecheckexit 0 (test layer 234 errors / 65 signatures held, unchanged);pnpm --filter @objectstack/objectql testTest Files 309 passed, Tests 5195 passed (lock VERDICT command-exit 0). The new engine-readonly-when-cycle-isolation.test.ts has 18 pins, all green at 2d75a59, and was 17/17 red on unchanged main code (H2 run). Every existing readonlyWhen suite is unchanged and green (exact-drop-set, interdependent-locks, parent, stored-view, derived-writes, rule-validator). H3 probe: a temporary vitest file, deleted and never committed, ran against the new strips and the BASE strips from 2c1011b over 16,500 random lock systems (12,000 single-row, 4,500 bulk, some with index reads) at rule-validator blob 87b0b5a7 (unchanged ebb8a76..2d75a59). 0 violations of (S) no kept key locked on the stored row, (E) every unlocked dropped key is in a cycle, (L) each verdict depends only on the keys it transitively reads, (D) declaration and payload order independence, (C) acyclic parity with base, (R) refused-or-not unchanged. Over-locked keys: base 664, CC 661, SCC 575; outside a cycle: 90 / 87 / 0. My first probe run applied the wrong own-value rule (a dropped key must be judged with its own incoming value), so it produced false E hits; I fixed the probe and discarded those readings. Ablation: fix committed first, then scripts/ablation-replace.mjs in WRAP mode changed 'every = true;' to 'every = false;' (anchor 1 to 0, blob 87b0b5a7 to 9b744861). Exactly the 2 index_read pins went red, with w stored 'wv' while its lock is TRUE on the stored row, so a lock opens. Restore proven: blob == HEAD 87b0b5a7 and git diff HEAD empty. The subject resolves through the relative ./validation/rule-validator.js to src, not dist, so no dist preflight applies. eslint --no-inline-config on the 2 changed .ts files: 2 files, 0 errors, 0 warnings. The config has no parserOptions.project (no type-aware lint), so no untouched file's verdict can move; the .md changesets are outside the lint files globs. objectql build: ESM, CJS and DTS succeeded, and the CJS entry loads (exit 0, 176 exports).",
"mcp_calls": "0 — no MCP GitHub tool was called",
"api_writes": "2 — both through the fleet-write relay as objectstack-fleet[bot]: (1) POST /repos/objectstack-ai/objectstack/dispatches executing pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, PR 19979, run 36010065844 success); (2) POST /repos/objectstack-ai/objectstack/dispatches executing comment, POST /repos//issues/19929/comments (this report). No label write: the dispatch names no label, and a publishing diff carries a changeset, so skip-changeset does not apply. The 5 git pushes are not REST writes.",
"open_questions": [
{
"question": "check-empty-changeset --base origin/main is red by design (exit 1, DELIBERATE CORRECTION class): this PR rewrites the pending .changeset/19911-readonlywhen-interdependent-locks.md, whose mechanism paragraph and cycle bullet this change made false. The gate prescribes a maintainer's confirmation on the PR, and it stays red until then.",
"options": [
"A: the maintainer confirms the correction on PR 19979. The gate stays red by design and the note ships true.",
"B: restore 19911's text from base. The gate goes green but republishes sentences that are false for the shipped code (a whole-update bound and fallback)."
],
"recommendation": "A. The 19911 note ships in the same release as this change, and B would publish a false description of the lock settlement to the CHANGELOG an upgrading author greps."
}
],
"out_of_scope_findings": [
"class: none (documentation incompleteness, not a filing class) · content/docs/data-modeling/fields.mdx never mentions that a field inside a readonlyWhen cycle can be over-locked in the fail-safe direction · carrier: none (承接者:无) · noted in PR acceptance notes only, not filed",
"class: none (observation) · the onFieldsDropped event lists fields in payload key order (reportDroppedFields walks the payload), not declaration order · pre-existing and consistent · carrier: none (承接者:无) · noted only"
],
"gates": "Derived: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 2d75a59 gave 63 commands (4 paths). Run: those 63 plus 2 named only in the dispatch list (check:stack-collection-maps, check:swallow-census-controls) = 65. --ran reconcile: 63 derived, 61 run, 2 NOT-MEASURED, 0 UNRUN, exit 0. Exit 0: 62, including check-engine-split-ratio --days 90, which exited 2 on the shallow clone and 0 after git fetch --shallow-since=2026-06-19 origin main. Exit 3 (PREREQUISITE NOT MET, NOT MEASURED, because it needs every workspace package's dist): check:dual-build-cjs-loads, check:type-check-debt. Exit 1, red by design: check-empty-changeset --base origin/main (the 19911 correction, open question 1). CI-owned and NOT MEASURED locally: Test Core, Dogfood, Build Core and the workspace type-check lanes. Serial neighbour: git merge-tree --write-tree --name-only of 2d75a59 against PR 19728's head 3b9c5f2, from a driverless bare clone, exit 0, tree 80a7d59f, no conflicted paths (merge-base 6eaa0f4).",
"line_budget": "n/a — no skills/** in the diff",
"deviations": [
"Route: SCC dependency-order settlement instead of the suggested connected components. That is the suggested-route zone, and the measurement (table in the PR) decided it.",
"H3's phrase 'never opens a lock the whole-update settlement keeps closed' is not literally true, and is not the intended fix: x IS kept closed by the whole-update settlement and now lands, because its lock is FALSE on the stored row. What I measured is the invariant the card's contract states: no kept key is locked on the row the update stores (0 violations).",
"The harness attribution reminder asked for a model-named Co-Authored-By trailer and a claude.com footer. I used AGENTS.md's model-free pair (Claude-Session plus Co-authored-by: Claude) and the session-URL PR footer, as AGENTS.md takes precedence.",
"The probe and a copy of the base rule-validator lived temporarily in the worktree as untracked files. Both were deleted before the gate derivation and never committed.",
"git fetch --shallow-since=2026-06-19 origin main deepened the shared object store so check-engine-split-ratio could measure. It moves no ref anyone else owns."
],
"files_changed": [
"packages/objectql/src/validation/rule-validator.ts",
"packages/objectql/src/engine-readonly-when-cycle-isolation.test.ts",
".changeset/19929-readonlywhen-cycle-isolation.md",
".changeset/19911-readonlywhen-interdependent-locks.md"
],
"cleanup": "Worktree removed (node_modules deleted first; git worktree remove exit 0, no --force), the temporary ref refs/issue-19929/pr19728 deleted, and no process was left running."
}
Generated by Claude Code
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsACCEPT: PR #19979 at
2d75a593domain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T14:48Z. Reviewed againstreferences/review-checklist.md, on GitHub rather than the dev's report (os-dev-reporton this card).- Shape: draft, base
main, first lineFixes #19929,Clause-②: nocitingfields.mdx. The only closing keyword names this card. - Scope: 4 files, +706/−65.
rule-validator.ts(settleReadonlyWhenDrops, SCC grouping), a new test file, the new.changeset/19929-*.md, and the triage-requested correction of the pending.changeset/19911-*.md.engine.tsis untouched, and so iscontent/docs/releases/. Not governed (check-governed-merges --pr 19979: 0 of 4 paths, 771 lines). - Contract review of record: PASS, 5816146511, same head, at-tier and isolated. The reviewer's own probe ran 11,885 random lock systems: 0 locks opened, 0 unlocked drops outside a cycle, 0 order dependence, and acyclic parity with base. It judged each rewritten sentence of the corrected 19911 note.
- CI at this head: every required context is
success. The skips areConsole Pin Gate,Build DocsandPacked-tarball smoke (opt-in), all roster skips. The one red isCheck Changeset: the DELIBERATE CORRECTION class, confirmed per ruling 1A (Why three engine-lane landings needed the maintainer this round (a pending release-note correction, a first-time queue-flake signature, a subagent's denied label write): can each become seat-decidable? #19940, 5814546887) under the three-condition red-by-design path recorded on the PR (5816155567). - Serial neighbour:
git merge-treeagainst draft PR feat(formula,objectql): read one hop through a lookup in a validation predicate #19728's head is clean (dev reading). - Deviation accepted: strongly connected components instead of the suggested connected components, decided by measurement (0 versus 87 over-locked keys outside a cycle). Known delta: in 4 of 16,500 probe systems a two-agreeing-set cycle keeps its fail-safe drops. That is cycle keys only, and stated in both changesets.
- Out-of-scope observations:
fields.mdxis silent on cycle over-locking: Acceptance notes; dropped as a card (no filing class, a docs gap).onFieldsDroppedpayload-key order: Acceptance notes; dropped (pre-existing, now pinned).
Landing:
ready+ auto-merge through the queue.
Generated by Claude Code
- Shape: draft, base
objectstack-fleet commented
on Sep 24, 2026 ContributorAuthorMore actionsLanded: PR #19979, verified on
maindomain:engine#1,session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T15:09Z.- Merged through the merge queue as squash
c8399867b8: one parent, an ancestor oforigin/mainafter a fresh fetch.readonlyWhenLockGroupshas 4 hits inrule-validator.tsat the squash and 0 at its parent. - This card closed
completedviaFixes #19929.pm:dispatchedwas removed in the same act; the assignee stays as the record of who carried it. - No other lane card closed with this landing: the only
domain:enginecard closed since 14:00Z is this one. - The pending
.changeset/19911-readonlywhen-interdependent-locks.mdnow carries the corrected per-cycle sentences, confirmed under ruling 1A (PR comments 5816146511 and 5816155567).
Generated by Claude Code
- Merged through the merge queue as squash
- added a commit that references this issue
on Sep 28, 2026
Filing gate: ① a defect with a named landing site, the fallback of
settleReadonlyWhenDropsinpackages/objectql/src/validation/rule-validator.ts, reached fromObjectQL.update(). Finding class (a). A residue of the #19927 class that PR #19928 does not close: present before and after it, unmoved.Filed by the
domain:engineexecution seat 1 (session_01TEhopqrWQYBycZzyJHpAZr) from its isolated contract review of PR #19928 (record 5804807706 on #19927, flag ③-4: "the fallback is global ... FILE SEPARATELY if a component-wise settlement is wanted; not blocking"). ⛔ Filed bare: routing and grading are triage's. ⛔ Not a claim.What happens
settleReadonlyWhenDropsiterates a release of dropped keys until the drop set is exact. When it does not settle within its bound, it falls back to the fixpoint's larger, fail-safe set, and it does so for the WHOLE update.One update touches two independent groups of conditional locks:
c: previous.c == 'L',x: record.c == 'open',y: record.x == 'xv'on a row withc: 'L'. Alone, PR fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) #19928 settles it at{c, y}, andxlands;a: record.b == 'x',b: record.a == 'old_a'on a row{ a: 'old_a', b: 'y' }, witha: 'new_a', b: 'x'in the same update.The reviewer measured that the update drops all five:
xincluded, although{c, y}settles the chain on its own andx's lock is FALSE on the stored row. The same happens with a{a}/{b}two-exact-set cycle beside the chain: exact sets{a, c, y}and{b, c, y}exist, and all five are dropped.Why it is a defect
content/docs/data-modeling/fields.mdx: "ignores writes to fields whosereadonlyWhenpredicate isTRUE".x's predicate is FALSE on the stored row, andxis in no cycle; a cycle among OTHER fields makes the server ignore it. It is the fail-safe direction (no lock opens), but a caller's legitimate edit is silently lost, and understrictReadonlyWritesthe refusal namesx.The pending
@objectstack/objectqlchangeset entries for #19911 / #19927 describe the residue as occurring "only where locks read each other in a cycle". That holds for the update as a whole, not per field, so a reader could take it as sparingx.Reach
An object whose update touches both a conditional-lock chain and an unrelated conditional-lock cycle in one write. The reviewer's in-tree scan of the 51 files that declare
readonlyWhenfound no lock reading another lock's field, so no shipped metadata reaches it today. Authored metadata can.Suggested shape (⛔ not a ruling)
Settle each connected component of the lock-dependency graph independently (with
parent-scoped locks counted as reading the master-detail field), and apply the fail-safe fallback only to the component that does not settle. The taker measures that it never opens a lock and stays deterministic, with the same probe style as PR #19923 / PR #19928. Land after PR #19928 (same function).Filing-gate answers
packages/objectqlafter triage routes it (domain:engine).closedincluded:readonlyWhen unrelated cycle same update over-locks acyclic cascade component-wise settlement fallback global→ 5 hits: A three-lock readonlyWhen cascade drops a write whose own lock is FALSE on the stored row: a legitimate edit is silently ignored #19927 (the parent, closed by PR fix(objectql): settle a chain of readonlyWhen locks on the drop set the stored row agrees with (#19927) #19928 for the chain alone), 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, A stripped master-detail repoint unlocks a parent-scoped readonlyWhen: a by-id update judges the lock against the parent it names, not the parent it stores #19853, [objectql] TRUE readonlyWhen strips beforeUpdate-derived values too — a conditionally-locked derived field has no server-side write path at all (isSystem included), unlike static readonly's hook-stamp protection #9107 (closed, sibling strip behaviours), claimSeedOwnership writes up to 20k single-id system updates in a loop, so per-record sharing materialisation cannot batch them #14530 (unrelated). None is this defect.Dedupe words:
readonlyWhen cycle over-locks unrelated chain·settleReadonlyWhenDrops fallback global component·readonlyWhen per-component settlementGenerated by Claude Code