Skip to content

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

@objectstack-fleet

Filing gate: ① a defect with a named landing site, the fallback of settleReadonlyWhenDrops in packages/objectql/src/validation/rule-validator.ts, reached from ObjectQL.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:engine execution 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

settleReadonlyWhenDrops iterates 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:

The reviewer measured that the update drops all five: x included, although {c, y} settles the chain on its own and x'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 whose readonlyWhen predicate is TRUE". x's predicate is FALSE on the stored row, and x is 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 under strictReadonlyWrites the refusal names x.

The pending @objectstack/objectql changeset 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 sparing x.

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 readonlyWhen found 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

Dedupe words: readonlyWhen cycle over-locks unrelated chain · settleReadonlyWhenDrops fallback global component · readonlyWhen per-component settlement


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Blocked-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 declare readonlyWhen found 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/main fdeeea0cc9)

    判定


    Generated by Claude Code

  2. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Unlocked → pm:queue

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

    Blocked-by: #19927 is satisfied: #19927 closed completed when PR #19928 landed (squash 0b866bf191, verified on origin/main). settleReadonlyWhenDrops on main now 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:queue in the same act (label replace). ⛔ Not dispatched: this seat is standing down on the maintainer's order; the next domain:engine seat takes it by the full order. Serial note: PR #19728 (#18682, spec lane, draft) still edits rule-validator.ts; stay merge-tree disjoint.


    Generated by Claude Code

  3. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: 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 (settleReadonlyWhenDrops and its fallback), packages/objectql/src/engine.ts only 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) edits rule-validator.ts and engine.ts in 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) names record-validator.ts, a different file. The last landing on the function is PR #19928 (0b866bf191).


    Generated by Claude Code

  4. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    os-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 typecheck exit 0 (test layer 234 errors / 65 signatures held, unchanged); pnpm --filter @objectstack/objectql test Test 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

  5. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR #19979 at 2d75a593

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T14:48Z. Reviewed against references/review-checklist.md, on GitHub rather than the dev's report (os-dev-report on this card).

    • Shape: draft, base main, first line Fixes #19929, Clause-②: no citing fields.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.ts is untouched, and so is content/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 are Console Pin Gate, Build Docs and Packed-tarball smoke (opt-in), all roster skips. The one red is Check 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-tree against 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.mdx is silent on cycle over-locking: Acceptance notes; dropped as a card (no filing class, a docs gap).
      • onFieldsDropped payload-key order: Acceptance notes; dropped (pre-existing, now pinned).

    Landing: ready + auto-merge through the queue.


    Generated by Claude Code

  6. objectstack-fleet commented on Sep 24, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #19979, verified on main

    domain:engine#1, session_01Bvd69VPa6puiNzzPUroDBx, written 2026-09-24T15:09Z.

    • Merged through the merge queue as squash c8399867b8: one parent, an ancestor of origin/main after a fresh fetch. readonlyWhenLockGroups has 4 hits in rule-validator.ts at the squash and 0 at its parent.
    • This card closed completed via Fixes #19929. pm:dispatched was 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:engine card closed since 14:00Z is this one.
    • The pending .changeset/19911-readonlywhen-interdependent-locks.md now carries the corrected per-cycle sentences, confirmed under ruling 1A (PR comments 5816146511 and 5816155567).

    Generated by Claude Code

  7. added a commit that references this issue on Sep 28, 2026
    c839986
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions