You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
console: a non-participant reader is offered inline edit on a record whose pending approval locks it (lockRecord: true), and the save answers 409 RECORD_LOCKED #25934
A member can read a record that has a pending lockRecord: true approval, but is not on the request's slate, did not submit it, and holds no override. On that record's page the console offers inline edit. The save is then refused with 409 RECORD_LOCKED. The admin sees the same record correctly locked: no inline edit, Edit greyed, and the "Locked for approval" band. So the lock is drawn only for viewers the approvals door answers with a row.
Measured on main77392e9 with the console at pin 5fef032b1ada (built from f0fdd45b; see #25478). Reproduced on 3 fresh loads. The resolver below is unchanged at objectui origin/main3a73c07.
Reproduction
On a stock showcase boot, as admin, mint a contributor member: POST /api/v1/auth/organization/invite-member {"email":E,"role":"member"}, then POST /api/v1/auth/sign-up/email as E, then POST /api/v1/data/sys_user_position {"user_id":U,"position":"contributor","organization_id":ORG}.
As admin, PATCH /api/v1/data/showcase_project/ID {"budget":600000}. This opens showcase_budget_approval at manager_review.
As admin, POST /api/v1/approvals/requests/MGR/approve. The run moves to exec_review, and the new request row reads lock_record: true.
As the member, sign in through the console form and open /_console/apps/showcase_app/showcase_project/record/ID.
Expected: no inline-edit affordance (the record is locked). Actual: Edit is enabled, 12 fields show "Double-click to edit", and there is no lock band. Saving Health sends PATCH /api/v1/data/showcase_project/ID {"health":"yellow"} and gets 409 RECORD_LOCKED.
The member's own GET /api/v1/approvals/requests?object=showcase_project&recordId=ID answers 200 {"data":[]}.
Mechanism
objectui packages/react/src/context/InlineEditContext.tsxresolveApprovalLock (rule 2, objectui#12220) reads an answered approvals read with no open request as "no approval running":
if (openApproval) return { pending: true, locked: openApproval.locked };
if (readAnswered) return { pending: false, locked: false };
The approvals door serves a request only to its participants and the override actor (plugin-approvals request read gate; the record-reader tier is off on stock). So for a non-participant, an empty read does not mean "no approval running". The console unlocks the record, and the server then refuses the write. Nothing the non-participant can read tells them the record is locked.
The fix is a decision. One option is a lock signal a record reader can see without seeing the request. The other is the console refusing to infer "unlocked" from an empty read on an approval-enabled object. Either way the console stops offering an edit the server will refuse (objectui#2902's mislabel, for this persona).
Ruling: batch #401 item 5 · letter A (readers) · maintainer 「同意」 2026-10-26T17:31Z
Recorded by the director seat (summon #51 continuation, session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #401 with 「同意」 — every seat letter. Brief 6420330739; the domain:ui seat 2 analysis 6419712936 is the ruling's substance. Letter A, a server lock signal for record readers, with the three execution parameters settled in this stroke: (a) carrier — a new record-anchored read on IApprovalService plus a REST route; (b) disclosure — every caller who can read the record (the #8652 anchor, engine.find as the caller), not writers only; (c) payload — { locked: boolean } only, no request id, node, participants or pending. The lock judgement reuses lifecycle-hooks.ts's lockRecord !== false reading, never a second copy. The console feeds the signal into resolveApprovalLock as its lock input; rule 2's semantics for a composition without an approval service stay unchanged (no service → no signal → not locked), so objectui#12220's four tests do not flip; a non-participant regression test is added. #8652 ruling 5299823744 (request content visible to readers only when approvalsVisibleToReaders is on) is untouched. Not B (locks every non-participant on every approval-enabled object), not C (exposes the approval history to fix an affordance), not D (consumer-side tolerance).
Workload of the chosen letter (card column): domain:services 1 PR (precedents #3815 +108/−1, #22660 +604/−30) → domain:ui 1 PR after PR #26053 (same files) lands.
Execution: needs-user-decision → pm:queue in this act (domain:ui, p3, area:workflow). The claiming seat files the domain:services protocol card (IApprovalService method + plugin-approvals implementation + route + integration tests for non-participant / read-only reader / non-reader — the last refused or empty, never leaking existence; changeset Clause-②: yes (widening), Metadata-protocol: none) and moves this card to pm:blocked with Blocked-by: it; the ui dispatch carries the premise re-checks 1–3, 6, 7, 9 of 6419712936 and reports a fork if any fails — ⛔ no retreat to B or D. Ruling-ref: names this comment.
Carrying out ruling 6422939157 (letter A, readers): the domain:services protocol card is filed as #26314. It covers the record-anchored { locked: boolean } read on IApprovalService, the plugin-approvals implementation, the REST route, and integration tests for a non-participant, a read-only reader and a non-reader (refused or empty, never leaking existence). It declares Clause-②: yes (widening) and Metadata-protocol: none, and reuses the lockRecord !== false reading.
This card moves pm:queue → pm:blocked. When #26314 lands, the domain:ui dispatch feeds the signal into resolveApprovalLock, after PR #26053 (same files). The dispatch carries premise re-checks 1–3, 6, 7 and 9 of 6419712936 and reports a fork if any fails. ⛔ No retreat to B or D.
2026-10-27T00:20Z · domain:server seat 2 · ⛔ Not a claim. The blocker has cleared: #26314 landed as PR #26361 (merge 6a2866af71), so GET /api/v1/approvals/records/:object/:recordId/lock and client.approvals.getRecordLock(object, recordId) are on main, answering { locked } to every caller who can read the record. This card moves from pm:blocked to pm:queue for the domain:ui lane.
Claim: PM loop round 1
Session: session_017iXjUm1rujwDk7f3KT1Pkk (seat); implementing card thread session_01ADHkmQYqTcUW7GgPkfWCzw
Account: os-steve
Branch: claude/project-thread-i81sbn (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: the card thread's own container
Domain: domain:ui
Seat: domain:ui#1
File surface: ui/packages/app-shell/src/hooks/useRecordApprovals.ts, ui/packages/app-shell/src/views/RecordDetailView.tsx (the lock input only), ui/packages/react/src/context/InlineEditContext.tsx (resolveApprovalLock / ApprovalLockInput, if the rule needs a new input), ui/packages/plugin-detail/src/DetailView.tsx only if the band computes its own lock input, their tests (including a new non-participant regression test; the four objectui#12220 tests must not flip), re-recorded sdui records for touched registry packages, and a root changeset. Out of bounds: ObjectView.tsx / objectView*.ts (#25992), the reaction remove path (#26346), and any server or packages/ change. Ruling 6422939157 letter A (readers); ⛔ no retreat to B or D (stop on breach; explain in the report). ⛔ ui/.github/** and the root .github/workflows/ci.yml are out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root .changeset/.
Container & model: small, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: yes (widening), provisional: ApprovalLockInput (exported from @object-ui/react) is expected to gain an optional server-lock input; re-declared no at hand-back if no exported type changes
Metadata-protocol: none
Responsibility: n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public Console
Thread-read: 6430539374
Premise: holds (read-only premise check at origin/main6390fe5, 2026-10-27T01:18Z: #26314 closed via PR #26361, which adds client.approvals.getRecordLock → { locked }; PR #26053 merged; RecordDetailView.tsx still feeds resolveApprovalLock only from the request-list read; spot-checked by the seat. The dev re-runs premises 1–3, 6, 7 and 9 of 6419712936 before any edit)
Serial constraints cleared: RecordDetailView.tsx lands after #26346 (seat 1, p2); react.json after seat 2's #26326, then #26363; plugin-detail.json (if touched) after seat 2's #26325. The seat sets the turn
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
Claim: PM loop round 1
Session: session_017iXjUm1rujwDk7f3KT1Pkk (seat); implementing card thread session_01ADHkmQYqTcUW7GgPkfWCzw
Account: os-steve
Branch: claude/project-thread-i81sbn (platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)
Worktree: the card thread's own container
Domain: domain:ui
Seat: domain:ui#1
File surface: ui/packages/app-shell/src/hooks/useRecordApprovals.ts, ui/packages/app-shell/src/views/RecordDetailView.tsx (the lock input only), ui/packages/react/src/context/InlineEditContext.tsx (resolveApprovalLock / ApprovalLockInput, if the rule needs a new input), ui/packages/plugin-detail/src/DetailView.tsx only if the band computes its own lock input, their tests (including a new non-participant regression test; the four objectui#12220 tests must not flip), re-recorded sdui records for touched registry packages, and a root changeset. Out of bounds: ObjectView.tsx / objectView*.ts (#25992), the reaction remove path (#26346), and any server or packages/ change. Ruling 6422939157 letter A (readers); ⛔ no retreat to B or D (stop on breach; explain in the report). ⛔ ui/.github/** and the root .github/workflows/ci.yml are out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root .changeset/.
Container & model: small, mode:thread, model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate), effort: medium
Clause-②: yes (widening), provisional: ApprovalLockInput (exported from @object-ui/react) is expected to gain an optional server-lock input; re-declared no at hand-back if no exported type changes
Metadata-protocol: none
Responsibility: n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public Console
Thread-read: 6430539374
Premise: holds (read-only premise check at origin/main6390fe5, 2026-10-27T01:19Z: #26314 closed via PR #26361, which adds client.approvals.getRecordLock → { locked }; PR #26053 merged; RecordDetailView.tsx still feeds resolveApprovalLock only from the request-list read; spot-checked by the seat. The dev re-runs premises 1–3, 6, 7 and 9 of 6419712936 before any edit)
Serial constraints cleared: RecordDetailView.tsx lands after #26346 (seat 1, p2); react.json after seat 2's #26326, then #26363; plugin-detail.json (if touched) after seat 2's #26325. The seat sets the turn
Dispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
{
"issue": 25934,
"status": "done",
"branch": "claude/project-thread-i81sbn",
"pr": "https://github.com/objectstack-ai/objectstack/pull/26402",
"head": "fca25b0b5397d5e33d5c9ac74c50519930abd70c",
"session": "session_01ADHkmQYqTcUW7GgPkfWCzw",
"premise_still_valid": true,
"premises": "re-run on origin/main 61c07986 before any edit: P1 hold (InlineEditContext.tsx:107); P2 hold (RecordDetailView.tsx:1664, DetailView.tsx:1716; expected :1658/:1716, line drift only); P3 hold (useRecordApprovals.ts:480 is the only lock source; :326/:385 comments); P6 hold (lifecycle-hooks.ts beforeUpdate :538, lockedError(recordLockRefusal :633, refusal text :294; expected :487/:582/:294, drift only); P7 hold (approval-service.ts:336 lock_record?: boolean); P9 serial condition met: PR #26053 merged (054652bf), and the analysis expected it open. Blocker #26314 closed via PR #26361 (6a2866af): client.approvals.getRecordLock at packages/client/src/index.ts:6270, route at packages/rest/src/approvals-routes.ts:352 (501 without the service).",
"summary": "useRecordApprovals (app-shell) reads client.approvals.getRecordLock through the mounted adapter's client when the list read finds no pending request, and exposes recordLock. RecordDetailView feeds a locked answer to resolveApprovalLock as its openApproval: that is rule 1's input exactly, so ApprovalLockInput gains nothing. For a signal-only lock it threads approvalIsSubmitter false, so the band draws no recall. No service means 501, no signal and no open approval, so rule 2 and the four objectui#12220 tests are unchanged. DetailView.tsx is untouched: the band computes from the host's threaded locked/approvalPending/approvalIsSubmitter, it does not compute its own lock input. react and plugin-detail are untouched. Clause-② re-declared no: the only type that gains a member is the hook's non-exported result interface, and the hook is not re-exported from the app-shell entry (src/index.ts, measured on the source entry; the app-shell dist was not built).",
"tests": "Final head fca25b0b, one locked run: turbo build app-shell^... 64/64, then pnpm --filter @object-ui/app-shell type-check exit 0, then 4 files (new RecordDetailView.approvalLock-25934, RecordDetailView.approvalLock-12220, useRecordApprovals.answered-12220, RecordDetailView.approvalRecallGate): Test Files 4/4, Tests 25/25, VERDICT command-exit 0. Pre-merge e0f9e560: 82 neighbour files (all RecordDetailView.*, useRecordApprovals*, RecordApprovalsPanel*, AppContent.recordSaveToastNamesRecord-26172): 82/82 files, 551/551 tests, VERDICT 0. Narrowed by name; the full app-shell suite is left to CI Test UI. Ablation: with the fix committed, both source files were restored to base 61c07986 (on disk getRecordLock 3->0, lockedForReader 3->0). The non-participant case went red at :235 (expected 'false' to be 'true', locked); both lock-read cases went red because getRecordLock was never called; the participant control stayed green: 3 failed / 1 passed, VERDICT command-exit 1. Restored by git checkout HEAD --: git diff HEAD empty, blob hashes equal HEAD. Tests import src directly, so no dist leg applies.",
"gates": "fca25b0b: dispatch-gates --commands derived 35; all 35 exit 0, check:type-check-debt included (run with no other locked run; re-measure OK, 0 raw errors); --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Roster gate under .changeset: check-changeset-fixed.mjs exit 0. All other families (Lint & Repo Gates, Type Check UI, Test UI, Build & E2E UI, ...): PR #26402 check-runs, in_progress at report time.",
"line_budget": "+373/-7 over 4 files: source +81/-7 (useRecordApprovals.ts +65/-5, RecordDetailView.tsx +16/-2), test +278, changeset +14",
"files_changed": [
".changeset/25934-reader-record-lock.md",
"ui/packages/app-shell/src/hooks/useRecordApprovals.ts",
"ui/packages/app-shell/src/views/RecordDetailView.tsx",
"ui/packages/app-shell/src/views/RecordDetailView.approvalLock-25934.test.tsx"
],
"deviations": [
"Clause-② re-declared from the claim's provisional yes (widening) to no, in the PR body and the changeset, per the delta's own re-declare clause: no exported type changes.",
"RecordDetailView.tsx edit includes the approvalIsSubmitter line (:1691 area), next to the lock region: a signal-only lock must not light the band's recall lever. The reaction remove path (#26346) is untouched.",
"The PR body starts with the harness attribution comment and project-thread line, and Fixes #25934 is line 3 (os-dev: the harness line is kept and reported)."
],
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0 — no mcp__github__ tool calls (transcript grep count 0); reads via gh api REST GET",
"api_writes": "3 relay REST writes + 3 git pushes. Relay: pr_create, 2 requests in 1 action (POST /repos/objectstack-ai/objectstack/pulls draft, POST /issues/26402/assignees os-steve; read-back identical 7942 bytes); this os-dev-report comment (POST /issues/25934/comments via post-stamped). Pushes: branch create, changeset, merge of origin/main.",
"open_questions": [
{
"question": "Should resolveApprovalLock's openApproval doc in @object-ui/react name the server record lock as a host input, alongside the pending request?",
"options": [
"A leave it: the rule and its input are unchanged, and the call site in RecordDetailView documents the feed",
"B add one sentence to ApprovalLockInput.openApproval, which costs a react package change and its serial line (#26326, #26363)"
],
"recommendation": "A: the signal is rule 1's input as written, and A keeps this PR off the react line."
}
],
"out_of_scope_findings": []
}
Activity
objectstack-fleet commentedon Oct 26, 2026
Ruling: batch #401 item 5 · letter A (readers) · maintainer 「同意」 2026-10-26T17:31Z
Recorded by the director seat (summon #51 continuation,
session_01KrKXQVvgYk2AJ4VkqMobyb, ledger objectstack#12708). The maintainer answered batch #401 with 「同意」 — every seat letter. Brief 6420330739; thedomain:uiseat 2 analysis 6419712936 is the ruling's substance. Letter A, a server lock signal for record readers, with the three execution parameters settled in this stroke: (a) carrier — a new record-anchored read onIApprovalServiceplus a REST route; (b) disclosure — every caller who can read the record (the #8652 anchor,engine.findas the caller), not writers only; (c) payload —{ locked: boolean }only, no request id, node, participants orpending. The lock judgement reuseslifecycle-hooks.ts'slockRecord !== falsereading, never a second copy. The console feeds the signal intoresolveApprovalLockas its lock input; rule 2's semantics for a composition without an approval service stay unchanged (no service → no signal → not locked), so objectui#12220's four tests do not flip; a non-participant regression test is added. #8652 ruling 5299823744 (request content visible to readers only whenapprovalsVisibleToReadersis on) is untouched. Not B (locks every non-participant on every approval-enabled object), not C (exposes the approval history to fix an affordance), not D (consumer-side tolerance).Workload of the chosen letter (card column):
domain:services1 PR (precedents #3815 +108/−1, #22660 +604/−30) →domain:ui1 PR after PR #26053 (same files) lands.Execution:
needs-user-decision→pm:queuein this act (domain:ui, p3,area:workflow). The claiming seat files thedomain:servicesprotocol card (IApprovalServicemethod +plugin-approvalsimplementation + route + integration tests for non-participant / read-only reader / non-reader — the last refused or empty, never leaking existence; changesetClause-②: yes (widening),Metadata-protocol: none) and moves this card topm:blockedwithBlocked-by:it; the ui dispatch carries the premise re-checks 1–3, 6, 7, 9 of 6419712936 and reports a fork if any fails — ⛔ no retreat to B or D.Ruling-ref:names this comment.objectstack-fleet commentedon Oct 26, 2026
席位核对 · Blocked-by · seat
domain:ui#1· 2026-10-26T20:32ZBlocked-by: #26314
Carrying out ruling 6422939157 (letter A, readers): the
domain:servicesprotocol card is filed as #26314. It covers the record-anchored{ locked: boolean }read onIApprovalService, theplugin-approvalsimplementation, the REST route, and integration tests for a non-participant, a read-only reader and a non-reader (refused or empty, never leaking existence). It declaresClause-②: yes (widening)andMetadata-protocol: none, and reuses thelockRecord !== falsereading.This card moves
pm:queue→pm:blocked. When #26314 lands, thedomain:uidispatch feeds the signal intoresolveApprovalLock, after PR #26053 (same files). The dispatch carries premise re-checks 1–3, 6, 7 and 9 of 6419712936 and reports a fork if any fails. ⛔ No retreat to B or D.objectstack-fleet commentedon Oct 27, 2026
2026-10-27T00:20Z ·
domain:serverseat 2 · ⛔ Not a claim. The blocker has cleared: #26314 landed as PR #26361 (merge6a2866af71), soGET /api/v1/approvals/records/:object/:recordId/lockandclient.approvals.getRecordLock(object, recordId)are onmain, answering{ locked }to every caller who can read the record. This card moves frompm:blockedtopm:queuefor thedomain:uilane.Generated by Claude Code
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_017iXjUm1rujwDk7f3KT1Pkk(seat); implementing card threadsession_01ADHkmQYqTcUW7GgPkfWCzwAccount:
os-steveBranch:
claude/project-thread-i81sbn(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree: the card thread's own container
Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/hooks/useRecordApprovals.ts,ui/packages/app-shell/src/views/RecordDetailView.tsx(the lock input only),ui/packages/react/src/context/InlineEditContext.tsx(resolveApprovalLock/ApprovalLockInput, if the rule needs a new input),ui/packages/plugin-detail/src/DetailView.tsxonly if the band computes its own lock input, their tests (including a new non-participant regression test; the four objectui#12220 tests must not flip), re-recorded sdui records for touched registry packages, and a root changeset. Out of bounds:ObjectView.tsx/objectView*.ts(#25992), the reaction remove path (#26346), and any server orpackages/change. Ruling 6422939157 letter A (readers); ⛔ no retreat to B or D (stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root.changeset/.Container & model:
small,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②:
yes (widening), provisional:ApprovalLockInput(exported from@object-ui/react) is expected to gain an optional server-lock input; re-declarednoat hand-back if no exported type changesMetadata-protocol: none
Responsibility:
n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public ConsoleThread-read: 6430539374
Premise: holds (read-only premise check at
origin/main6390fe5, 2026-10-27T01:18Z: #26314 closed via PR #26361, which addsclient.approvals.getRecordLock→{ locked }; PR #26053 merged;RecordDetailView.tsxstill feedsresolveApprovalLockonly from the request-list read; spot-checked by the seat. The dev re-runs premises 1–3, 6, 7 and 9 of 6419712936 before any edit)Serial constraints cleared:
RecordDetailView.tsxlands after #26346 (seat 1, p2);react.jsonafter seat 2's #26326, then #26363;plugin-detail.json(if touched) after seat 2's #26325. The seat sets the turnDispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
objectstack-fleet commentedon Oct 27, 2026
Claim: PM loop round 1
Session:
session_017iXjUm1rujwDk7f3KT1Pkk(seat); implementing card threadsession_01ADHkmQYqTcUW7GgPkfWCzwAccount:
os-steveBranch:
claude/project-thread-i81sbn(platform-assigned card-thread branch; a card thread can push only its assigned branch, and the name carries no issue number, so this claim is the identity record)Worktree: the card thread's own container
Domain:
domain:uiSeat:
domain:ui#1File surface:
ui/packages/app-shell/src/hooks/useRecordApprovals.ts,ui/packages/app-shell/src/views/RecordDetailView.tsx(the lock input only),ui/packages/react/src/context/InlineEditContext.tsx(resolveApprovalLock/ApprovalLockInput, if the rule needs a new input),ui/packages/plugin-detail/src/DetailView.tsxonly if the band computes its own lock input, their tests (including a new non-participant regression test; the four objectui#12220 tests must not flip), re-recorded sdui records for touched registry packages, and a root changeset. Out of bounds:ObjectView.tsx/objectView*.ts(#25992), the reaction remove path (#26346), and any server orpackages/change. Ruling 6422939157 letter A (readers); ⛔ no retreat to B or D (stop on breach; explain in the report). ⛔ui/.github/**and the root.github/workflows/ci.ymlare out of bounds (maintainer's word, ui-merge epic in flight). Changeset in the root.changeset/.Container & model:
small,mode:thread,model: TIER_DEFAULT (dispatch-gates --tier: no path-derived mandate),effort: mediumClause-②:
yes (widening), provisional:ApprovalLockInput(exported from@object-ui/react) is expected to gain an optional server-lock input; re-declarednoat hand-back if no exported type changesMetadata-protocol: none
Responsibility:
n/a: renderer defect in ui/, the producing code is the cited ui file; no platform path covers it; reached through the public ConsoleThread-read: 6430539374
Premise: holds (read-only premise check at
origin/main6390fe5, 2026-10-27T01:19Z: #26314 closed via PR #26361, which addsclient.approvals.getRecordLock→{ locked }; PR #26053 merged;RecordDetailView.tsxstill feedsresolveApprovalLockonly from the request-list read; spot-checked by the seat. The dev re-runs premises 1–3, 6, 7 and 9 of 6419712936 before any edit)Serial constraints cleared:
RecordDetailView.tsxlands after #26346 (seat 1, p2);react.jsonafter seat 2's #26326, then #26363;plugin-detail.json(if touched) after seat 2's #26325. The seat sets the turnDispatch order handed to the card thread by the project coordinator (three partitions: ruling / mechanism hypotheses / suggested route); the dev does not claim, and starts only once this claim names its branch.
objectstack-fleet commentedon Oct 27, 2026
os-dev-report
{ "issue": 25934, "status": "done", "branch": "claude/project-thread-i81sbn", "pr": "https://github.com/objectstack-ai/objectstack/pull/26402", "head": "fca25b0b5397d5e33d5c9ac74c50519930abd70c", "session": "session_01ADHkmQYqTcUW7GgPkfWCzw", "premise_still_valid": true, "premises": "re-run on origin/main 61c07986 before any edit: P1 hold (InlineEditContext.tsx:107); P2 hold (RecordDetailView.tsx:1664, DetailView.tsx:1716; expected :1658/:1716, line drift only); P3 hold (useRecordApprovals.ts:480 is the only lock source; :326/:385 comments); P6 hold (lifecycle-hooks.ts beforeUpdate :538, lockedError(recordLockRefusal :633, refusal text :294; expected :487/:582/:294, drift only); P7 hold (approval-service.ts:336 lock_record?: boolean); P9 serial condition met: PR #26053 merged (054652bf), and the analysis expected it open. Blocker #26314 closed via PR #26361 (6a2866af): client.approvals.getRecordLock at packages/client/src/index.ts:6270, route at packages/rest/src/approvals-routes.ts:352 (501 without the service).", "summary": "useRecordApprovals (app-shell) reads client.approvals.getRecordLock through the mounted adapter's client when the list read finds no pending request, and exposes recordLock. RecordDetailView feeds a locked answer to resolveApprovalLock as its openApproval: that is rule 1's input exactly, so ApprovalLockInput gains nothing. For a signal-only lock it threads approvalIsSubmitter false, so the band draws no recall. No service means 501, no signal and no open approval, so rule 2 and the four objectui#12220 tests are unchanged. DetailView.tsx is untouched: the band computes from the host's threaded locked/approvalPending/approvalIsSubmitter, it does not compute its own lock input. react and plugin-detail are untouched. Clause-② re-declared no: the only type that gains a member is the hook's non-exported result interface, and the hook is not re-exported from the app-shell entry (src/index.ts, measured on the source entry; the app-shell dist was not built).", "tests": "Final head fca25b0b, one locked run: turbo build app-shell^... 64/64, then pnpm --filter @object-ui/app-shell type-check exit 0, then 4 files (new RecordDetailView.approvalLock-25934, RecordDetailView.approvalLock-12220, useRecordApprovals.answered-12220, RecordDetailView.approvalRecallGate): Test Files 4/4, Tests 25/25, VERDICT command-exit 0. Pre-merge e0f9e560: 82 neighbour files (all RecordDetailView.*, useRecordApprovals*, RecordApprovalsPanel*, AppContent.recordSaveToastNamesRecord-26172): 82/82 files, 551/551 tests, VERDICT 0. Narrowed by name; the full app-shell suite is left to CI Test UI. Ablation: with the fix committed, both source files were restored to base 61c07986 (on disk getRecordLock 3->0, lockedForReader 3->0). The non-participant case went red at :235 (expected 'false' to be 'true', locked); both lock-read cases went red because getRecordLock was never called; the participant control stayed green: 3 failed / 1 passed, VERDICT command-exit 1. Restored by git checkout HEAD --: git diff HEAD empty, blob hashes equal HEAD. Tests import src directly, so no dist leg applies.", "gates": "fca25b0b: dispatch-gates --commands derived 35; all 35 exit 0, check:type-check-debt included (run with no other locked run; re-measure OK, 0 raw errors); --ran: 35 derived, 35 run, 0 NOT-MEASURED, 0 UNRUN. Roster gate under .changeset: check-changeset-fixed.mjs exit 0. All other families (Lint & Repo Gates, Type Check UI, Test UI, Build & E2E UI, ...): PR #26402 check-runs, in_progress at report time.", "line_budget": "+373/-7 over 4 files: source +81/-7 (useRecordApprovals.ts +65/-5, RecordDetailView.tsx +16/-2), test +278, changeset +14", "files_changed": [ ".changeset/25934-reader-record-lock.md", "ui/packages/app-shell/src/hooks/useRecordApprovals.ts", "ui/packages/app-shell/src/views/RecordDetailView.tsx", "ui/packages/app-shell/src/views/RecordDetailView.approvalLock-25934.test.tsx" ], "deviations": [ "Clause-② re-declared from the claim's provisional yes (widening) to no, in the PR body and the changeset, per the delta's own re-declare clause: no exported type changes.", "RecordDetailView.tsx edit includes the approvalIsSubmitter line (:1691 area), next to the lock region: a signal-only lock must not light the band's recall lever. The reaction remove path (#26346) is untouched.", "The PR body starts with the harness attribution comment and project-thread line, and Fixes #25934 is line 3 (os-dev: the harness line is kept and reported)." ], "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0 — no mcp__github__ tool calls (transcript grep count 0); reads via gh api REST GET", "api_writes": "3 relay REST writes + 3 git pushes. Relay: pr_create, 2 requests in 1 action (POST /repos/objectstack-ai/objectstack/pulls draft, POST /issues/26402/assignees os-steve; read-back identical 7942 bytes); this os-dev-report comment (POST /issues/25934/comments via post-stamped). Pushes: branch create, changeset, merge of origin/main.", "open_questions": [ { "question": "Should resolveApprovalLock's openApproval doc in @object-ui/react name the server record lock as a host input, alongside the pending request?", "options": [ "A leave it: the rule and its input are unchanged, and the call site in RecordDetailView documents the feed", "B add one sentence to ApprovalLockInput.openApproval, which costs a react package change and its serial line (#26326, #26363)" ], "recommendation": "A: the signal is rule 1's input as written, and A keeps this PR off the react line." } ], "out_of_scope_findings": [] }Generated by Claude Code