Repository navigation
feat(objectql,plugin-sharing): an organization authors its own assignment rules as sys_assignment_rule records - #30760
objectstack-fleet[bot] wants to merge 7 commits into
Conversation
…ment rules as sys_assignment_rule records An organization's own rules are evaluated first, ascending by sort_order and then by name, with the rules packages declare as the fallback. A user target owns the row only while the user is an active member of the row's organization; a lapsed one leaves the row ownerless and warns once, never refusing the insert. The sys_assignment_rule save guard refuses a row whose name a package rule holds, whose object cannot be routed, whose entry shape the stack schema refuses, whose criteria or load the engine's CEL paths cannot compile, whose user target is not an active member, or that lands in no organization. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
…boot report Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
…er, the organization-ownership inventory and the tenancy census, and add the changeset PLATFORM_PROVIDED_OBJECT_NAMES gains sys_assignment_rule under plugin-sharing, between sys_assignment_cursor and sys_queue. The organization-ownership inventory lists it as an attribution-fate table (its organization column is required, so no NULL row has a producer), and the platform-object tenancy census picks up the new object. The changeset declares Clause-② yes (widening), Metadata-protocol added and the Skills-docs follow-up. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
…es combine The assignment_rule registry entry's comment no longer calls organization-authored routing deferred, and the AssignmentRuleSchema docblock states the order the engine evaluates in: the organization's active sys_assignment_rule rows first (sort_order, then name), the declared rules after them in registry order, the first matching entry deciding across both. No key, shape or describe() text changes; the schema and reference generators produce no diff. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
…nt_rule.criteria's description line The empty-state gate ties a permissive-empty statement to the nearest shallower key; on a continuation line it resolved to the field's `required` key rather than `criteria`. The sentence now opens the description, so the statement is attributed to `criteria`. Wording is unchanged in substance. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 5 package(s): 90 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 17 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 179 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 32b206b3533a5f855b82fde15dd717ca31b9019a && git checkout 32b206b3533a5f855b82fde15dd717ca31b9019a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 ac7e06d99504da19669bc6ce16643ee99c41a29d && git checkout -B drift-repro adf6ee0948d8dd8e747678e6d5107e0c4a86bb31 && git merge --no-ff ac7e06d99504da19669bc6ce16643ee99c41a29d
node scripts/docs-audit/affected-docs.mjs --json adf6ee0948d8dd8e747678e6d5107e0c4a86bb31
|
…cope in the empty-state registry An organization's assignment rule with an empty condition takes every new record no earlier rule took. The entry records that this selects work to route and grants nothing, mirroring the declared rule's criteria entry, which stays byte-identical. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
…ry in en, zh-CN, ja-JP and es-ES plugin-sharing contributes nav_assignment_rules to the Setup app at runtime; check:app-nav-i18n requires a label for every runtime-merged nav id in every locale file of the app's translations. Claude-Session: https://claude.ai/code/session_016WV4d14C8K4gMehVb6jBvA Co-authored-by: Claude <noreply@anthropic.com>
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW Contract reviewServed-tier: Governing text: ruling A 6631036940; execution parameters 6670178869 (items 1–8); claim 6672306838 with amendments 6672490095, 6673262187, 6674205693, 6674382817; dev report 6673979825; spec#3 consents 6672529419 (hunks 1–3, terms) and 6674042568 (hunk 4) on #30558; ADR-0142 D5 and merge question 11; the PR body. Read-only shape held: Check-runs on this head (API, at review time): 64 runs, 55 The four
① Derived judgments
② Semver level
③ Boundary flags
Author note: PR Transitional text. ADR-0142 merge question 11 and the D5 note still say "deferred" on ④ Design judgment
Should-fix (non-blocking, for the seat's ACCEPT note or a follow-up): (a) the Implemented-by: VERDICT: PASS |
|
domain:server#1 · os-elon-musk · session_016WV4cytLmJxfAmgPVjUbbW 席位核对 · ACCEPT · 2026-11-06T02:21Z · #30066Head-sha: ac7e06d
|
Requested by Elon · project thread
Fixes #30066
Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: follow-up owed — content/docs/automation/queues-and-assignment-rules.mdx (an organization's own rules, their place before the declared ones, and the active-member rule for a user target), skills/objectstack-automation/SKILL.md (the
assignTo: { type: 'user' }line: a user target owns a row only while the user is an active member)维护者速读
sys_assignment_rule,一行一条规则);引擎先评本组织的规则,再用应用包声明的规则兜底,第一条命中的说了算。指定给某个人的规则,只有那人仍是本组织的活跃成员时才生效。sys_assignment_rule(表不存在时不读);包规则里写死用户 id 的,若该用户不是记录所在组织的成员,从此不再分派(记一次 warn,插入照常)。回滚即 revert 本 PR,表里已存的行不会被引擎读到。packages/objectql/src/assignment-rules.ts(apply()里组织规则在前、读失败时不走兜底;routeToUser的成员校验);packages/plugins/plugin-sharing/src/assignment-rule-hooks.ts(保存时的全部拒收);packages/plugins/plugin-sharing/src/objects/sys-assignment-rule.object.ts。pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/ownership-queue.test.ts,预期全绿,其中「an organization's own rules go first」一组 9 条都通过。What changes
@objectstack/plugin-sharing: a new object,sys_assignment_rule(src/objects/sys-assignment-rule.object.ts), registered in the plugin manifest, with a Setup nav entrynav_assignment_rulesbeside Queues that opens the generic record list (the dedicated Setup page is the domain:ui card). One row is one rule with one entry:object_name,criteria(CEL overrecord, empty = every row),sort_order,recipient_type(queueoruser) +recipient_id(the existingrecipient-pickerwidget, which reads a sibling literally namedrecipient_type), an optionalstrategy(round_robin/least_loaded) andload, andactive.organization_idis required; the name is unique per organization.The save guard (
src/assignment-rule-hooks.ts, bound onkernel:readybeside the queue guards).beforeInsert/beforeUpdatejudge the merged row and refuse with oneVALIDATION_FAILEDnaming every field at fault:queuecolumn;AssignmentRuleSchemarefuses for a declared entry of the same shape (a strategy on a user target,least_loadedwithoutload,loadwithout a strategy, a malformed queue name, an unknown recipient type);criteriaorloadthe engine cannot compile, judged by the same CEL paths the seam runs (ExpressionEngine.compile;compileCelToFilter) — ruling item 5;sys_memberrow and no ban in force) — ruling item 2;An update that touches none of the judged columns (a label,
active,sort_order) is not re-judged. A membership read that fails refuses the save rather than storing an unjudged target. A boot report logs, aterror, any stored row whose name a later-installed package now declares.@objectstack/objectql: the seam reads both sources (src/assignment-rules.ts). For an insert of an object that carries the queue column, the evaluator reads the organization'ssys_assignment_rulerows once per organization in the batch, orders them bysort_orderthen name, and puts them before the declared rules (ruling item 1: first match; the activation ledger has no organization column, so the other order would let a package rule always win). Then:organizationMemberStandinganswersmember; otherwise the row is written ownerless and the lapse warns once per rule, user and organization (ruling item 2). With nosys_membertable registered, no user can be confirmed and none is assigned;The engine's call site (
src/engine.ts) asksevaluator.mayRoute(object)instead ofrulesFor(object).length > 0, so an object with no declared rule is still routed by an organization's own. New exports for the guard:ASSIGNMENT_RULE_OBJECT,assignmentExpressionRefusals,organizationMemberStanding(+ typeOrganizationMemberStanding).@objectstack/spec(domain:spec#3 consented, mailbox comment 6672529419):PLATFORM_PROVIDED_OBJECT_NAMESgainssys_assignment_ruleunderplugin-sharing, betweensys_assignment_cursorandsys_queue, in the same commit as the census and inventory rows; the[ADR-0142 D5]comment on theassignment_ruleregistry entry drops "declared only: organization-authored routing is deferred" (ruling item 6), with the entry'sdescriptionand flags untouched; theAssignmentRuleSchemadocblock states how the two sources combine, in the order the engine evaluates them. No key, shape or.describe()text changes, andgen:schema/gen:docsregenerate with no diff (the docblock is not rendered into a generated file).Ledgers. The federated-injected-column-readers ledger records the two new
organization_idkeyings (both platform tables, not reads of a federated column); the cli organization-ownership inventory listssys_assignment_rule(fateattribution);scripts/platform-object-tenancy-census.jsonis regenerated.Tests
packages/objectql/src/ownership-queue.test.ts, three new top-level describes: "a user target owns the row only while the user is an active member" (5), "an organization's own rules go first, the declared rules are the fallback" (9), "assignmentExpressionRefusals" (3). The D5 boot now registerssys_memberwith a membership for the existing user-target case.packages/plugins/plugin-sharing/src/ownership-queue.test.ts, new describe "the sys_assignment_rule save guard": every refusal above by field, merged-row judging on update, a failed membership read refusing the save, re-binding not stacking hooks, and the boot report.packages/plugins/plugin-sharing/src/objects/sys-sharing-rule.by-object.test.ts: the by-object roster gainssys_assignment_rule.Ablations (each through
scripts/ablation-replace.mjs, mutation proven on disk by anchor count and blob hash, restore proven by blob == HEAD and an emptygit diff HEAD; the suites import the subject fromsrc, so no rebuild sits between):apply():[...own, ...fallback]→[...fallback, ...own]src/ownership-queue.test.tsrouteToUser:answer === 'member'→answer !== 'unconfirmable'src/ownership-queue.test.ts&& false)src/ownership-queue.test.tsAll three restored, then green.
Acceptance notes
packages/spechunk, consented bydomain:spec#3(6674042568, same terms as 6672529419).pnpm --filter @objectstack/spec run check:empty-statewas red onab98c3b8: it reportssys_assignment_rule.criteriaas an unclassified permissive empty state, because its description tells the admin that an empty condition takes every new record no earlier rule took. That is the real behaviour, so the description stays and the empty state is declared instead: onescopeentry inpackages/spec/scripts/liveness/empty-state-registry.mts, right after the declared rule'scriteriaentry, which stays byte-identical. With it the gate prints "all classified". No changeset line is owed for it (packages/spec/scripts/**does not publish). The entry, as committed:packages/spec/src/system/constants/platform-object-names.test.ts > registers exactly the objects each package declaresfails onsys_assignment_rule(measured: 1 failed / 13 passed without the line, 14/14 with it).skills/**line calls organization rules deferred or assignment rules declared-only (grep fordeferred/declared onlyoverskills/**: no assignment hit). The owed follow-up is teaching the new behaviour: the docs page's "Declare an assignment rule" section and the skill'sassignTo: { type: 'user' }line.errora stored row a later package's rule now shadows.sys_assignment_ruleonce per organization in the batch while the table is registered. Without the table, or for an object without the column, nothing is read (pinned: "the cheap path").assignment-rule-user-targetlint already warns against it). The changeset's Upgrading paragraph says so.sharing-plugin.ts(+4) and the plugin-sharing translations; fix(scripts,objectql): codemod-split-engine --members reads hand-written facet headers and inverts its import edits; move registerApp, registerDriver and registerHook #30716 moves code out ofengine.ts(registerApp / registerDriver / registerHook), not the insert body this PR's one-line call-site change sits in.4b6296e8, already pushed, so the line sits in a later commit of this PR (d86cdc3e, with the census and inventory rows) rather than rewriting published history. Both merge together in this PR, which is what the condition protects.plugin-sharing(besidesys_queueandsys_sharing_rule, whose model it follows) rather thanplatform-objects, and the call site changed isengine.ts;engine-insert.tsis untouched.Verification
Readings at head
ab98c3b8unless a line names another.ab98c3b8differs from796614dconly by moving one sentence ofsys_assignment_rule.criteria's description onto thedescription:line (so the empty-state gate attributes it tocriteria, notrequired).Package tests and typecheck
pnpm --filter @objectstack/objectql test— Test Files 556 passed (556), Tests 11086 passed (11086); lock VERDICT command-exit 0.pnpm --filter @objectstack/plugin-sharing test— Test Files 66 passed (66), Tests 1421 passed (1421); lock VERDICT command-exit 0.pnpm --filter @objectstack/cli exec vitest run --project unit src/utils/organization-ownership-inventory.test.ts— 26 passed (26). The cliintegrationtier is CI's; this diff touches no spawn entry.typecheckon objectql, plugin-sharing and cli (eachtsc --noEmitpluscheck:test-typecheck) — lock VERDICT command-exit 0.pnpm --filter @objectstack/spec test— Test Files 817 passed (817), Tests 25792 passed, 1 todo; lock VERDICT command-exit 0 (at796614dc).pnpm --filter @objectstack/spec exec vitest run --project repo src/system/constants/platform-object-names.test.ts— 14 passed (14); without the roster line, 1 failed / 13 passed.pnpm --filter @objectstack/plugin-sharing exec vitest run src/objects src/ownership-queue.test.tsatab98c3b8— 88 passed (88).pnpm --filter @objectstack/spec gen:schema && gen:docs— no file changed.Derived gates
796614dc,dispatch-gates --commandsderived 99 families (the same 99 atab98c3b8): 98exit 0, andpnpm --filter @objectstack/spec run check:empty-stateexit 1(see Acceptance notes).--ran: "99 derived famil(ies) accounted for — 99 run, 0 NOT-MEASURED (a DERIVED zero)".d86cdc3ewithout its roster line), it derived 77 families; each was run with its exit code recorded before any pipe. Five first answeredexit 3(prerequisite not met: a pinned fixture commit outside the shallow clone, and unbuiltdist/for the i18n and dual-build gates); after fetching that commit and building the closures each gate names, all five re-ran atexit 0.--ranon that run: "77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED".node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD— 5 commit messages carry no card relation and no model identifier.check-nul-bytesexit 0.pnpm lint(repo-wideeslint . --no-inline-config) — exit 0 on that pre-spec tree and again at796614dc;eslint --no-inline-configon the one fileab98c3b8changes — exit 0.Generated by Claude Code