Skip to content

feat(console): ownership queues in the Console — two-source queue pickers, Claim on a queued record, an In-a-queue list filter, and the authoring guide (ADR-0142) - #27264

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-8qr81n
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/project-thread-8qr81n

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Requested by Yibo Wang · project thread

Part of #25769

Ruling-ref: ADR-0142 accepted by the maintainer's merge of #25816 (2026-10-26T06:44:50Z); this PR carries its Execution plan items 3 (Console) and 4 (docs). Item 2, the HotCRM STOPGAP delete, remains on its own lane, so #25769 stays open.
Clause-②: no
Metadata-protocol: none
Skills-docs: none here. skills/** is out of scope (Tier H); the skills-side teaching of queues and assignment rules is owed to #26475.

维护者速读

  • 改了什么:Console 补上工单队列的三处界面——审批人与分享接收人的队列选择器同时列出包声明的队列和组织自建的 sys_queue;坐在队列里的记录头部出现「认领」,只在服务端 explain 对 transfer 回答 queue_claim: admits 时显示,点击即发 { owner_id: 自己 };列表工具栏新增「在队列中」筛选(owner_id 为空且 queue 有值)。另加一篇作者指南 content/docs/automation/queues-and-assignment-rules.mdx。
  • 为什么改:ADR-0142 已接受(docs(adr): ADR-0142 — an ownership queue is a platform primitive: queue entity, membership through the existing recipient kinds, claim through the transfer gate, declarative assignment rules on insert #25816 合并),服务端在 feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337 落地,但 Console 仍是占位文本框,没有认领入口,也看不到停在队列里的记录;执行计划第 3、4 项要求补齐。
  • 风险与回滚:不改 spec、不改服务端;认领失败关闭(没有答复就不显示按钮),输掉并发认领的 409 明确提示「别人先认领了」;筛选只在对象带注入的 queue 系统列时出现。回滚即还原本 PR。
  • 先看:ui/packages/plugin-detail/src/queueClaim.ts、ui/packages/app-shell/src/views/RecordDetailView.tsx(认领)、ui/packages/plugin-list/src/ListView.tsx(筛选)、ui/packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.tsx(审批人选择器)。
  • 两分钟验证:cd ui && pnpm exec vitest run packages/plugin-detail/src/queueClaim-25769.test.tsx packages/plugin-list/src/__tests__/ListView.queueSittingFilter-25769.test.tsx packages/fields/src/widgets/RecipientPickerField.queue-25769.test.tsx packages/app-shell/src/views/metadata-admin/inspectors/FlowReferenceField.queuePicker-25769.test.tsx,预期 4 个文件 36 条全绿。
  • 需要你做的:无,席位落地

What changes

  • Queue pickers read both sources (ADR-0142 D6/D7). @object-ui/fields gains mergeQueueCandidates / queueCandidateLabel (one list by machine name, declared first, ordered by name) and a RECIPIENT_REGISTRY_AND_RECORD_KINDS table. RecipientPickerField lists a queue recipient from the metadata store's queue items and sys_queue rows (find('sys_queue', { $top: 500, $orderby: { name: 'asc' } })) and stores the name; with either reader missing it keeps the text input. The read face (refLabelDirectory) names a stored queue the same way. In @object-ui/app-shell, FlowReferenceField replaces the "until the console's two-source queue picker lands" placeholder with TwoSourcePickCell, driven by the binding the server publishes (registry-and-data, or the bundled spec's when nothing is published); json-schema-to-fields now keeps a whole registry-and-data binding instead of dropping it. A server that publishes anything else for queue keeps the old text cell.
  • Claim (ADR-0142 D3/D4). @object-ui/plugin-detail adds queueClaim.ts: recordSitsInQueue (owner empty and a queue named), useQueueClaimOffered (asks POST /api/v1/security/explain { operation: 'transfer' } and offers only on a queue_claim rule with effect: 'admits'; fails closed) and claimQueuedRecord (sends { owner_id: <self> } through update; a 409 CONCURRENT_UPDATE is taken). RecordDetailView adds a sys_claim header action on that answer, toasts claimed / taken / refused, and invalidates the record.
  • "In a queue" filter (ADR-0142 D3). @object-ui/plugin-list ListView offers a toolbar toggle only when the served object definition carries the injected queue column (system: true). Pressed, it AND-s ['owner_id','is_empty',true] and ['queue','is_not_empty',true] (lowered to $empty) into the fetch, the self-querying views and the export. Empty means null or '' on both columns, as the explain engine and the assignment seam read it.
  • i18n. fields.recipient.selectQueue, detail.claim / claimed / claimTaken / claimFailed, list.sittingInQueue / sittingInQueueHint in all 10 locales; engine.inspector.reference.pickFromQueues in the studio en/zh namespaces.
  • Docs (plan item 4). New content/docs/automation/queues-and-assignment-rules.mdx: declaring a queue and an assignment rule, the load requirement, what a parked record is and who sees it, how a claim is spelled (and release / move), and that a queue is not a messaging audience. Linked from automation/index.mdx, automation/approvals.mdx, permissions/sharing-rules.mdx (the recipient table gains its queue row) and permissions/rls.mdx (current_user.queues). The generated references/automation/assignment-rule.mdx is not touched; scripts/docs-audit/handwritten-docs.json regenerated by its own --write.
  • Changeset .changeset/25769-console-queue-affordances.md: patch on @object-ui/fields, @object-ui/app-shell, @object-ui/plugin-detail, @object-ui/plugin-list, @object-ui/i18n.

Tests

  • RecipientPickerField.queue-25769.test.tsx (7): the merge helper's order, collisions and shapes; the picker's two requests, both sources listed and a shadowing row hidden, the stored name, one failing source still offering the other, and the text input with no store (no find call).
  • FlowReferenceField.queuePicker-25769.test.tsx (9): json-schema-to-fields keeps a whole binding and drops a half one; twoSourceFor precedence; with an adapter, metaList('queue') and find('sys_queue', …), both options listed, the chosen name committed; a stored name neither lists shown flagged (not found); manual entry and back; no adapter reads nothing; another published binding keeps the text cell.
  • queueClaim-25769.test.tsx (12): sits-in-queue for owned / unowned / no queue; queueClaimAdmitted true only on queue_claim + admits (not a visible transfer, not queue_membership, not a malformed answer); the hook's POST body, no button on neutral, a non-OK or thrown probe, no probe for a non-sitting record or an unknown user; the write is { owner_id } only, a 409 is taken, a 403 is refused with its error.
  • ListView.queueSittingFilter-25769.test.tsx (8): objectHasOwnershipQueue (map and array forms, a non-system queue refused); the two triples pinned as is_empty / is_not_empty; buildEffectiveFilter with and without the conditions; the toggle renders on an injected column and its press and release change the fetch $filter; no toggle without the injected column.

Acceptance notes

  • An unowned record with no queue is claimable on the server by anyone with a positive sharing basis (D4), but the Console offers Claim only on a record that sits in a queue, as plan item 3 names it.
  • The pickers and the read face cap sys_queue at 500 rows by name, as the record pickers do.
  • The "In a queue" toggle is ephemeral toolbar state, not saved with the view.
  • The approver read face (the label shown for a stored queue approver outside the editor) is unchanged and shows the name.
  • No dogfood or live-boot leg: the harness's queue rows belong to the server PR (feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337); this PR's legs are the component tests above.

Verification

Head e3e83a00, merge base ae16687a. Every locked run is read from its VERDICT command-exit line.

  • New tests: 4 files, 36 tests, all pass (VERDICT command-exit 0).
  • Touched suites under os-verify-lock: plugin-detail + i18n + the fields recipient/queue files, 416 files, 4361 passed / 31 skipped; the app-shell scope (FlowReferenceField*, json-schema-to-fields*, flow-node-config*, utils/approverValueSources*, widget-ref-cells, RecordDetailView*), 103 files, 945 passed / 1 skipped; plugin-list whole, 168 files, 1675 passed. Each VERDICT command-exit 0. A single combined run of all of them hit the session's 30-minute limit and was split, not narrowed.
  • type-check (tsc --noEmit + tsc -p tsconfig.test.json) of @object-ui/fields, plugin-detail, plugin-list, i18n, app-shell: each VERDICT command-exit 0.
  • node scripts/pm/dispatch-gates.mjs --commands: 78 commands, all exit 0. Four were run twice. check:role-word found role: in the new guide; the wording was fixed in 4dde047e, and it exits 0. Three scripts/ tests timed out at 15s while the locked suites shared the box; on a quiet box, the two files exit 0 and the whole scripts/ run passes 5019 tests with 2 skipped. check:skill-examples exited 3 because client-react was not built; after that build it exits 0. check:type-check-debt exited 3 because it got no turn on the lock; run under the lock, it exits 0.
  • dispatch-gates --ran: 78 derived, 78 run, 0 NOT-MEASURED, 0 UNRUN.
  • The @object-ui/plugin-form write census (affordanceGrantMap-12082, red on CI at 58d26514) grades the two new write sites: claimQueuedRecord is a write helper, and the record page's handleClaim is outOfFamily, because a claim is admitted from the row's own sharing verdict, not an object grant, and is offered only on the explain answer. Run locked: 55/55 pass, and the plugin-form type-check gives VERDICT command-exit 0.
  • Docs: check:doc-authoring, check-adr-links, check-doc-frontmatter, and check:docs-audit-scope exit 0. Every new /docs/… link resolves to a page in the tree. Check Documentation Links (lychee) runs in CI.

Generated by Claude Code

…e sharing-rule recipient (ADR-0142 D6/D7)

Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
Co-Authored-By: Claude <noreply@anthropic.com>
…t filter (ADR-0142 D3/D4)

Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
Co-Authored-By: Claude <noreply@anthropic.com>
…n-a-queue filter (ADR-0142)

json-schema-to-fields now keeps a whole registry-and-data binding, so a
published two-source queue binding reaches the approver picker.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
…als, sharing rules and RLS (ADR-0142)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
@github-actions

github-actions Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1287b2fa30a48113e71b42271668156bf13ce264 → packageMentionDocs.

…s — null or ''

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
…er, and the record page's Claim as answered by explain, not an object grant

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzfFCwcM21eGjVuqwRz
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读 (席位终稿)

  • 改了什么:ADR-0142 执行计划第 3、4 项。
    • 控制台:审批人和共享规则接收方的选择器能选队列,同时列出包里声明的队列和组织自建的 sys_queue。
    • 记录页:坐在队列里的记录有「认领」按钮,只在服务端 explain 判定 queue_claim: admits 时出现。认领就是一次普通更新 { owner_id: 自己 },抢输了提示「别人先认领了」。
    • 列表:带注入 queue 列的对象,工具栏有「在队列中」筛选。
    • 文档:新增一篇作者指南,讲队列与分配规则。
  • 为什么改:服务端的队列、认领和分配规则已经落地(PR feat!: ownership queues and assignment rules (ADR-0142 phase 2) #26337),但控制台没有入口,作者也没有指南。
  • 风险与回滚:只改 ui 和文档,不改 spec,不受管。所有判定都以服务端回答为准,前端没有第二个判定方。回滚 = revert 本 PR。
  • 先看:ui/packages/plugin-detail/src/queueClaim.ts,以及 content/docs/automation/queues-and-assignment-rules.mdx。
  • 两分钟验证:PR 正文「两分钟验证」那条命令,预期 4 个文件 36 条全绿。
  • 需要你做的:无,席位落地。

席位意见:接受。四面都没碰(没有 packages/spec/src/**、没有受管路径、没有 Clause-②: yes 申报),按细则由席位自审,不另起复核。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-28T14:17Z

Head: e3e83a00044f0403fe98735df1ae70dd48174192 · card #25769 (ADR-0142 plan items 3 and 4) · claim 6467924998

  1. 形态: a draft PR to main carrying Part of #25769. It has 40 paths (+1693 / −27): the Console affordances in @object-ui/app-shell, fields, plugin-detail, plugin-list and i18n; one guide plus links in content/docs/**; the docs-audit ledger row; and one root changeset. The diff touches no packages/spec/src/**, no governed path and no Clause-②: yes line, so none of the four contract faces apply, and the seat reviews it itself at TIER_DEFAULT with no second reviewer. check-governed-merges --pr 27264 reports NOT governed. The assignee is os-support-ai.
  2. 前提实测 (the seat read the diff against origin/main):
    • Claim. useQueueClaimOffered POSTs security/explain with operation: 'transfer' for the caller, and offers the button only when a layer's record rules carry queue_claim with effect: 'admits'. That is exactly what explain-engine.ts (~:1925-1937) reports from deps.claimVerdict, and a self-explanation needs no admin rights (security-plugin-explain-access.ts: the target check runs only when userId differs from the caller). The claim is { owner_id: <self> } through update, and isConcurrentUpdateError maps the server's 409 CONCURRENT_UPDATE (concurrent-update-error.ts) to "someone else claimed it". There is no client membership test.
    • Picker. mergeQueueCandidates merges GET /meta/queue with the sys_queue rows, and the declared item wins on a name collision, as the server's registry-first resolution does.
    • Filter. It is offered only where the served object carries the injected queue column (QUEUE_FIELD_DEF, system: true). It sends owner_id is_empty + queue is_not_empty, which lower to $empty, so empty means null or '' on both columns, as the explain engine and assignment-rules.ts read it.
    • Guide. Its API facts hold on main: defineQueue from @objectstack/spec/security, defineAssignmentRule from @objectstack/spec/automation, and the stack keys queues / assignmentRules.
  3. 门禁 check-runs (on this head): 65 runs read at 2026-10-28T14:17Z: 55 success, 10 skipped, none red or pending. Every required context is green.
    • The earlier head 58d26514 was red only on Test UI (16/16). The @object-ui/plugin-form write census (affordanceGrantMap-12082) listed the new claim write as unlisted. Round 3 grades claimQueuedRecord as a write helper, and handleClaim as outOfFamily, with the reason that a claim is admitted from the row's sharing verdict and offered only on the explain answer. The test is not loosened, and the frozen unmapped grade is not used.
    • Dev-side readings: 78 of 78 derived gates exit 0 with 0 NOT-MEASURED. The locked suites passed, the plugin-form suite and three source-walking censuses (208 files) passed, and the five type-checks are green.
  4. 接受的偏差:
    • (a) handleClaim is graded outOfFamily instead of being mapped behind a new AFFORDANCE_GRANTS row. A grants row would make an object grant judge the claim, which is the second client judge the order forbids.
    • (b) Round 2, ordered by the seat, moved the "In a queue" filter from is_null / is_not_null to the $empty pair. The PR body (seat-applied r2, read back) says so.
  5. 范围外去向: none new. Plan item 2 (HotCRM) is its own lane, and the skills/** guidance is skills(data,automation,platform): teach ownership queues and assignment rules (ADR-0142 phase 2, PR #26337) with eval prompts #26475, as the body's Skills-docs: line says. spec+server: an ownership queue primitive (queue entity, membership, claim) and declarative assignment rules, so an app routes ownerless records without app-level round-robin (maintainer ruling D6 = A) #25769 stays open for those.
  6. 落地条款: the seat checked the prose faces against the diff on this head: the PR body (r2, read back), the changeset, the new guide and the edited approvals.mdx, sharing-rules.mdx, rls.mdx and automation/index.mdx links. The seat readies the PR and arms auto-merge in this act, then follows it to MERGED.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 14:18
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 2302b92 Oct 28, 2026
66 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-8qr81n branch October 28, 2026 14:34
os-zhuang pushed a commit that referenced this pull request Oct 28, 2026
Conflicts:
- ui/packages/plugin-detail/src/synth/buildDefaultPageSchema.ts: main
  retired the `slots.tabs` slot (#27219) while this branch rewrote the
  `discussion` slot to the timeline stream and deleted `rightRail`; the
  resolution keeps main's no-tabs-slot doc and branch, this branch's
  timeline-stream `discussion` doc, and no `rightRail`.
- ui/packages/app-shell/src/views/RecordDetailView.tsx: the
  `@object-ui/plugin-detail` import line takes the union of main's queue
  claim imports (#27264) and this branch's timeline/arrangement imports.

Claude-Session: https://claude.ai/code/session_cdb7a717def74e719988f089fbaa18ff
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/xl tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants