Repository navigation
feat!: ownership queues and assignment rules (ADR-0142 phase 2) - #26337
Conversation
Queue and assignment_rule metadata types, the stack's queues and assignmentRules keys, the queue sharing recipient and queue approver re-admitted, the ownership-queue service contract, the injected queue column, the public-form strip, explain kinds and the ledger rows. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…itting-record access path ADR-0142 phase 2, plugin-sharing half: - sys_queue, sys_queue_member and sys_assignment_cursor, owned here. - OwnershipQueueService, registered under `ownership-queue` in init(): registry-first resolution, the activation-ledger switch, one expander ordered by user id, with an access/addressing purpose split. - The read filter and the update arm gain `owner_id IS NULL AND queue IN current_user.queues`; checkEdit tests the basis before the public abstain. - The `queue` sharing recipient expands through the service, with the recompute on queue, position, team and business-unit writes. - Name guards on the authored tables and a boot report for shadowed rows. - Spec: the contract's `purpose` option and `declaredQueueNamed`, the dropped-refinements baseline, and the regenerated schema artifacts. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…ment rules at insert (ADR-0142)
- Inject the system-managed `queue` text column on ownership:'user' objects,
beside `owner_id`, and refuse an author-declared `queue` at registration
(INVALID_METADATA, 422) with a rename prescription.
- Lower an update's write precondition to a guarded `updateMany` plus a
readback; zero rows answers 409 CONCURRENT_UPDATE ("the record was taken").
A driver with no `updateMany` degrades to a plain update, warned once.
- Evaluate declared assignment rules once per inserted row, after every
beforeInsert hook, for rows whose owner and queue are both empty: park,
round_robin (cursor under a version precondition, retried) and
least_loaded, under a system context pinned to the row's organization.
Assignment never refuses an insert.
- Refuse kernel:ready when a stack declares queues or assignment rules and no
`ownership-queue` service is registered.
- Spec: govern `queue` and `assignment_rule` in the liveness ledger, add the
strictness-ledger rows, localize the queue column label, register the three
plugin-sharing platform objects, keep `queue` out of title derivation, and
regenerate the reference docs and API artifacts.
Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD
Co-authored-by: Claude <noreply@anthropic.com>
…(ADR-0142 D3/D4) Step 3.5 of the authz middleware reads the injected `queue` column: - insert naming a queue: admitted with the transfer grant or for a member; a member's insert stays owned (the stamp is unchanged) with a home queue, and only the grant parks a row ownerless in one write - claim (owner_id empty -> self, by id): admitted when the row's sharing verdict is 'allow', and the write is made conditional on owner_id being empty so a concurrent claimant gets 409 CONCURRENT_UPDATE - release (owner_id self -> empty): admitted when the record has a home queue - move (queue changed): transfer-class; a name that does not resolve in the row's organization is 422 QUEUE_NOT_FOUND - a guest supplies neither anchor Explain reports queue_membership and queue_claim on the sharing layer. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…-0142 D5/D6/D7) - plugin-approvals: a `queue` approver resolves to the queue's members at node entry through the ownership-queue service (addressing purpose), in the directory organization and bounded by the request org's readers. An empty queue warns and adds no slot; the `queue:<name>` literal is never written again. - lint: `queue` joins the group-routed set of the approver empty-slate check, the org-axis red-line table admits the queue recipient, and two advisory assignment-rule checks report a user-id target and a target queue declared with no member. - ui: the flow designer stops warning that queue approvers resolve to nobody. - dogfood: the authz matrix gains the ownership-queue row and re-syncs the sharing-rules recipient list. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…DR-0142 D1/D5) - runtime: the catalog activation door takes `queue` (resolved by the ownership-queue service) and `assignment_rule` (resolved in the engine's metadata registry), with their own refusal wording. - plugin-sharing: `declaredQueueNamed` answers a switched-off queue too, so the door can switch it back on; a ledger write for a queue recomputes the queue-recipient sharing rules naming it. - i18n: the queue recipient option and the two metadata types' display pairs, translated in zh-CN, ja-JP and es-ES. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…s for queues - Regenerate the platform-object tenancy census for sys_queue, sys_queue_member and sys_assignment_cursor. - Classify the assignment-rule entry `criteria` empty state as scope. - Count the queue and assignment-rule schema modules in llms.txt. - Queue row refusals carry closed-catalog field codes: a declared-name clash is `invalid_value` (`duplicate` is not a FieldErrorCode member). - Docs transcripts print 54 author-time rules. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
… counts and checklist waivers - The shadowed-queue report keeps its logger receiver. - The queue test's engine double applies the caller's limit by presence and no longer writes into its row source. - The by-id precondition write passes driver options typed against IDataDriver instead of erasing them to any. - Docs: the overlay page counts 31 registry types, the quick reference counts 15 automation and 6 security reference pages. - Platform checklist: queue and assignment_rule are waived until an app the checklist drives declares them. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…kill keys - The metadata artifact door registers declared `queues` and `assignmentRules` (beside `sharingRules`), so an artifact-only boot serves them; the stack-collection map waivers record the other sites. - `AssignmentRuleEntryParsed` names the parsed entry; the four isomorphic queue and assignment-rule aliases are pinned (789 -> 793). - The showcase waives `queue` and `assignment_rule` until an app adopts them. - The platform skill lists the two new top-level keys; the two ADR citations in the next paragraph pay for them (488 lines either side). Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…; changeset names metadata and skills Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…export-origins snapshots Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
Conflicts resolved: - objectql engine.ts: keep main's envelopeDriverWriteRefusal import beside the queue imports. - platform-objects echo pin: 680 + 3 (main, the sharing composite) + 2 (ADR-0142, queue and assignment_rule) = 685. - plugin-security explain-access: main's resolveUserAuthzGrants import beside the ownership-queue contract import. - spec dropped-refinements baseline: 244/838 at the base, +1/+2 from main, +3/+18 from this branch = 248/858. Regenerated: spec api-surface/data.json, export-origins/data.json and the approval and metadata reference pages. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
Resolves the type-alias pin count conflict with #25808 (790 + 4 = 794) and regenerates the spec api-surface, export-origins and reference docs. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 11 package(s): 108 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 19 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 160 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f77c91b6f37e201b68cb347e6787a2c6ac361c9 && git checkout 4f77c91b6f37e201b68cb347e6787a2c6ac361c9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1685a06dd941f4037536ff8253617a85d296e4e9 d6bbeaeb4eada74dc9e266fdfa25ac058ce4af7e && git checkout -B drift-repro 1685a06dd941f4037536ff8253617a85d296e4e9 && git merge --no-ff d6bbeaeb4eada74dc9e266fdfa25ac058ce4af7e
node scripts/docs-audit/affected-docs.mjs --json 1685a06dd941f4037536ff8253617a85d296e4e9
|
…rn channel check:optional-error-sink refuses a sink whose optional `error` has no guaranteed fallback. The boot report now types its logger with a required `warn` and calls `error` when present, `warn` otherwise. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…42 D3) An object whose records have a user owner can no longer declare its own `queue` field: registration refuses it. The changeset now declares `Clause-②: yes (narrowing)` and `Metadata-protocol: narrowed`, and the upgrade guide carries the D3 entry `object-queue-field-reserved` with the remedy. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
Main moved the error-code ledger while this branch added QUEUE_NOT_FOUND, so the merge ref's generated references (the ErrorCode enum count in contract.mdx and automation-api.mdx) were stale in CI. Regenerated from the merged tree. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
…ship inventory row sys_queue, sys_queue_member and sys_assignment_cursor carry organization_id, so the ADR-0131 D10 plan refused them as uninventoried. Each is written under its organization; a member and a cursor name their queue or rule by name, so no parent anchors them (fate 3, the Default Organization under single). Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
The merge queue removed the PR on a conflict. The one conflict is a count: the metadata-forms positive control in object-lifecycle-panel-echo- decisions.test.ts (686 on main after #26548, 688 here with this branch's two registry types). Both comments are kept, main's first. Claude-Session: https://claude.ai/code/session_01GTntg542RUmmRRbMY6wMZD Co-authored-by: Claude <noreply@anthropic.com>
|
Seat delta review: re-arm after the queue ejection. Seat The queue removed this PR at 14:16Z for MERGE_CONFLICT. The new head Delta measured. File by file, Evidence at Next. Re-arm auto-merge. |
…the same three plugin-security files) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FEkQ5kdwdgQcbyZc22QQYf
Requested by Steedos · project thread
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
queuesRLS 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
queue(QueueSchema,packages/spec/src/security/queue.zod.ts) and stack keyqueues; new system objectsys_queuein plugin-sharing for an organization's own queues. Theownership-queueservice (OWNERSHIP_QUEUE_SERVICE, contractIOwnershipQueueService) resolves a name from the registry first, then from the organization's rows. The service is not namedqueue, which the job queue already holds (phase-2 carry 6409493120).sys_queue_member. One member expander serves every consumer, and the BU-tree recompute covers queues.ownership: 'user'object gets a system-managedqueuecolumn besideowner_id; declaring your ownqueuefield on one is refused at registration (INVALID_METADATA).queuejoins the injected system columns andPUBLIC_FORM_SERVER_MANAGED_FIELDS. Members reach a sitting record (owner_idempty,queueincurrent_user.queues) through the plugin-sharing owner match: read and edit, never delete.queue. A claim setsowner_idfrom 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 get409 CONCURRENT_UPDATE. A release gives your own record back to its home queue. A move needs the transfer grant, and an unresolvable name is422 QUEUE_NOT_FOUND. An insert naming a queue needs the transfer grant or membership; a guest has neither. Explain reportsqueue_membershipandqueue_claimon the sharing layer.assignment_rule(AssignmentRuleSchema) and stack keyassignmentRules. The engine routes each inserted row that is still ownerless and queueless afterbeforeInsertonce: park it in a queue, or pick a member byround_robin(cursor insys_assignment_cursor, advanced under the precondition) orleast_loaded. Assignment never refuses an insert.kernel:readyrefuses a stack that declares queues or assignment rules when noownership-queueservice is registered (merge question 17).queueapprover 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 thequeue:NAMEplaceholder is never written again.APPROVER_VALUE_BINDINGS.queuereads both sources (Open item 1).queuesharing recipient is authorable:ShareRecipientTypegainsqueue,expandRecipientreturns the queue's members, and grants are recomputed when membership changes or a queue is switched on or off.approval-approvers-may-resolve-empty; the org-axis red line admits the queue recipient; two advisory rules withos explainentries,assignment-rule-user-target(warning) andassignment-rule-queue-unstaffed(info).POST /security/_activation/:type/:nameswitches a declaredqueueorassignment_ruleon and off.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 registersqueuesandassignmentRules),packages/metadata-core(queueclassified 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:queuesandassignmentRulesin the top-level key list, required bycheck:skill-top-level-keys.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.tsanddocs/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 underdocs/audits/, and prose counts in sixcontent/docspages that count gates read.content/docs/automation/approvals.mdxnow documents thequeueapprover type.skills/** readings
skills/objectstack-platform/SKILL.md: 488 to 488 lines; 5824 to 5826 tokens (ceiling 5828).SKILL.md: 4525 to 4525 lines; 54450 to 54452 tokens.Deviations and readings for review
queue_membership,queue_claim), matching every otherExplainRuleKindmember.sys_queue_member.queueis 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 nosys_queuerow.queuefield applies to the base registration layer only.updateManyadvances 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.invalid_value, becauseFieldErrorCodeis a closed catalog with noduplicatemember.ownership-queue-gate.test.ts: an echo of the storedqueueis 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); clearingowner_idon 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 writingqueuefails closed without the grant.main, both on conflict (pin counts and generated references).Open question (needs a decision)
The
queuesRLS membership key. Execution plan item 1 lists "the queue service and itsqueuesmembership-resolver key". The resolver contract has oneIRlsMembershipResolverslot, which an app's own resolver also fills, and the producer screens reserved keys out of a resolver's answer. So plugin-sharing cannot addqueueswithout 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: reservequeuesinRESERVED_RLS_MEMBERSHIP_KEYSand have plugin-security resolve it from theownership-queueservice, as the kernel resolvespositions, 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/objectstackderived 163 commands; all 163 run, each exit code captured before any pipe, every one 0.--ranreconcile: 163 derived, 163 run, 0 NOT-MEASURED, 0 UNRUN.check:type-check-debtran underos-verify-lock.sh.src/security,src/automation/assignment-rule,src/stackandsrc/data/filter.test.ts(54 / 1955), andsrc/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.维护者速读
(本节按 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。current_user.queues(见上方待决问题)。席位意见
(待席位填写)
你要做的
queues这个 RLS 成员键,是否由 plugin-security 作为平台保留键提供(推荐),在本卡后续补上。Landing head —
f35f41144(seat note)The Tests and Gates sections above were measured at
578e7a41. At the landing headf35f41144:cbd27d9a6. It closes all three must-fix items of 6430868091. The seat's delta review carries it tof35f41144: in every changed file,git diff cbd27d9a6 f35f41144makes exactly main's own change from7c34e5dc2tofe53769a8, except the count lines of three files. Those aretype-alias-convention.pin.test.ts(796),dropped-refinements.baseline.json(251 / 887) andreferences/index.mdx(1556).The declaration lines above follow the changeset:
Clause-②: yes (narrowing)/Metadata-protocol: narrowed.Generated by Claude Code