Repository navigation
Commit a9f654e
feat!: ownership queues and assignment rules (ADR-0142 phase 2) (#26337)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} -->
_Requested by **Steedos** · [project
thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbBvbmHqnauTSkDLEUWJthmQ)_
Part of #25769. This is phase 2 of ADR-0142: the one spec + server PR of
the record's Execution plan item 1. The card stays open for what this PR
does not carry: the `queues` RLS membership key (an open question below,
it needs a decision), the HotCRM STOPGAP deletion (its own lane, after a
published release), the Console queue picker and Claim affordance
(objectui lane), and the authoring guide (Execution plan item 4).
Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up #26475
Contract review tier: **H**. The diff is over 3000 changed lines (168
files, +6230 / -380) and touches `skills/**`, so it lands only on a
governed approver's review.
## What lands, by decision
- **D1, the queue entity.** New metadata type `queue` (`QueueSchema`,
`packages/spec/src/security/queue.zod.ts`) and stack key `queues`; new
system object `sys_queue` in plugin-sharing for an organization's own
queues. The `ownership-queue` service (`OWNERSHIP_QUEUE_SERVICE`,
contract `IOwnershipQueueService`) resolves a name from the registry
first, then from the organization's rows. The service is not named
`queue`, which the job queue already holds (phase-2 carry 6409493120).
- **D2, membership.** A declared queue's members are positions (merge
question 18); an organization adds users, teams, positions and business
units through `sys_queue_member`. One member expander serves every
consumer, and the BU-tree recompute covers queues.
- **D3, where a queued record sits.** Every `ownership: 'user'` object
gets a system-managed `queue` column beside `owner_id`; declaring your
own `queue` field on one is refused at registration
(`INVALID_METADATA`). `queue` joins the injected system columns and
`PUBLIC_FORM_SERVER_MANAGED_FIELDS`. Members reach a sitting record
(`owner_id` empty, `queue` in `current_user.queues`) through the
plugin-sharing owner match: read and edit, never delete.
- **D4, claim, release, move.** The step-3.5 owner gate handles `queue`.
A claim sets `owner_id` from empty to the caller, by id, admitted when
the sharing model allows the caller the record; the engine's by-id write
precondition makes one claimant win and the other get `409
CONCURRENT_UPDATE`. A release gives your own record back to its home
queue. A move needs the transfer grant, and an unresolvable name is `422
QUEUE_NOT_FOUND`. An insert naming a queue needs the transfer grant or
membership; a guest has neither. Explain reports `queue_membership` and
`queue_claim` on the sharing layer.
- **D5, assignment rules.** New metadata type `assignment_rule`
(`AssignmentRuleSchema`) and stack key `assignmentRules`. The engine
routes each inserted row that is still ownerless and queueless after
`beforeInsert` once: park it in a queue, or pick a member by
`round_robin` (cursor in `sys_assignment_cursor`, advanced under the
precondition) or `least_loaded`. Assignment never refuses an insert.
`kernel:ready` refuses a stack that declares queues or assignment rules
when no `ownership-queue` service is registered (merge question 17).
- **D6, the `queue` approver type** is authorable again: it resolves to
the queue's members when the step opens, an empty queue adds nobody and
logs a warning, and the `queue:NAME` placeholder is never written again.
`APPROVER_VALUE_BINDINGS.queue` reads both sources (Open item 1).
- **D7, the `queue` sharing recipient** is authorable:
`ShareRecipientType` gains `queue`, `expandRecipient` returns the
queue's members, and grants are recomputed when membership changes or a
queue is switched on or off.
- **Lint.** A queue-only approval node is reported
`approval-approvers-may-resolve-empty`; the org-axis red line admits the
queue recipient; two advisory rules with `os explain` entries,
`assignment-rule-user-target` (warning) and
`assignment-rule-queue-unstaffed` (info).
- **Runtime.** `POST /security/_activation/:type/:name` switches a
declared `queue` or `assignment_rule` on and off.
- **Dogfood.** The authz conformance matrix gains the claim admission,
the access path and the two re-admitted members (Open item 5).
## Edits outside the claim's declared file surface
The claim names spec, plugin-sharing, plugin-approvals and the gate site
in objectql / plugin-security. Measurement put producers and gate-owned
ledgers elsewhere too, each edited in place:
- `packages/lint`, `packages/runtime`, `packages/qa/dogfood`: the lint,
activation and dogfood rows of Execution plan item 1.
- `packages/metadata` (the artifact door registers `queues` and
`assignmentRules`), `packages/metadata-core` (`queue` classified as an
injected system column), `packages/platform-objects` (labels and the
queue recipient option in zh-CN, ja-JP, es-ES), `ui/packages/app-shell`
(the flow designer stops warning that a queue approver resolves to
nobody).
- `skills/objectstack-platform/SKILL.md`: `queues` and `assignmentRules`
in the top-level key list, required by `check:skill-top-level-keys`.
- Gate-owned ledgers: `scripts/platform-object-tenancy-census.json`
(regenerated), `scripts/check-stack-collection-maps.mjs` (waiver rows
for the two keys), `scripts/engine-double-contract.pinned.json`,
`examples/app-showcase/src/coverage.ts` and
`docs/qa/platform-checklist/coverage.json` (waived: no fixture declares
a queue or an assignment rule until the HotCRM phase),
`packages/spec/liveness/*` (two hand-authored ledgers plus an
empty-state registry row), `packages/spec/llms.txt`, the
strictness-ledger counts under `docs/audits/`, and prose counts in six
`content/docs` pages that count gates read.
`content/docs/automation/approvals.mdx` now documents the `queue`
approver type.
## skills/** readings
- Touched file `skills/objectstack-platform/SKILL.md`: 488 to 488 lines;
5824 to 5826 tokens (ceiling 5828).
- Whole bundle, all 11 `SKILL.md`: 4525 to 4525 lines; 54450 to 54452
tokens.
- The two added keys are paid by deleting two ADR citations,
"(ADR-0019)" and "(ADR-0020)", from the same file. No re-wrap.
## Deviations and readings for review
- The ADR writes the explain kinds hyphenated; the spec enum is
snake_case (`queue_membership`, `queue_claim`), matching every other
`ExplainRuleKind` member.
- `sys_queue_member.queue` is a text column holding the queue name, not
a lookup, because an organization staffs a declared queue under its name
and a declared queue has no `sys_queue` row.
- The registration refusal for a declared `queue` field applies to the
base registration layer only.
- Declared assignment rules are validated at load time by lint only; the
runtime logs an unresolved target once and leaves the row ownerless.
- Round-robin cursor: a driver with no `updateMany` advances it
unconditionally, logged once (merge question 7). A cursor that keeps
losing races under concurrent inserts, past its bounded retries, parks
the row in the queue with a warning; this second fallback is not in the
record.
- An organization queue whose name clashes with a declared queue is
refused with field code `invalid_value`, because `FieldErrorCode` is a
closed catalog with no `duplicate` member.
- Gate readings, each with a case in `ownership-queue-gate.test.ts`: an
echo of the stored `queue` is not a move; a claim on a record with no
queue at all is admitted when sharing positively admits it (an edit
share, Modify All); clearing `owner_id` on a record with no home queue
is a disown and needs the grant; a member who supplies an empty owner
beside a queue on insert is still stamped owner, because parking needs
the grant (merge question 13); a predicate update writing `queue` fails
closed without the grant.
- Two merges of `main`, both on conflict (pin counts and generated
references).
## Open question (needs a decision)
**The `queues` RLS membership key.** Execution plan item 1 lists "the
queue service and its `queues` membership-resolver key". The resolver
contract has one `IRlsMembershipResolver` slot, which an app's own
resolver also fills, and the producer screens reserved keys out of a
resolver's answer. So plugin-sharing cannot add `queues` without either
displacing an app's resolver or being screened out. This PR does not
implement it; the access path of D3 does not need it (it goes through
the sharing owner match). Recommendation: reserve `queues` in
`RESERVED_RLS_MEMBERSHIP_KEYS` and have plugin-security resolve it from
the `ownership-queue` service, as the kernel resolves `positions`, in a
follow-up on this card.
## Tests and gates (head 578e7a4, merge base 5cb1c1a)
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 163 commands; all 163 run, each exit
code captured before any pipe, every one 0. `--ran` reconcile: 163
derived, 163 run, 0 NOT-MEASURED, 0 UNRUN. `check:type-check-debt` ran
under `os-verify-lock.sh`.
- Suites at this head, under the lock: objectql 474 files / 8820 tests;
plugin-security 228 / 4964 passed (45 pre-existing skips, none added);
plugin-sharing 57 / 1271; plugin-approvals 98 / 1280; platform-objects
96 / 1361; lint 166 / 7393; runtime activation door and route ledger 2 /
75; dogfood authz conformance 61; app-shell targeted 2 / 34.
- Spec: the touched test files plus `src/security`,
`src/automation/assignment-rule`, `src/stack` and
`src/data/filter.test.ts` (54 / 1955), and
`src/type-alias-convention.pin.test.ts` (3). The full spec suite was not
run locally: it exceeds the 560s local budget on this box; CI runs it.
- Full dogfood battery: owed by the narrowing; see the landing-head
section below for the reading at the landing head.
## 维护者速读
(本节按 os-dev 对受管面 PR 的要求用中文书写;仓库其余 GitHub 文本按 AGENTS.md
用英文。两条规则在此并存,已在报告中注明。)
**改了什么**
平台有了「队列」。一条记录没有负责人时,可以先停在某个队列里:队列成员都能看到并编辑它(不能删除),谁先「认领」谁就成为负责人,同时认领的另一人会收到冲突提示。应用还可以声明「分配规则」:新建记录没有负责人时,按条件放进某个队列,或在队列成员中按轮询、最少负载挑一个人。审批里的「队列审批人」和共享规则里的「队列接收方」,以前是保留未开放的选项,现在真正可用。
**为什么改**
Tesla 裁定 D6 = A:队列与分配是平台能力,不该由应用自己写。HotCRM 目前用 587
行临时代码给工单和线索做轮询分配,还要绕过负责人转移的权限闸门。本 PR 落地并发版后,HotCRM
可以删掉这些代码,改成声明两个队列、两条规则。ADR-0142 已被接受,本 PR 是它的第二阶段(规范 + 服务端)。
**风险与代价(含回滚)**
- 所有按用户归属的对象都会多一个系统字段 `queue`。已有对象若自己声明了同名字段,注册时会被拒,需要改名。
- 新增三张系统表:`sys_queue`、`sys_queue_member`、`sys_assignment_cursor`。
- 声明了队列或分配规则、却没装 plugin-sharing 的组合,启动时会被拒,而不是静默不分配。
- 不声明队列的现有应用,行为不变。
- 改动面大(168 个文件),还动了对外发布的技能包,属于 Tier H,必须人工批准。
- 回滚:revert 本 PR 即可。新列和三张表里已有的数据会留下,但不再有代码读取。
- 还没做:RLS 策略里暂时不能直接使用 `current_user.queues`(见上方待决问题)。
**席位意见**
(待席位填写)
**你要做的**
1. 对待决问题拍板:`queues` 这个 RLS 成员键,是否由 plugin-security
作为平台保留键提供(推荐),在本卡后续补上。
2. 审阅并批准本 PR。
3. 合并并发版后,HotCRM 删除临时代码的那张卡才能开工。
## Landing head — `f35f41144` (seat note)
The Tests and Gates sections above were measured at `578e7a41`. At the
landing head `f35f41144`:
- CI ran 62 check-runs: 57 success, 5 skipped by path, 0 failed.
- The full dogfood battery covered 304 files: 301 passed, 3 skipped, 0
failed (2521 tests: 2503 passed, 18 skipped).
- The contract review is 6441855511, PASS at `cbd27d9a6`. It closes all
three must-fix items of 6430868091. The seat's delta review carries it
to `f35f41144`: in every changed file, `git diff cbd27d9 f35f411`
makes exactly main's own change from `7c34e5dc2` to `fe53769a8`, except
the count lines of three files. Those are
`type-alias-convention.pin.test.ts` (796),
`dropped-refinements.baseline.json` (251 / 887) and
`references/index.mdx` (1556).
The declaration lines above follow the changeset: `Clause-②: yes
(narrowing)` / `Metadata-protocol: narrowed`.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a485b5a commit a9f654e
184 files changed
Lines changed: 6475 additions & 403 deletions
File tree
- .changeset
- content/docs
- automation
- deployment
- getting-started
- references
- api
- automation
- kernel
- security
- ui
- docs
- audits
- 2026-07-unknown-key-strictness-ledger.counts
- qa/platform-checklist
- examples/app-showcase/src
- packages
- cli
- src/utils
- test
- drivers/driver-sql/src
- lint/src
- metadata-core
- src
- test
- metadata-protocol/src
- metadata/src
- objectql/src
- platform-objects/src
- apps/translations
- system
- plugins
- plugin-approvals
- src
- plugin-security/src
- plugin-sharing/src
- objects
- translations
- qa/dogfood/test
- rest/src
- runtime/src
- domains
- spec
- api-surface
- authorable-defaults
- authorable-surface
- declaration-map
- export-origins
- json-schema.manifest
- liveness
- state-counts
- scripts/liveness
- src
- api
- automation
- contracts
- data
- kernel
- meta-spelling
- migrations/entries/semantic
- security
- system
- constants
- scripts
- skills
- objectstack-automation/references
- objectstack-platform
- ui/packages
- app-shell/src
- utils
- views/metadata-admin
- inspectors
- plugin-list/src/__tests__
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
401 | 401 | | |
402 | 402 | | |
403 | 403 | | |
404 | | - | |
| 404 | + | |
405 | 405 | | |
406 | 406 | | |
407 | 407 | | |
408 | | - | |
409 | | - | |
410 | | - | |
411 | | - | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
412 | 412 | | |
413 | 413 | | |
414 | 414 | | |
415 | | - | |
| 415 | + | |
416 | 416 | | |
417 | 417 | | |
418 | 418 | | |
419 | 419 | | |
420 | 420 | | |
421 | | - | |
422 | | - | |
423 | | - | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
424 | 424 | | |
425 | | - | |
| 425 | + | |
426 | 426 | | |
427 | 427 | | |
428 | 428 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
758 | 758 | | |
759 | 759 | | |
760 | 760 | | |
761 | | - | |
| 761 | + | |
762 | 762 | | |
763 | 763 | | |
764 | 764 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
752 | 752 | | |
753 | 753 | | |
754 | 754 | | |
755 | | - | |
| 755 | + | |
756 | 756 | | |
757 | 757 | | |
758 | 758 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
305 | 305 | | |
306 | 306 | | |
307 | 307 | | |
308 | | - | |
| 308 | + | |
309 | 309 | | |
310 | 310 | | |
311 | 311 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
347 | 347 | | |
348 | 348 | | |
349 | 349 | | |
350 | | - | |
| 350 | + | |
351 | 351 | | |
352 | 352 | | |
353 | 353 | | |
| |||
615 | 615 | | |
616 | 616 | | |
617 | 617 | | |
| 618 | + | |
618 | 619 | | |
619 | 620 | | |
620 | 621 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
28 | 28 | | |
29 | 29 | | |
30 | 30 | | |
31 | | - | |
| 31 | + | |
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
| |||
303 | 303 | | |
304 | 304 | | |
305 | 305 | | |
| 306 | + | |
306 | 307 | | |
307 | 308 | | |
308 | 309 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
470 | 470 | | |
471 | 471 | | |
472 | 472 | | |
| 473 | + | |
473 | 474 | | |
474 | 475 | | |
475 | 476 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
640 | 640 | | |
641 | 641 | | |
642 | 642 | | |
643 | | - | |
| 643 | + | |
644 | 644 | | |
645 | 645 | | |
646 | 646 | | |
| |||
707 | 707 | | |
708 | 708 | | |
709 | 709 | | |
710 | | - | |
| 710 | + | |
711 | 711 | | |
712 | 712 | | |
713 | 713 | | |
| |||
740 | 740 | | |
741 | 741 | | |
742 | 742 | | |
| 743 | + | |
| 744 | + | |
743 | 745 | | |
744 | 746 | | |
745 | 747 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
133 | 135 | | |
134 | 136 | | |
135 | 137 | | |
| |||
374 | 376 | | |
375 | 377 | | |
376 | 378 | | |
| 379 | + | |
| 380 | + | |
377 | 381 | | |
378 | 382 | | |
379 | 383 | | |
| |||
0 commit comments