Skip to content

feat(approvals): expose the pending node's lockRecord policy on the request row (#3814, objectui#2902) - #3815

Merged
os-zhuang merged 1 commit into
mainfrom
fix/approvals-expose-lock-record
Jul 28, 2026
Merged

feat(approvals): expose the pending node's lockRecord policy on the request row (#3814, objectui#2902)#3815
os-zhuang merged 1 commit into
mainfrom
fix/approvals-expose-lock-record

Conversation

@baozhoutao

Copy link
Copy Markdown
Contributor

Closes #3814. Unblocks objectui#2902.

问题

审批节点声明 lockRecord(默认 true),记录锁钩子严格执行它 —— lockRecord: false 的节点上记录在整个等待期间都可写。服务端一直是对的,但这个策略对任何客户端都不可见。

rowFromRequestapproval-service.ts:236)确实解析了 node_config_json,却只从中挑白名单几项(__flowLabel / __nodeLabel / __round / escalation.timeoutHours / decisionOutputs)—— lockRecord 从来不在列表里,ApprovalRequestRow 上也没有别的字段承载锁信息。

于是 GET /approvals/requests 的消费方能知道的最强事实只有「存在一条 pending 请求」,从这里只能推出「记录被锁」。这个推断在每个 lockRecord: false 的节点上都是错的;一条串了不同策略节点的 flow 会让它错得肉眼可见 —— 同一个 UI 状态既表示「随便改」,也表示「保存会被 RECORD_LOCKED 打回」。客户端没有第三条路:反过来猜就会放出一个必然在保存时死掉的编辑入口。

不是回归 —— 自 Phase B autopilot 引入 lockRecord 起就是这样。

改动

ApprovalRequestRow 增加 lock_record: boolean

lock_record: cfg?.lockRecord !== false,

取值来自钩子读的那同一份 node_config_json 快照,用同一个 !== false 默认,所以客户端渲染的标志和服务端执行的规则不可能漂移。openNodeRequest / getRequest / listRequests 三条读路径都带上。

顺带把 showcase 的 showcase_budget_approval 变成这个能力的 dogfood:单人的 Manager Review 配 lockRecord: false(本就带 revise 送回循环,语义上假定记录可改),多人会签的 Executive Review 保持 true。一条 flow 同时覆盖两种策略。

兼容性

纯增量。老客户端忽略新字段即可;新客户端读 request.lock_recordundefined(老后端)按锁死处理 —— 就是现状行为。无迁移。

测试

新增 3 条:节点锁 → true、节点显式关闭 → falselockRecord 未设 → true(与钩子只在显式 === false 时放行的默认严格对齐)。三条都同时断言 openNodeRequest / listRequests / getRequest 三个读路径。

  • plugin-approvals 249 通过
  • spec 244 / lint 471 / rest 408 / app-showcase 58 通过

真机验证

配合 objectui 的对应改动,在 showcase 上跑通了一条真实审批:预算 600k→620k 触发 → 停在 Manager Review,API 返回 lock_record: false → 批准 → 进 Executive Review,返回 lock_record: true。同一条记录上两个节点分别报出各自的策略。

🤖 Generated with Claude Code

@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 7:03am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/plugin-approvals, @objectstack/spec.

104 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/plugin-approvals, packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/plugin-approvals, @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

… request row (#3814, objectui#2902)

An approval node declares `lockRecord` (default `true`) and the record-lock
`beforeUpdate` hook enforces exactly that — `lockRecord: false` and the record
stays writable while the node waits. Correct since Phase B, and invisible to
every client.

`rowFromRequest` parses `node_config_json` but projects a whitelist out of it
(`__flowLabel`, `__nodeLabel`, `__round`, `escalation.timeoutHours`,
`decisionOutputs`); `lockRecord` was never in that list, and no other field on
`ApprovalRequestRow` carried the lock. So the strongest thing a console could
learn from `GET /approvals/requests` was "a pending request exists", from which
it can only assume the record is locked. That is wrong on every opted-out node,
and a flow chaining nodes with different policies makes it visibly wrong: one
UI state for "you may edit this" and "your save dies with RECORD_LOCKED".

`ApprovalRequestRow` now carries `lock_record: boolean`, read from the same
snapshot the hook reads with the same `!== false` default, on every service
read. The flag a client renders and the rule the server applies cannot drift.

Additive and backward compatible. The showcase's `showcase_budget_approval`
declares `lockRecord: false` on its single-approver Manager Review and keeps
`true` on the multi-approver Executive Review, so one flow exercises both.

Closes #3814

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@os-zhuang
os-zhuang merged commit a6c3f38 into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the fix/approvals-expose-lock-record branch July 28, 2026 14:15
xuyushun441-sys pushed a commit that referenced this pull request Jul 28, 2026
…ock_record`

main shipped the same feature first. #3815 (`a6c3f3806`, #3814/objectui#2902)
landed `ApprovalRequestRow.lock_record`, computed `cfg?.lockRecord !== false`
off the `node_config_json` snapshot — byte-for-byte the rule this branch's
`locks_record` computes, from the same snapshot, with the same default.

Only the test file collided textually. `approval-service.ts` and the spec
contract auto-merged *clean*, which is the dangerous part: the result declared
both fields, so `GET /api/v1/approvals/requests` would have shipped one policy
under two names and every client would have had to guess which to read.

Resolved toward main: `lock_record` is already merged and released in a
changeset, so dropping it would be a breaking change layered on a duplicate.
Removed from this branch:

  - `locks_record` on `rowFromRequest` (plugin-approvals)
  - `locks_record?: boolean` on `ApprovalRequestRow` (spec contract)
  - its 3 tests — main's 3 `lock_record` tests are a superset, covering
    `openNodeRequest` / `listRequests` / `getRequest` on all three cases
  - the `locks_record` half of the changeset, which main's
    `approvals-expose-lock-record.md` already describes

The changeset is rewritten to the half that is still this branch's own
(`droppedFields` on `POST /batch`) and renamed to match; `plugin-approvals`
comes off its bump list, because after this resolution the branch no longer
touches that package. #3835 (batch creates through the create ingress) is
untouched and keeps its own changeset.

Console follow-up, NOT covered here: objectstack-ai/objectui#2914 is still open
and reads `locks_record`. It must be repointed at `lock_record` or the band
falls back to "assume locked" — the exact bug #3794 filed.

Verified after resolution: build 71/71 · spec 6836 · rest 419 (incl. all 5 new
batch tests) · plugin-approvals 318 (incl. main's 3 `lock_record` tests) ·
metadata-protocol 71 · no generated-artifact drift.

Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

approvals: 审批请求行不暴露节点的 lockRecord,客户端只能假定「有 pending 就是锁死」

2 participants