Skip to content

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

Closed

Description

@objectstack-fleet

Ruled: 6422939157 · letter A (readers) · 2026-10-26T17:31Z

QA-source: #25479 · approvals.record-page-decisions · negative[1]

What happens

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 main 77392e9 with the console at pin 5fef032b1ada (built from f0fdd45b; see #25478). Reproduced on 3 fresh loads. The resolver below is unchanged at objectui origin/main 3a73c07.

Reproduction

  1. 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}.
  2. As admin, PATCH /api/v1/data/showcase_project/ID {"budget":600000}. This opens showcase_budget_approval at manager_review.
  3. As admin, POST /api/v1/approvals/requests/MGR/approve. The run moves to exec_review, and the new request row reads lock_record: true.
  4. As the member, sign in through the console form and open /_console/apps/showcase_app/showcase_project/record/ID.
  5. 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.tsx resolveApprovalLock (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).

Activity

  1. objectstack-fleet commented on Oct 23, 2026

    @objectstack-fleet
    ContributorAuthor

    Re-measured on the new console pin: still reproduces.

  2. objectstack-fleet commented on Oct 25, 2026

    @objectstack-fleet
    ContributorAuthor

    Pause lifted · domain:ui seat 4 (vacant; written by its signed-off session session_01XWGTsmqrtrt1Zqh1ioWqsT) · 2026-10-25T14:11Z

    The os-sales tenure held this card back for the planned objectui → objectstack repo move. The maintainer lifted that pause in this project on 2026-10-25T14:08Z: 「你放回队列就写」. The card already carries pm:queue and is dispatchable under the normal rules. Nothing dispatches it now, because the domain:ui lane is off duty, so a future seat picks it up from the queue.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Transferred 2026-10-26T04:48Z from objectstack-ai/objectui#13393, with its comments and timeline; the old URL redirects here. objectui moved into this repository as the ui/ workspace (epic #25677) and is being archived (#25854). The body is left exactly as filed.

    Paths. Read every repository path in the body with a ui/ prefix: packages/react/src/context/InlineEditContext.tsx is now ui/packages/react/src/context/InlineEditContext.tsx.

    No stale pm:dispatched or assignee to strip.


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first touch — bug · priority:p3 · domain:ui · area:workflow · pm:queue · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-26T07:01Z

    ⛔ Not a claim, ⛔ not a dispatch. First touch after the transfer from objectui. The card had no triage audit comment there or here, which is what the domain:ui seat (#25966) needs before it can pick it.

    • p3, as filed: the save is refused correctly (409 RECORD_LOCKED), so this is affordance only, not a lock hole. The fix: draw the lock from the record's lock state, not from whether the approvals door returns a row for this viewer.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    决策分析 · domain:ui 席 2 · 本卡转入 needs-user-decision

    一句话问题: 一个能读、能改这条记录、但不在审批名单上的普通成员,打开一条正被"锁定型审批"占着的记录时,页面照样让他双击编辑;他改完点保存,服务器才回一句"该记录审批中已锁定,不能编辑"。同一条记录,管理员看到的是锁定横幅、编辑置灰。页面把"这人看不到审批单"误读成了"这条记录没在审批"。

    ⚠️ 人工地板: 能真正修好的选项(A、C)都会改变"非参与者默认能知道什么"——这是安全/权限边界的改动,恒在人工地板,⛔ 不入 PM 裁量,⛔ 不可代裁。另外 A 要新增公开契约面:按「改协议单独立卡,⛔ 不作缺陷卡的选项」,A 若获裁,协议部分另立卡,本卡转为消费方执行卡(见裁后执行段)。


    背景

    • 卡面复现(QA approvals.record-page-decisions negative[1]):showcase 项目预算 >500000 时进入 exec_review(lockRecord: true)。admin 看到的是锁定;一个 contributor 成员能读能改该项目,但不在审批名单上,他看到 Edit 可点、12 个字段可双击编辑、没有锁定横幅。保存时 PATCH 返回 409 RECORD_LOCKED。该成员自己的 GET /api/v1/approvals/requests?object=showcase_project&recordId=ID 返回 200 {"data":[]}。
    • 服务器拦截是对的(分诊 2026-10-26T07:01Z 定 p3:只是可见性问题,锁本身没有漏洞)。错在页面:它只能从"审批读接口给我看到的单子"推断锁,而这扇门对非参与者默认不给任何一行。
    • 卡从 objectui#13393 迁来,路径一律加 ui/ 前缀(迁移评论 6408477511)。

    Governing text

    1. 规约契约:锁状态由服务器给,客户端不猜、不提供会被拒的编辑。packages/spec/src/contracts/approval-service.ts:320-336(lock_record 文档块):"Mirrors the lockRecord policy the record-lock beforeUpdate hook enforces … so a client never has to guess, and never disagrees with the server … should fail closed (assume locked) rather than offer an edit the server will reject with RECORD_LOCKED."
      — 这条约定的本意今天对"非参与者"这类用户落空:lock_record 只放在请求行上,而这类用户读不到请求行。这是契约在载体上的缺口,不是代码偏离了规约。
    2. 维护者裁决(【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652,评论 5299823744,2026-08-15T01:29Z):能读记录的人可只读查看该记录的审批请求及完整动作历史,由每个对象各自的开关打开,默认关闭,"existing deployments' confidentiality posture is unchanged"。代码落点:packages/plugins/plugin-approvals/src/approval-service.ts:8312-8358(文档块,裁决在 :8316),规约面 packages/spec/src/data/object.zod.ts:350(approvalsVisibleToReaders 默认 false,"comments and attachments included")。
      — 这条裁决管的是审批单内容(谁审、评论、附件、快照)的可见性;它没有裁过"锁住了"这个事实对读者是否可见。
    3. 服务器早已把"锁住了"告诉任何尝试保存的人:packages/plugins/plugin-approvals/src/lifecycle-hooks.ts:290-294 recordLockRefusal → "<对象> '<标题>' is locked while an approval is in progress, and cannot be edited until that approval is complete",由 :578-582 抛出 409。
    4. QA 清单的验收句不分人群:docs/qa/platform-checklist/areas/approvals.json:867 "a LOCKING pending approval hides the record's inline-edit affordances";:881 "an inline-edit affordance offered while a lockRecord:true approval is pending is a FAIL"。
    5. AGENTS.md chore: version packages #10(不宣传运行时不兑现的能力,declared ≠ enforced)与 Add comprehensive test suite for Zod schema validation #12(contract-first:⛔ 消费端不靠 ?? 回退去容错)。
    6. finding(skills): the dev claim comment's session ID cannot distinguish a mode:subagent dev from its PM — they share one session, so the branch is the only distinguishing identity #12220 规则本身:ui/packages/react/src/context/InlineEditContext.tsx:82-100 文档块,规则 2 有意如此——没有审批服务的组合也会回 200 { data: [] },如果残留的 pending 列还当锁,记录会被锁死,而且没有可撤回的东西。

    **ADR 检索结论:没有 ADR 已裁本题。**逐条读过工具点名的 ADR-0002 Decision(环境级数据库隔离,"reader" 只是 "reader code" 的字面命中)、ADR-0018 Decision §3(执行器描述符注册表)、ADR-0019 D1–D4(审批收归 Flow 节点;"What must NOT be lost" 只要求锁钩子和状态镜像原样保留),以及另外三个审批 ADR:ADR-0042 SLA、ADR-0043 可操作链接、ADR-0044 退回修改(D6 只定锁的生命周期)。另外在 docs/adr 全量 grep record.reader|approvalsVisibleToReaders|8652|non-participant|RECORD_LOCKED,零命中。最接近的是上面第 2 条 #8652 裁决,但它管的是审批单内容,不是锁这个事实。所以本卡是真决策,不是执行卡。

    协议声明、是否改协议

    选项 改协议? 说明
    A 改(增量) IApprovalService 新增一个以记录为锚的锁读取面,外加 REST 路由。只增不改,不破坏旧调用方。按规则另立协议卡(domain:services),本卡挂 Blocked-by:。Metadata-protocol: none(作者不可写的服务契约);Clause-②: yes (widening),先例 #22660 自标同款
    B 不改规约;但需要一个今天不存在的"对象是否启用锁定型审批"信号,真做就是另一项协议新增 部分推翻 #12220 规则 2
    C 不改 作者在元数据里写 enable.approvalsVisibleToReaders: true
    D 不改 仅客户端

    前提(核于 origin/main dfaad4da,2026-10-26T14:37Z;每条附 re-check)

    1. 规则 2 仍把"读到了但没有未结请求"当作解锁。
      git show origin/main:ui/packages/react/src/context/InlineEditContext.tsx | grep -n "if (readAnswered) return { pending: false, locked: false }" → 期望 :107
    2. 宿主页与详情横幅都走这条规则。
      git grep -n "resolveApprovalLock({" origin/main -- ui/packages/app-shell/src/views/RecordDetailView.tsx ui/packages/plugin-detail/src/DetailView.tsx → 期望 RecordDetailView.tsx:1658(至 :1664)、DetailView.tsx:1716(至 :1720)
    3. 客户端唯一的锁来源是记录级请求列表读。
      git show origin/main:ui/packages/app-shell/src/hooks/useRecordApprovals.ts | grep -n "approvals/requests?object=" → 期望含 :480(:326、:385 是注释)
    4. 读者档默认关;只有宿主名单或对象声明能打开。
      git show origin/main:packages/plugins/plugin-approvals/src/approval-service.ts | grep -n "if (!this.recordReaderTierOn(object)) return;\|approvalsVisibleToReaders === true" → 期望 :8375、:8455(函数体 :8444-8456)
      git show origin/main:packages/spec/src/data/object.zod.ts | grep -n "approvalsVisibleToReaders: z.boolean().default(false)" → 期望 :350
    5. 示例应用里没有任何对象打开读者档。
      git grep -n "approvalsVisibleToReaders\|recordReaderVisibleObjects" origin/main -- examples → 期望零命中
    6. 服务器执法正确,而且拒绝文案已经说出"审批中已锁定"。
      git show origin/main:packages/plugins/plugin-approvals/src/lifecycle-hooks.ts | grep -n "registerHook('beforeUpdate'\|lockedError(recordLockRefusal\|is locked while an approval is in progress" → 期望 :487、:582、:294(另一处 :791 是委托对象的守卫,不是本钩)
    7. lock_record 只放在请求行上。
      git show origin/main:packages/spec/src/contracts/approval-service.ts | grep -n "lock_record?: boolean" → 期望 :336
    8. showcase 不用状态镜像列,所以 finding(skills): the dev claim comment's session ID cannot distinguish a mode:subagent dev from its PM — they share one session, so the branch is the only distinguishing identity #12220 的"列回退"对它不起作用。
      git grep -n "approval_status" origin/main -- examples/app-showcase/src → 期望零命中
    9. 席 1 的在飞 PR fix(app-shell,react,plugin-detail): fold an override-only admin's approval levers into one menu and draw Recall once #26053 正改同一批文件(InlineEditContext.tsx +29/−1、RecordDetailView.tsx +33/−2、DetailView.tsx +10/−2)。
      gh api repos/objectstack-ai/objectstack/pulls/26053 --jq .state → 期望 open;gh api repos/objectstack-ai/objectstack/pulls/26053/files --jq '.[].filename' | grep -c "InlineEditContext\|RecordDetailView.tsx\|DetailView.tsx" → 期望 ≥3
    10. B 会翻转的测试。
      git grep -n "answered empty is neither pending nor locked\|answers 200 empty opens editable\|host read answered with no request" origin/main -- ui/packages → 期望 resolveApprovalLock-12220.test.ts:25、RecordDetailView.approvalLock-12220.test.tsx:213、DetailView.approvalReadAnswered-12220.test.tsx:70
    11. 线上复现(非仓内):以卡面成员身份 GET /api/v1/approvals/requests?object=showcase_project&recordId=ID → 期望 200 {"data":[]},随后 PATCH → 409 RECORD_LOCKED。

    具体问题

    "这条记录正被审批锁住"这个事实,是否默认告诉每一个能读这条记录的人(不连带审批单的内容)? 如果告诉,由服务器给出,页面照读;如果不告诉,页面只能继续猜,或者让作者把整份审批历史都打开。

    选项 × 真实代价 × 工作量

    选项 做什么 客户可感知后果 工作量(车道 · PR 数 · 实测先例)
    A 服务器给读者一个锁信号 新增一个以记录为锚的锁读取(建议形态:IApprovalService 加一个只回 { locked: boolean } 的方法,另配 REST 路由)。准入沿用 #8652 的锚:以调用者身份能读到这条记录即可,不新增权限概念。页面把它当作 resolveApprovalLock 的锁输入 非参与者打开被锁记录时,直接看到锁定横幅、编辑置灰,不再"改完才被拒"。他仍看不到谁在审、评论、附件、快照。**新增披露:**能读记录的人会知道"有一个锁定型审批正在进行",包括不能改这条记录的只读者 domain:services 1 PR(协议卡)→ domain:ui 1 PR(本卡),共 2 车道 2 PR,串行。服务端先例:#3815 把 lock_record 加到请求行上 +108/−1(5 文件,2026-07-28T14:15Z);#22660 按调用者读记录做锚加集成测试 +604/−30(20 文件,2026-10-10T11:36Z)。客户端:无先例(ui/ 在 #25825 以压平方式导入,本仓无 objectui 历史;objectui 仓本会话不可读)。须排在 #26053 之后
    B 页面不再从"空读"推断"没锁" 对"启用了锁定型审批"的对象,读到空就当"不确定/锁定"。这需要一个今天不存在的"对象是否启用锁定型审批"信号 showcase 没有状态镜像列,规则 3 帮不上。想修好复现只能保守锁:每个非参与者在每条该类对象记录上都失去行内编辑,即使根本没有审批在跑。等于把 #12220 修掉的"误锁"换一个人群重新引入 domain:ui 1 PR,外加那个不存在的信号(实际是另一项协议新增);翻转 4 个测试用例(前提 10)。无先例
    C 维持现状,作者自行开读者档 不写代码。需要的应用对对象写 enable: { approvalsVisibleToReaders: true } 打开之后锁定能正确显示,但代价是把**整份审批历史(含评论、附件、提交时快照)**给所有读者看,用来修一个按钮的显示。不开的应用,本问题永远存在(保存时仍 409,文案清楚) 0 PR;每对象 1 行元数据,由各应用作者承担。无须先例
    D 被拒后页面自愈 页面收到 409 RECORD_LOCKED 后,把本页切到锁定态(横幅,无撤回) 用户仍会先被引导去改一次并被拒一次,之后才看到锁。不新增披露(409 已经说过) domain:ui 1 PR。无先例。(现有 409 提示文案在行内保存路径上如何呈现:未验证)

    业务含义直译

    • A 像门禁屏上显示"本会议室使用中":谁都能看到"占用",但看不到会议内容。
    • B 像只要某会议室装了门禁,就默认"可能占用",一律不让进,哪怕里面是空的。
    • C 为了让人知道会议室被占用,干脆把会议纪要贴在门上。
    • D 让人推门撞一下,再在门上挂"使用中"。

    四轴(业务立场)

    • **① 项目长远合理性(权重 ≥50%,先读):两年后平台应该是这样:"记录被锁"是记录自己的一个状态,由服务器给每个能看这条记录的人;"审批单内容"是另一层机密,按参与关系和 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 开关控制。**主流参照:Salesforce 被审批锁住的记录在记录页上给所有查看者显示锁图标,Apex 也有 Approval.isLocked(recordId),但审批历史仍按共享规则控制。A 正是把这两层拆开;它也补上了 lock_record 契约"客户端永不猜"在非参与者身上落空的那一块。B、D 是客户端推断或事后容错,和契约文档块"never has to guess"正相反。C 把两层重新绑在一起。读数:A 收窄特例,消掉"锁可见性 = 审批单可见性"这个隐式耦合。
    • **② 实际业务拉动:**今天撞上的是 QA 实测(3 + 5 次复现,两次全新启动库)。showcase 有 5 个 lockRecord: true 节点(examples/app-showcase/src/automation/flows/index.ts:281/1815/1894、dynamic-approval.flow.ts:41/60)。【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 当初的下游拉动(Titanwind EHR)实测的正是"持有记录 CRUD 却读到 200 空"的同一人群。没有现场客户就本症状报单,严重度如分诊所定:p3,只是显示问题。
    • **③ 防 AI 犯错:**出错时谁看到什么?A 是服务器算一次,页面照抄,AI 写的页面没有猜的余地。B 让 AI 再写一条"对象是否启用审批"的推断,错了就静默误锁或静默放行。D 本身就是消费端容错(AGENTS.md Add comprehensive test suite for Zod schema validation #12 明禁的形态)。C 最危险:AI 为了修"按钮多显示了"会顺手把 approvalsVisibleToReaders: true 写进元数据,静默泄露评论和附件,而且看起来像一次正确的修复。
    • **④ 创业阶段不扩散:**A 新增一个永久契约键(一个布尔)和一条路由,是永久义务。D 不加键。C 不加键但扩大披露。B 需要的新信号同样是加键,而且修得更差。新增门禁:无,哪个选项都不加门禁。

    推荐 + 置信缺口

    推荐 A(只看①得出)。锁是记录的状态,由服务器给,页面不猜;审批单内容的机密级别维持 #8652 原状。

    • 不取 B:修好复现的唯一做法是把非参与者普遍误锁,还要先造一个不存在的信号。
    • 不取 C:用暴露整份审批历史来修一个显示问题,披露和收益不成比例。
    • 不取 D:是消费端容错,用户仍然先被拒一次,"不提供会被拒的编辑"这一契约本意没有兑现。
    • **工作量:**A(2 车道 2 PR,服务端参照 +108 至 +604 行)约为 D(1 小 PR)的 ≥3 倍。①读数不接近,所以不翻字母;p3 只影响排期,不影响方向。

    置信缺口(明说,不另荐):

    1. **披露面:**A 让"只读不可写"的人也知道"有锁定型审批在跑"。409 文案只会告诉尝试写的人。如果某部署把"有审批在进行"本身视为机密(人事、医疗类),这就是新增披露。更窄的变体是"只给可写者",但在 packages/spec/src/contracts/*.ts 中检索 (check|has|can)*Permission*( 零命中,今天没有现成的契约面可以问"调用者能否改这条",要么新造,要么接受"可读即可见"。请维护者在同一笔裁决里定。
    2. 客户端工作量无先例,只能参照 fix(app-shell,react,plugin-detail): fold an override-only admin's approval levers into one menu and draw Recall once #26053 的触面规模(该 PR 未合入,不作先例)。
    3. Salesforce 的对照是凭公开产品行为,本会话没有逐项实测。

    **自检:只看①选 A;②③④ 是否翻转:否。**②是 p3,只影响排期;④多一个布尔键,不足以翻转;③同向支持 A。

    裁后执行段(裁 A 时;维护者只裁方向,下面由席位执行)

    1. 同一笔裁定以下执行参数(按"裁决一次裁清",⛔ 不另立决策卡):(a) 载体:新方法加路由(推荐),或者在记录级列表读的响应上附字段;(b) 披露面:可读者(推荐,与 【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 同锚,以调用者身份 engine.find),或者只给可写者;(c) 只给 locked,不给 pending/请求 id/节点名(推荐)。
    2. 席位在 domain:services 立协议卡:IApprovalService 新方法 + plugin-approvals 实现(锁判定复用 lifecycle-hooks.ts 的 lockRecord !== false 同一读法,⛔ 不另写一份)+ REST 路由 + 集成测试(覆盖非参与者、只读者、不可读者三种人;不可读者 ⇒ 拒绝或空,⛔ 不泄露存在)+ changeset 带 Clause-②: yes (widening)。
    3. 本卡同笔挂 pm:blocked 和 Blocked-by: <协议卡>,去掉 needs-user-decision。协议卡落地后由 domain:ui 派发:useRecordApprovals.ts 读新信号,在 RecordDetailView.tsx:1658 处并入 resolveApprovalLock 的锁输入。规则 2 对"没有审批服务的组合"的语义保持不变(没有服务就没有锁信号,仍判不锁),finding(skills): the dev claim comment's session ID cannot distinguish a mode:subagent dev from its PM — they share one session, so the branch is the only distinguishing identity #12220 的 4 个测试不翻转;新增非参与者被锁的回归测试。**串行约束:**在 fix(app-shell,react,plugin-detail): fold an override-only admin's approval levers into one menu and draw Recall once #26053 合入之后开工。
    4. 派发令附带前提:dev 先跑前提 1–3、6、7、9。前提不成立就报 fork,⛔ 不许硬做,⛔ 不许悄悄退回 B/D。
    5. 裁 C:本卡关为 not planned,并在 object.zod.ts:350 描述中补一句"打开它也会让读者看到锁"(纯文档)。裁 D:本卡直接派 domain:ui,在 fix(app-shell,react,plugin-detail): fold an override-only admin's approval levers into one menu and draw Recall once #26053 之后。裁 B:先立"对象是否启用锁定型审批"信号的协议卡。

    os-decision-facets

    • ① 项目长远合理性:A 把"记录被锁"和"审批单内容"拆成两层,收窄"锁可见性 = 审批单可见性"这个隐式特例,并兑现 lock_record 契约"客户端永不猜";B/D 是客户端推断或事后容错,C 把两层重新绑死。
    • ② 实际业务拉动:今天是 QA 实测在撞(showcase 5 个锁定节点,8 次复现);【能力需求】plugin-approvals 审批请求可见性:对业务记录有读权者应可只读查看审批动态(或提供可见性 hook) #8652 的下游 EHR 拉动就是同一人群;没有现场客户单,p3,只影响显示,服务器已正确拒绝。
    • ③ 防 AI 犯错:A 由服务器算一次、页面照抄,响亮且唯一;C 会诱导 AI 写一行元数据就静默泄露评论和附件;B/D 是消费端推断和容错,出错时静默。
    • ④ 创业阶段不扩散:A 新增一个布尔契约键加一条路由(永久义务,已压到最窄);D 零键但不兑现契约本意;不新增门禁。
      Prior rulings read: approval,lock,reader,lockrecord → 31 hits; ADR-0002 Decision §6, ADR-0018 Decision §3, ADR-0019 D1, ADR-0019 D2, ADR-0019 D3, ADR-0019 D4; thread: none

    (逐条读毕:上述 ADR 无一裁读者对锁的可见性;另读 ADR-0042/0043/0044 亦无。人工补检:#8652 维护者裁决(评论 5299823744)只裁审批单内容的可见性,已列入 Governing text 第 2 条。)

    相关单与 PR


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    Brief-claimed-by: session_01KrKXQVvgYk2AJ4VkqMobyb · batch #401 · 2026-10-26T15:44Z

    Director seat, summon #51 continuation (ledger objectstack#12708). Presented to the maintainer in batch #401 as item 4 of 5 (with #26149, batch #400's open item, re-presented beside it at the maintainer's word 「按照总监决裁格式详细写」). Seat increment (re-verified on origin/main cdd4eea9; the card's own analysis is not restated):

  7. 2 remaining items

  8. objectstack-fleet commented on Oct 26, 2026

    @objectstack-fleet
    ContributorAuthor

    席位核对 · Blocked-by · seat domain:ui#1 · 2026-10-26T20:32Z

    Blocked-by: #26314

    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.

  9. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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.


    Generated by Claude Code

  10. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 6390fe5, 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.

  11. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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/main 6390fe5, 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.

  12. objectstack-fleet commented on Oct 27, 2026

    @objectstack-fleet
    ContributorAuthor

    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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:workflowApprovals and automation — the work that runs without a person driving itbugSomething isn't workingdomain:uipriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions