Repository navigation
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
Conversation
…ue entity, membership, claim through the transfer gate, declarative assignment rules (Proposed) Phase 1 of the queue card: the record only. Spec and server follow after the maintainer accepts the shape. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
The owner match and ownership floor live in plugin-sharing; the guest refusal also holds at the public-form door; the claim's one-owner write names its engine precondition; round-robin's degraded path and the unresolved-target path are defined; three new forks (member insert with a queue, who may claim, a declared queue column) join the merge questions. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
The queue access basis confers edit and never delete; a claim needs the sharing service's positive verdict, so the creator floor and an app widener license none; the precondition write re-reads its row; the boot and registration refusals name their owner and code; the plan lists the hand-authored liveness rows and the added tests. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
D8's claim rows and the refusal string follow D4's positive-basis rule; D4 says where the gate reads the sharing verdict and where it asks one. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
Independent full review — ADR-0142 (rounds 1 and 2 of 4)An independent full review of the whole record, on the contract-review tier at effort xhigh, by a reviewer agent separate from the drafter (fed the record, the rulings and the code at
Round 1 — full review of
|
Independent full review — ADR-0142 (rounds 3 and 4 of 4 — final verdict PASS)An independent full review of the whole record, on the contract-review tier at effort xhigh, by a reviewer agent separate from the drafter (fed the record, the rulings and the code at
Round 3 — delta review of
|
Contract reviewServed-tier: Read-only shape held: the net diff against Check-runs on this head at review time: 51 total, every one ① Derived judgmentsThe contract the record fixes, each claim about today's code read in the cited code — holds.
Does it decide only what the ruling (D6 = A) and the triage order allow? Yes. The ruling makes queue/assignment rules platform; the triage grade orders the ADR first with four parts (entity, membership, claim, assignment rules) and the two re-admissions after. The record declares exactly those, keeps Status Proposed, implements nothing, edits no other ADR (the diff is one file), and defers the execution to one spec + server PR under ruling D5 (spec key with its first reader). Everything deferred is actually deferred and named: update-time re-routing stays in HotCRM (merge question 5, D8 row), a queue as a messaging audience (8), organization-authored assignment rules (11), a Drafter judgements the ruling did not settle — are they all among the merge questions? Fifteen are, and each is stated with its alternative and cost (1 column placement, 2 regime/owner, 3 pre-resolved set, 4 no owner opt-in, 5 insert-only, 6 no claim verb, 7 cursor degradation, 8 no
Nits (non-blocking; fold into the same edit or the spec + server PR): ADR-0019 says the approver resolution moved "nearly verbatim", the record says "verbatim"; D7 item 4's lint-time clause ("admitted when the queue's declared members are of the three flat kinds") is vacuous once D1 makes declared queues positions-only — reword to "a declared queue's members are positions, so the table admits it on a platform-global object; an authored queue is judged at runtime"; ADR-0057's comparison table still maps "Public Group/Queue" onto Internal consistency. Context §1, TL;DR 2–4, D3's four sites (read filter; write filter's update arm only; ② Semver levelLevel: none for this PR. The diff publishes nothing from any released package — one new file under The two declaration lines. The PR body carries ③ Boundary flagsEach deviation and open question of os-dev-report
Implemented-by: VERDICT: PASS |
…merge questions The engine-internal reach of the by-id write precondition, the boot refusal against a loud warning, and positions-only declared members are now merge questions 16 to 18. The ADR-0019 quote says "nearly verbatim"; D7 item 4's lint clause says what holds under positions-only members. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
Merge question 16 names the data API's existing If-Match / expectedVersion check as compare-then-write and states the real fork; a lost claim answers the ledgered CONCURRENT_UPDATE 409; question 17 quotes the degradation rule. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
D9 says the PR adds CONCURRENT_UPDATE's objectql ledger row; Context §1 names both doors that compare the version, updateData and deleteData. Claude-Session: https://claude.ai/code/session_017BFhbTnbbHcXkSnMss2btn Co-authored-by: Claude <noreply@anthropic.com>
Independent full review — ADR-0142 (seat patch round: rounds 5 to 7 — final verdict PASS at
|
| Round | Head | Verdict | must-fix / should-fix / nit | Applied in |
|---|---|---|---|---|
| 5 (delta) | e08c90e |
FAIL | 1 / 1 / 1 | e765768 |
| 6 (delta) | e765768 |
PASS WITH FIXES | 0 / 0 / 1 | 05ae041a |
| 7 (final delta) | 05ae041a |
PASS | 0 / 0 / 0 | — |
05ae041a is the PR head: 860 lines, 18 merge questions.
Round 5 — delta review of e08c90e
Verdict: FAIL
Reviewed: docs/adr/0142-ownership-queue-membership-claim-and-assignment-rules.md at e08c90e (branch claude/project-thread-eypatm, committed; working tree clean). The record at 0ae85b0 (the merge of main) is byte-identical to a2e71ca, so the delta under review is 0ae85b0..e08c90e: 33 insertions / 9 deletions — merge questions 16 to 18, their three cross-references, the ADR-0019 wording, and D7 item 4. Code read at origin/main 5f5f0b4221d9; HotCRM at 8f52859.
Summary. Two of the three new merge questions are true to the code and the cited records and state their costs honestly; the two nits are closed. Merge question 16 fails: its premise — that exposing a conditional update would give the data API optimistic concurrency it does not have, at the price of a new vocabulary, a new 409 and a new spec key — is contradicted by the code, where PATCH /data/:object/:id and DELETE already carry an opt-in If-Match / expectedVersion protocol with a spec key and a 409 CONCURRENT_UPDATE. The fork the seat asked for is real, but it is a different fork from the one written, with different costs. One paragraph to fix; one should-fix follows from the same fact.
Gates (worktree root, at e08c90e):
node scripts/check-adr-symbol-anchors.mjs— EXIT=0 (2655 anchors across 144 records resolve; the record'shotcrm:anchors are reported cross-repo, not judged).node scripts/check-adr-links.mjs— EXIT=0 (807 relative links resolve).- House rules: no HTML comments, no model names, no
path:NNNanchors; the merge-question list runs 1 to 18 with the three new cross-references (D1 → 18, D1 → 17, D4 → 16) pointing at the right numbers; Chinese rulings unchanged.
Closure of the seat's items
- (a) merge question 16 (
expectengine-internal or on the data API) — present, cross-referenced from D4; premise wrong, see R5-F1. - (b) merge question 17 (boot refusal against a loud warning) — closed. AGENTS.md's rule is characterised correctly: "Functional degradation →
warn/info… an optional service never showed up" and "Anif (!service)composition branch is usually functional and belongs atwarn" (AGENTS.md, "Degradation log levels —warnvserror"); ADR-0093 D5 is correctly the precedent for refusing to boot a requested-but-degraded state ("Default: refuse to boot. Exit non-zero with an actionable error naming the package, the flag, and the two remedies"). Both costs are stated and both are real. See R5-N1 for one quotation worth adding. - (c) merge question 18 (declared members are positions only; drafter-derived) — closed. ADR-0131 D3 says what the record says it says ("Positions, permission sets and capabilities are definitions, and a definition lives only in the environment registry"; "assignments are organization-level" in its heading); D4 ("every reference to one … names it by its machine name"); ADR-0086 D1's table row ("Sharing rule | hybrid | Rule shape = metadata; recipients naming a real team/queue = config"). The alternative's costs ("organization ids inside sealed package metadata that resolve on no other install, and a config binding inside an artifact the organization cannot edit, only clone") follow from ADR-0131 D2 (declared, never materialised) and are honest; the parallel to D5's
assignTo: useratwarningis the record's own existing treatment. - Nit, ADR-0019 wording — closed: ADR-0019 says "moved nearly verbatim under the node, not rewritten", which the record now quotes.
- Nit, D7 item 4 — closed, and true to
packages/lint/src/validate-org-axis-red-lines.ts: its table intercepts onlybusiness_unitandunit_and_subordinates(BU_TREE_RECIPIENT_TYPES), listspositionas a flat expansion that "②" lets past, and its hint namesuser/team/positionas the sanctioned audiences on atenancy.enabled: falseobject; so a declared queue whose members are positions is admitted at lint time, and only a BU-kind member can be refused, which the item now says happens at runtime for authored queues.
R5-F1 — must-fix (wrong fact at the premise of a merge question the seat asked for). The data API already has If-Match optimistic concurrency, a spec key and a 409 for it; merge question 16 says exposing a conditional update would add all three.
Evidence. Merge question 16: "Exposing it on the data API (a conditional update, If-Match-like, through REST and the SDK) would give every app optimistic concurrency — a real want — at the price of a new request vocabulary through every door, a 409 every client must learn to handle, a spec key and ledger entry of its own". At 5f5f0b42: packages/rest/src/crud-routes.ts, the PATCH /data/:object/:id handler — "OCC: clients opt in by sending either the standard If-Match header or an expectedVersion field in the JSON body", and the DELETE handler — "same opt-in protocol as PATCH (If-Match header or expectedVersion query string)"; packages/spec/src/api/protocol.zod.ts, UpdateDataRequestSchema.expectedVersion — "Optimistic concurrency token (typically the updated_at value the client read). When provided, the server compares it against the current record version and returns 409 CONCURRENT_UPDATE if they differ"; packages/metadata-protocol/src/protocol.ts#ConcurrentUpdateError — code = 'CONCURRENT_UPDATE', status = 409, carrying currentVersion and currentRecord; CONCURRENT_UPDATE is in packages/spec/src/api/error-code-ledger.zod.ts; the SDK door forwards the token (packages/runtime/src/domains/data.ts). So the vocabulary, the key, the code and the client handling all exist on every door today. What does not exist is atomicity: the check is compare-then-write in the protocol layer — updateData probes the row (probeRecord), runs assertVersionOf over that probe ("the COMPARISON half, over the row the existence probe has already read. A token that agrees (or no token) issues no query of its own"), then calls engine.update — so two writers that both read the same updated_at can both pass. That is the primitive D4 says the engine lacks, and the record's Context §1 paragraph on concurrency primitives names updateMany and "no atomic increment" but not this protocol, which is the one existing conditional-update contract on the data path.
Why it matters. The maintainer is being asked to choose "engine-internal" over an alternative described as expensive and new; the real alternative is cheap and old — lower the existing expectedVersion check onto the same precondition (expect: { updated_at: TOKEN }), which would make today's optimistic concurrency atomic for every client already sending If-Match, at the cost of moving the comparison from the protocol layer into the write: updateData's probe-first ordering (404 wins over 409, #4435), the token normalisation (normaliseVersionToken, #13382) and the #23586 rule that a conflict re-reads the row under the caller's context before it reports currentVersion / currentRecord would all have to be preserved around a driver-level zero-row result. Either answer is defensible; the question as written compares against something that does not exist.
Fix. (1) Context §1, the "Concurrency primitives" paragraph: add the existing protocol in one sentence — expectedVersion / If-Match on PATCH and DELETE, compared in updateData before engine.update, not atomic — so D4's "no such primitive" reads as the measured gap it is. (2) Rewrite merge question 16: the fork is "the precondition stays engine-internal, and the existing expectedVersion check stays where it is" versus "the existing check is lowered onto it and becomes atomic"; the recommendation can stay engine-internal, with the honest cost that the data API's optimistic concurrency remains compare-then-write and the honest benefit that nothing on the data path changes shape on a queue card. Drop "new request vocabulary", "a 409 every client must learn", "a spec key … of its own". (3) D4: say the existing expectedVersion check is untouched by the PR.
R5-F2 — should-fix (ADR-0112 D1/D3, which the record binds itself to). The lost claim's 409 is RESOURCE_CONFLICT while the ledger already has CONCURRENT_UPDATE for the same condition.
Evidence. D9: "RESOURCE_CONFLICT 409 for a lost claim race". ConcurrentUpdateError's message is "Record was modified by another user" — a lost claim is exactly that, on the same by-id update verb, through the same REST door, where clients already branch on CONCURRENT_UPDATE. Two 409 codes for "another writer got there first" on one verb is the vocabulary split ADR-0112 D1 exists to prevent.
Fix. D9 names CONCURRENT_UPDATE for the lost claim (and D4's "the record was taken" becomes its message), or states the distinction that earns a second code — for example that the claim's conflict is on a field predicate rather than a version token and must not carry currentRecord. Recommend the first.
R5-N1 — nit. Merge question 17 paraphrases AGENTS.md ("puts a functional degradation at warn"); the sentence it departs from is sharper and worth quoting, since it names this exact case: "An if (!service) composition branch is usually functional and belongs at warn". Quoting it makes the override visible as an override, which is what a merge question is for.
What the delta got right (so the fix round need not re-verify): merge questions 17 and 18 and their cross-references; the ADR-0019 quotation; D7 item 4 against the lint table; D4's "engine-internal … reachable from no data door" is true as a description of the PR's design (the REST door builds its update request from named fields — object, id, data, expectedVersion, context — and updateData composes the engine options itself, so no caller-supplied option reaches engine.update today). Nothing else in the record changed, and the round-4 verification of the rest stands; no new contradiction found against ADR-0019, 0049, 0057, 0058, 0078, 0086 D1, 0090, 0093 D5, 0105, 0111, 0117, 0118, 0131 D2/D3/D4 or 0138.
Round 6 — delta review of e765768
Verdict: PASS WITH FIXES
Reviewed: docs/adr/0142-ownership-queue-membership-claim-and-assignment-rules.md at e765768 (branch claude/project-thread-eypatm, committed; working tree clean; delta from e08c90e is 23 insertions / 12 deletions: one sentence in Context §1, two in D4, one in D9, merge question 16 rewritten, merge question 17's quotation). Code read at origin/main 5f5f0b4221d9.
Summary. R5-F1, R5-F2 and R5-N1 are closed and every new sentence is true to the code: the existing If-Match / expectedVersion check is described as what it is (a protocol-layer compare before engine.update, not atomic), merge question 16 now compares the real two options with the real costs, D4 leaves the existing check untouched, the lost claim's CONCURRENT_UPDATE 409 fits both the ledger and the REST arm's shape, and the AGENTS.md quotation is verbatim. No must-fix, no should-fix. One nit on a ledger mechanic the PR will meet, folded with one half-sentence of precision; the record can go to the maintainer with it applied or not.
Gates (worktree root, at e765768):
node scripts/check-adr-symbol-anchors.mjs— EXIT=0 (2658 anchors across 144 records resolve; the newpackages/metadata-protocol/src/protocol.ts#ConcurrentUpdateErroranchor resolves to the class as described; the record'shotcrm:anchors are reported cross-repo, not judged).node scripts/check-adr-links.mjs— EXIT=0 (807 relative links resolve).- House rules: no HTML comments, no model names, no
path:NNNanchors, codes SCREAMING_SNAKE; no residualRESOURCE_CONFLICT, "new request vocabulary" or "spec key … of its own"; Chinese rulings unchanged.
Closure of round 5
- R5-F1 (merge question 16's premise) — closed. Context §1's new sentence is correct on every clause:
PATCHandDELETEtake an opt-inIf-Matchheader orexpectedVersiontoken (packages/rest/src/crud-routes.ts, both handlers;packages/spec/src/api/protocol.zod.ts,UpdateDataRequestSchema/DeleteDataRequestSchema.expectedVersion), answer 409CONCURRENT_UPDATEwhen it differs from the row'supdated_at(ConcurrentUpdateError:code = 'CONCURRENT_UPDATE',status = 409; the protocol's own words, "expectedVersionthat does not match the current record'supdated_at"), compared inupdateDataover the existence probe beforeengine.update(probeRecord→assertVersionOf→engine.update; the assertion is "the COMPARISON half, over the row the existence probe has already read … issues no query of its own"). Merge question 16's two options are now the real ones, and the three things the alternative must preserve are each in the code: the probe-first ordering ("404 wins over 409 when both could apply",updateData), token normalisation (normaliseVersionToken, rest/OCC: postgres 驱动下乐观锁必现假冲突 409 —— normaliseVersionToken 对 Date 做 String() 丢毫秒后与 ISO 字符串严格比较 #13382, "Both tokens are normalised to ONE representation before they are compared"), and the conflict's re-read under the caller's context before it reportscurrentVersion/currentRecord(metadata-protocol: a 409 CONCURRENT_UPDATE carries currentRecord as read with a system context (probeRecord), so a caller who sends a stale version may receive a row, or fields, that row- or field-level security hides from them #23586:readAsCallerthenthrow new ConcurrentUpdateError({ currentVersion, currentRecord: visible, … }),recordNotFoundErrorwhen the caller cannot read the row). Costs on both sides are stated and honest. D4 says the existing check is untouched by the PR and points at the question. - R5-F2 (one 409, not two) — closed. D9 and D4 name
CONCURRENT_UPDATEfor the lost claim with "the record was taken" as its message. This fits the shape at both ends:ConcurrentUpdateError's constructor takesmessage?besidecurrentVersion/currentRecord, and the REST arm inpackages/types/src/data-error-classification.tskeys onerror?.code === 'CONCURRENT_UPDATE' || error?.name === 'ConcurrentUpdateError', answersstatus: 409with the error's own sentence, and addscurrentVersion/currentRecordonly when present — so an engine-raised envelope carrying the code, 409 and that message, with neither field, takes the curated arm and reports no winner's row (the metadata-protocol: a 409 CONCURRENT_UPDATE carries currentRecord as read with a system context (probeRecord), so a caller who sends a stale version may receive a row, or fields, that row- or field-level security hides from them #23586 posture). The code is in the ledger (@objectstack/metadata-protocol, and the classification table's own row under@objectstack/types). See R6-N1 for the one ledger mechanic that follows. - R5-N1 (quote AGENTS.md's exact sentence) — closed; the quotation matches AGENTS.md's "Degradation log levels" section across its line wrap: "An
if (!service)composition branch is usually functional and belongs atwarn".
R6-N1 — nit. D9 says the lost claim uses "the ledger's existing code", which is true of the code and not quite of the row: the ledger is keyed by the package that stamps the code, and a package that stamps a code it carries no row for needs a PROVENANCE_WAIVERS entry ("Why the stamping package carries no row — recorded so provenance is a decision, not drift", packages/spec/src/api/error-code-ledger.zod.ts). CONCURRENT_UPDATE sits under @objectstack/metadata-protocol and @objectstack/types; the claim's 409 is raised in @objectstack/objectql, which carries no row for it. The Codes bullet's opening "registered in the ledger by the PR" covers it in spirit; make it explicit — "the PR adds the row under @objectstack/objectql" — so the provenance gate is met by decision rather than by surprise. In the same pass, Context §1's "compared in updateData" can read "in updateData / deleteData" — the delete door's comparison is assertVersionMatch, which reads the row itself; both are compare-then-write, as the sentence says.
No new contradiction. The changed sentences agree with D4's precondition ("no such primitive … today" is sharpened, not weakened, by naming the non-atomic check), with D5's cursor, with D9's one-409 rule, with Consequences ("exact only where a driver implements updateMany") and with merge questions 6 and 7. Nothing outside the diff changed; the round-5 verification of merge questions 17 and 18, D7 item 4 and the ADR-0019 quotation stands.
Round 7 — final delta review of 05ae041a
Verdict: PASS
Reviewed: docs/adr/0142-ownership-queue-membership-claim-and-assignment-rules.md at 05ae041a (branch claude/project-thread-eypatm, committed; working tree clean; delta from e76576846bc2 is 6 insertions / 3 deletions in the one file, three hunks: Context §1's concurrency sentence, D9's Codes bullet, merge question 16's second sentence). Code read at origin/main 5f5f0b4221d9.
Summary. R6-N1 is applied as asked, the three edited sentences are true to the code, and nothing else changed. No finding. The record is ready for the maintainer.
Gates (worktree root, at 05ae041a):
node scripts/check-adr-symbol-anchors.mjs— EXIT=0 (2658 anchors across 144 records resolve; the record'shotcrm:anchors are reported cross-repo, not judged).node scripts/check-adr-links.mjs— EXIT=0 (807 relative links resolve).- House rules: no HTML comments, no model names, no
path:NNNanchors, codes SCREAMING_SNAKE; Chinese rulings unchanged.
Closure of round 6
- R6-N1 — closed, both halves.
- D9 now reads "the ledger is keyed by the stamping package, and the PR adds the row under
@objectstack/objectql, where the claim's 409 is raised". True topackages/spec/src/api/error-code-ledger.zod.ts: the ledger is a map of owner keys to code arrays ('@objectstack/objectql': [is one of them), and a package that stamps a code it carries no row for must appear inPROVENANCE_WAIVERS("Why the stamping package carries no row — recorded so provenance is a decision, not drift");CONCURRENT_UPDATEsits today under@objectstack/metadata-protocoland@objectstack/typesonly, and D4 places the claim's 409 in the engine's lowering of the precondition, so the objectql row is the right addition. - Context §1 now reads "compared in
updateData(over the existence probe, beforeengine.update) and indeleteData(assertVersionMatch, reading the row itself)". True topackages/metadata-protocol/src/protocol.ts:deleteDatacallsthis.assertVersionMatch(request.object, request.id, request.expectedVersion, request.context)before it builds the engine delete, andassertVersionMatchis composed fromprobeRecordandassertVersionOf(its body inprotocol-record-reads.ts), so it performs its own read whereupdateDatareuses the existence probe. Both are compare-then-write, as the sentence concludes. - Merge question 16 now reads "compared in
updateData/deleteDatabefore the engine write (Context §1)", which matches the corrected Context §1 sentence and the two doors it names.
- D9 now reads "the ledger is keyed by the stamping package, and the PR adds the row under
No new factual error in the three hunks; no other line of the record changed (git diff e76576846bc2..05ae041a is exactly the three hunks above). The round-6 verification of the rest of the record stands.
Contract reviewServed-tier: Delta review. The governing record for the previous head Check-runs on this head at review time: 53, every one ① Derived judgments(a) Merge question 16 — is
(a′) The lost claim's 409 moved from (b) Merge question 17 — the boot refusal named as a choice against a loud warning. Answered correctly. The AGENTS.md quotation is verbatim across its line wrap ("An (c) Merge question 18 — positions-only declared members, marked drafter-derived. Answered correctly. ADR-0131 D3's heading reads "the catalog is environment-level, assignments are organization-level" and its body makes positions "definitions" that live "only in the environment registry"; D4 says every reference to a catalog item "names it by its machine name"; ADR-0086 D1's table row reads "Sharing rule | hybrid | Rule shape = metadata; recipients naming a real team/queue = config". The record says exactly these three things and labels the result "Drafter-derived, not ruled". The alternative (any of the five kinds with The two nits — closed. ADR-0019 line 97 reads "moved nearly verbatim under the node, not rewritten"; the record now quotes "moved nearly verbatim under the node". D7 item 4 now says a declared queue's members are positions, "a flat kind the table already admits on a platform-global object", so the Did anything else move in a way that decides something the ruling did not? No. Every other hunk is a cross-reference ("merge question 16/17/18") or the Context §1 fact sentence verified above. The merge-question list runs 1–18 and the three new entries are the three the earlier record asked for; nothing in TL;DR, D1–D3, D5–D8, Consequences, Alternatives, Open items, Execution plan or References changed. Status stays Proposed; the record still implements nothing and edits no other ADR. The code-vocabulary change (a′) is a correction toward an existing ledger code under ADR-0112 D1, with the provenance consequence stated — not a new choice the maintainer must be asked about separately. Do the earlier record's judgments still hold? Yes, every one, with the single supersession noted at (a′): the contract claims in D1–D9 against the cited code, the ruling and triage-grade quotations, the ADR-numbering check, the internal-consistency pass, and the "decides only what D6 = A and the triage order allow" conclusion are unaffected by a delta that adds three merge questions and corrects one code name. ② Semver levelLevel: none for this PR. Unchanged from the governing record: one new file under The two declaration lines. The PR body still carries ③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读改了什么 为什么改 风险与代价(含回滚) 席位意见 你要做的 席位核对Seat
|
|
Batch #392 governed-draft line · maintainer 「同意」 2026-10-26T04:56Z Recorded by the director seat (summon #51, |
Requested by 田騏 · project thread
Part of #25769
Clause-②: no
Metadata-protocol: none
Docs-only, Tier H (
docs/adr/**): the PR stays draft and the maintainer merges it; merging accepts the record. This is phase 1 of #25769, the shape proposal the triage grade orders first. The two declaration lines above describe this docs-only diff (no, none; the seat corrected them from the claim's yes/added, as the docs-only ADR precedent #25717 carried). The claim'sClause-②: yes/Metadata-protocol: addedbelong to the card's spec phase (a new metadata type, two re-admitted enum members), which lands in the one later spec + server PR on this same card. This diff moves no published package, so it carries no changeset and takesskip-changeset. #25769 remains open after this merges.维护者速读(草稿)
改了什么
只新增一份决策记录
docs/adr/0142-ownership-queue-membership-claim-and-assignment-rules.md(Proposed,Tier H,合并即接受),不改代码、不改 spec、不带 changeset。记录提出"工单队列"这一平台原语的形状:(1) 队列实体——像sharing_rule一样两个来源,包声明的queue元数据类型 + 组织自建的sys_queue行,按机器名引用;组合里没有队列服务却声明了queues/assignmentRules的 stack,由引擎在kernel:ready按名拒绝启动(ADR-0093 D5 的快速失败形),不留哑元数据。(2) 成员——沿用现有的分享接收人五种(用户/团队/岗位/业务单元/单元含下级),用现有的展开器,不造新主体。(3) 记录"坐进队列"=owner_id为空 + 新增系统托管列queue记队列名;队列成员通过按请求预解析的current_user.queues看到并可编辑坐在队列里的记录,这个析取加在 plugin-sharing 构建 owner 匹配的四个位置(读过滤;写过滤的 update 分支,不进 delete 分支;checkEdit作为独立的正向依据,放在公开 OWD 的 abstain 之前、不碰checkDelete共用的快速路径;explain),队列服务是queues这一个键的 RLS 成员解析器,顺带抬起平台created_by地板(访客提的单成员才改得动,公开 OWD 上也一样);访客提单在两个门都写不进queue(step 3.5 访客分支 + 公共表单服务器托管字段剥离);业务对象自带queue字段的,注册期拒绝并给出处方(INVALID_METADATA422 信封,与注册表现有的字段拒绝同形)。(4) 认领 = 分享服务正向放行的人把owner_id改成自己——队列依据、编辑分享、Modify All,即 hotcrm 今天的认领人集合;仅过 2.7 不算:created_by地板放行的创建者、app 自写的 RLS 放宽都不能认领,解除归属仍只能靠授权撤回;委托写入保留地板,所以访客提的单委托认领不了——由现有的所有权转移门禁(step 3.5)直接放行,写入带"owner 仍为空"的前像条件(引擎新增按 id 写入前置条件expect: {字段: 值},落到驱动的条件更新,成功后按 id 回读、按 id 分支照常继续,认领与轮询游标共用),并发只有一个赢家,输家 409;所有者可放回原队列;换队列仍需allowTransfer;成员插入时点名队列 = 有 owner 的记录 + 归属队列,插入戳不让位。(5) 分派规则 = 声明式元数据,条件 → 队列或用户,具名策略round_robin/least_loaded,只在插入时、所有 beforeInsert 钩子之后、落库之前评估一次,只对仍无 owner 的行生效;池子为空或目标队列在该组织解析不到,绝不阻断录入——记录坐进队列或留空,平台记一次日志。两个预留成员(审批人类型queue、分享接收人queue)只在上述运行时分支落地、lint 与测试钉住之后才重新开放可编写。为什么改
你 10-25 09:23Z 的裁决把评估 D6 定为 A:队列/分派规则是平台能力。今天 hotcrm 的
_case-assignment.ts(587 行)自己标注"⛔ STOPGAP,该删的代码":网页提单剥掉 owner 后"无处可坐",app 只能用钩子数行挑 owner、把认领拼成状态变更来绕过转移门禁,而 hotcrm 自己的规则 1–2 禁止补平台的洞。参考应用是 AI 抄得最多的样本,现在教的是错误模式。两个queue枚举成员自 #3508 起"声明了但没执行",注释里写着"只随真正的队列实现一起重开"。ADR-0138 D1(已接受)已经把"assignment rules"写进了场景自管 owner 的三件工具里,却没有机制。风险与代价(含回滚)
代价:跨包 L 级一个 PR(spec、objectql、plugin-security、plugin-sharing、plugin-approvals、lint、dogfood):两个新元数据类型、三个新系统对象(
sys_queue、sys_queue_member、sys_assignment_cursor)、一列新注入列、一个新预解析集合、插入管线一个新接缝、引擎一个按 id 写入前置条件(认领多一次按 id 回读)、设计器契约一种新绑定;两个新类型各要一份手写的 liveness 账本,不是可再生成的产物。每个ownership: 'user'对象多一列可能用不上(和owning_business_unit_id同价);已有应用若在这类对象上自带queue字段,升级时注册被拒,要改名。多队列含 BU 成员的租户每请求多一次 BU 树读取(待缓存)。认领的唯一所有者保证与轮询公平性只在驱动有updateMany时精确(已发布的五个驱动都有),否则退化为前像检查并记一次日志。谁拿到无主记录的编辑分享,谁就能认领它——这是分享作者声明的宽度,也是 hotcrm 今天的契约。hotcrm 三个工厂里的"升级改派"(更新时改路由)本记录不覆盖,hotcrm 暂留;hotcrm 已有的无主积压要补一次queue系统写入才进得了队列路径。回滚:记录是 Proposed,合并前零代码;合并后若 PR 未落,改状态行即可;PR 落地后回滚是删三张表、一列、一个接缝、一个前置条件和两条枚举——两个枚举成员退回今天"保留不可编写"的状态,无数据迁移。席位意见
你要做的
看 ADR 末尾"Open questions for the maintainer's merge decision"18 条,合并即接受,反对点编号:
owner_id留空 + 新列queue(推荐),而非 owner 指向队列或单独的队列项连接表;plugin-sharing,还是归plugin-security;owner_id更新,不另造claim动词;updateMany时退化为无条件推进(近似公平,记一次日志),不退化为最少负载;notify不加queue:),hotcrm"无人可通知"的缺口仍开;least_loaded必须声明load谓词,平台不给默认值;ownership: 'user'上自带queue字段,注册期拒绝并给处方(推荐),而非改用后缀列名owner_queue或像owner_id那样让作者字段胜出;expect只在引擎内部用(门禁的认领、分派接缝的游标),数据 API 不暴露,现有的乐观并发原样保留(推荐)。数据 API 今天已有If-Match/expectedVersion,不匹配答 409CONCURRENT_UPDATE,但那是协议层先比后写、不是原子的;另一选项是把这个现有检查落到同一个前置条件上、让乐观并发变成原子,代价是在队列卡上改写数据路径(先探存在再比版本的次序、令牌规范化、冲突时按调用者上下文回读currentRecord都要围着驱动的零行结果重保);推荐项的代价是现有检查仍不原子,日后再落是增量;认领输家的 409 直接用账本现有的CONCURRENT_UPDATE,同一动词不开第二个 409;queues/assignmentRules却没有队列服务,启动即拒绝(推荐),而非响亮告警后继续:这不是外观退化——规则覆盖的插入无处可坐、认领一律关闭、声明方毫不知情,ADR-0093 D5 是先例;代价是故意不装plugin-sharing的精简组合要先删键或装插件;sys_queue_member行加入(推荐;这是起草者从 ADR-0131 D3/D4 与 ADR-0086 D1 推出的,不是裁决);另一选项是声明队列可写五种成员、os lint给 warning(D5 对声明规则assignTo: user就是这样),代价是封印的包元数据里带组织 id、换个安装就解析不到。另附 ADR-0138 D1 一致性说明在 Verification notes;hotcrm 删码卡由席位在你合并本 PR 后立(删 D8 列出的 STOPGAP),阻塞到发布版本带上 spec + server PR。
What changed
docs/adr/0142-ownership-queue-membership-claim-and-assignment-rules.mdqueuemetadata type andsys_queuerows under ADR-0131 D6 regime C, owned byplugin-sharing, a boot refusal forqueues/assignmentRuleswithout a queue service · D2 membership through the five set-valued recipient kinds and the expanders that exist today,sys_queue_member· D3 a sitting record =owner_idempty + a system-managedqueuecolumn; members reach it through a pre-resolvedcurrent_user.queuesset added to theplugin-sharingowner match (edit, never delete); the guest refusal at step 3.5 and at the public-form door · D4 claim, release and move through the step-3.5 transfer gate, a by-id write precondition, engine-internal, so concurrent claims produce one owner (loser 409CONCURRENT_UPDATE; the data API's existingIf-Matchcheck is untouched) · D5 declared assignment rules (CEL criteria → queue or user,round_robinwith a cursor row,least_loadedwith a requiredloadpredicate), evaluated once per insert afterbeforeInserthooks, only on rows still ownerless, never blocking intake · D6 / D7 re-admission conditions for thequeueapprover type and thequeuesharing recipient, with every reader that special-casesqueuetoday · D8 the HotCRM mapping (what the platform replaces, what HotCRM deletes after a published release, what is not covered) · D9 names and codes · 18 merge questionsNo other ADR is edited. ADR-0138 D1 already names "assignment rules" among the instruments a scenario uses to own a guest-written row; this record is consistent with it and amends nothing (Verification notes).
Measurements (objectstack at
5f5f0b42, HotCRMobjectstack-ai/hotcrmat8f52859)security-plugin-authz-middleware.ts#registerAuthzMiddlewarestamps an emptyowner_idon insert, refuses a supplied foreign owner or an owner change withoutallowTransfer, and abeforeInserthook runs after it, so HotCRM's door ① (a hook stamping the payload) is invisible to the gate.transferis an explain/grant verb only, never an engine operation (DESTRUCTIVE_LIFECYCLE_VERBS).plugin-sharing'sSharingService(buildReadFilter,buildWriteFilter,checkEdit/matchesOwnerScope), not the security middleware, which stashes depth only; the platform ownership floor readscreated_by(OWNERSHIP_FLOOR_PREDICATE). The record places the queue access basis there.approval-service.ts#resolveApproverSpechas no queue branch and returns the deadqueue:VALUEliteral (approver-address.tsholds slot-address readers only and no queue handling);sharing-rule-service.ts#expandRecipientreturns[]forqueue. Every reader that special-casesqueue(spec, contracts, lint, the semantic migration entry, the recompute modules, the dogfood matrix, the select options onsys_sharing_rule) is listed in D6 / D7._case-assignment.ts(587 lines) holds three factories: intake least-loaded assignment, the escalation hand-off on a status transition, and a self-claim spelled as a status move.lead_auto_assignassigns leads;lead-assignment.flow.tsonly alerts the owner the hook chose. The record replaces intake assignment for cases and leads, the self-claim and the triage sharing rule; the escalation hand-off (an update-time re-route) is not covered and stays in HotCRM (merge question 5).Review record
Drafted on the contract-review tier at effort xhigh, as the owner's order requires for
docs/adr/**. An independent full review by a separate agent on the same tier, fed the record, the rulings and the code and not the drafter's notes, ran seven rounds. Before the seat's contract review: round 1 ata36d060FAIL (7 must-fix, 9 should-fix, 5 nits), applied in19d7532; round 2 FAIL (2 must-fix: the queue access basis conferred delete through a shared fast path, and the claimant rule missed the creator-floor arm), applied in3c6be23; round 3 PASS WITH FIXES (1 should-fix, 1 nit), applied ina2e71ca; round 4 ata2e71caPASS (comments 6399544349 and 6399553138).0ae85b0mergesmain(no conflict; the record is unchanged by it). The seat's contract review at0ae85b0(PASS, comment 6399715745) asked for three more merge questions and two nits, applied ine08c90e. Delta reviews of that round: round 5 FAIL (1 must-fix: merge question 16 weighed engine-internalexpect:against a data API it described as having no optimistic concurrency, butIf-Match/expectedVersionwith a 409CONCURRENT_UPDATEalready exists; 1 should-fix: reuse that code for the lost claim; 1 nit), applied ine765768; round 6 PASS WITH FIXES (1 nit), applied in05ae041a; round 7 at05ae041aPASS, no finding (comment 6400014400).Gates
Derived with
node scripts/pm/dispatch-gates.mjs --commands(change set vs merge base52f3f518e, 1 file, +860 / −0), all 20 run at head05ae041a, every oneexit 0:check-adr-links(+--self-test),check-adr-symbol-anchors(+--self-test),check-ci-filter-parity,check-closing-keyword-parity(+--self-test),check-comment-mask-corpus,@objectstack/lint check:doc-formula-expressions,check:adr-anchors,check:closed-ceremony-text,check:cross-package-test-inputs,check:doc-authoring,check:driver-memory-census,check:gitlink-declared,check:nul-bytes,check:pm-governed-merges,check:pm-prior-rulings,check:refd-timer-probe,check:watch-hint-literal.check:doc-formula-expressionsreads the built lint and formula packages. Before they were built withturbo run buildin this worktree, it exited 3 (PREREQUISITE NOT MET).The derivation at
05ae041awarned that the tree was 3 commits behindorigin/mainabebc4e1. So the 20 were derived again and run again on a local merge of05ae041awithabebc4e1, which was not pushed. The result: the same 20 commands, every oneexit 0, and no stale-tree warning.dispatch-gates --ran: 20 derived, 20 run, 0 NOT-MEASURED, 0 UNRUN, on both runs.No changeset:
docs/adr/**publishes nothing (skip-changeset).Acceptance notes
Generated by Claude Code