Repository navigation
fix(plugins)!: business-unit recipients and approvers reach every member past 10,000 rows, and a failed member read refuses instead of answering nobody - #29647
Conversation
…ber past 10,000 rows, and a failed member read refuses instead of answering nobody The sys_business_unit_member reads in plugin-sharing (BusinessUnitGraphService expandUnitMembers / expandUsers, the primary-business-unit boot backfill) and plugin-approvals (expandBusinessUnitUsers) read to completion through the shared keyset walk and refuse a failed or truncated read; absence still answers no members. An expression approver's failed resolveAs expansion now logs the same "could not be expanded" warning the static approver types log, with the error. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbWdszDuXCYxfoR2ynB
…t member read The sys_business_unit_member reads are now a keyset walk (id > cursor, ordered by id, a page limit). The hand-rolled engine in business-unit-graph.test.ts stamps the id a stored row carries and models $and, $gt, orderBy and limit, so its 25 cases keep pinning the two tenant screens and the two widths. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JQwxbWdszDuXCYxfoR2ynB
📓 Docs Drift CheckThis PR changes 2 package(s): 16 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 6 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 13 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 4de4a45dc38105cac2f5a70ac44c0c36649e724b && git checkout 4de4a45dc38105cac2f5a70ac44c0c36649e724b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 3482f3d8d2c48ec8d2e3306f1ce26c0364e800c2 241a137fd1e3a8f7caf4b530a0e4752b232d8bf3 && git checkout -B drift-repro 3482f3d8d2c48ec8d2e3306f1ce26c0364e800c2 && git merge --no-ff 241a137fd1e3a8f7caf4b530a0e4752b232d8bf3
node scripts/docs-audit/affected-docs.mjs --json 3482f3d8d2c48ec8d2e3306f1ce26c0364e800c2
|
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS Contract-review-claim: session_01JQwxbacSXJQqAztgt4UMrS · head 241a137 · 2026-11-02T23:54Z |
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS Contract reviewServed-tier: ① Derived judgments
② Semver levelMatches: ③ Boundary flags
④ Design judgment
Implemented-by: VERDICT: PASS |
|
domain:server#4 · os-warren · session_01JQwxbacSXJQqAztgt4UMrS 席位核对ACCEPT ·
|
Requested by Warren · project thread
Fixes #29606
domain:server#4 · os-warren · session_01JQwxbWdszDuXCYxfoR2ynB
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: updated here.
content/docs/automation/approvals.mdxnow namesdepartmentand thesys_business_unit_memberread beside the team, position and tier reads for a failed holder read, and says that anexpressionvalue whoseresolveAsread failed logs the same line. No skill teaches a bound on business-unit members or what a failed member read answers.维护者速读
sys_user.primary_business_unit_id也读完全部主成员关系,读失败或读不完整就不写并记录原因。审批表达式resolveAs展开失败时,现在和静态审批人一样打出「could not be expanded」告警并带上错误。sys_business_unit_member的读本身失败(原来答空),属收窄,标题带!;1 万人以下仍是一次读、结果不变;队列(ownership queue)的业务单元成员规格在读失败时仍贡献 0 人(文件头声明的姿态),只是现在会记一次日志,队列文件未改动。回滚即 revert 本 PR。packages/plugins/plugin-sharing/src/business-unit-graph.ts(readMembers)、packages/plugins/plugin-approvals/src/approval-service.ts(expandBusinessUnitUsers、warnExpansionFailed)、packages/plugins/plugin-sharing/src/primary-bu-projection.ts(readPrimaryMemberships)。pnpm --filter @objectstack/plugin-sharing exec vitest run src/business-unit-member-reads-complete.test.ts,预期 14/14 通过(含 10,001 名成员全部在集合内、读失败抛错、缺表仍答无人、回填 10,001 行全部投影)。What changes
The
sys_business_unit_memberreads still read once at a 10,000-row bound and turned a failed read into nobody. This extends the family #29362 (PR #29497) and #29503 (PR #29602) closed for the team, role and position reads to the last membership object, through the same sharedkeysetWalkandisObjectAbsentError(@objectstack/types). No new walk spelling for a holder read: plugin-sharing's member reads go throughreadMembershipToCompletion(team-graph.ts), plugin-approvals' throughreadHoldersToCompletion. No new export from any package index.Item 1, site 1: sharing
BusinessUnitGraphService.expandUnitMembers(everybusiness_unitrecipient, and queuebusiness_unitspecs) andexpandUsers(everyunit_and_subordinatesrecipient, and queueunit_and_subordinatesspecs) read through one privatereadMembers, which callsreadMembershipToCompletion. A failed or incomplete read throws, and nothing is cached for it.[]made the retired-recipient revoke that a business-unit graph write triggers (bu-tree-recompute.ts) take the rule's grants from every member.OwnershipQueueService.expandSpecalready catches an expansion that throws, logs it once and contributes nobody (its declared posture). That file is untouched (plugins: a queue sharing recipient still revokes grants on a member-table outage, because the queue expander answers "nobody" for every consumer; refuse for the sharing-grant pass and the approver slate (ADR-0142 amendment first) #29604 holds it). A queue BU spec answers what it answered before on a failed read, and now logs it.Item 1, site 2: sharing primary-business-unit backfill (
primary-bu-projection.ts).is_primaryrows pages to completion throughkeysetWalk. A scan that fails, or a walk that cannot vouch for completion, writes nothing and warns with the consequence and the error. Absence of the object answers nothing to project, silently.{ updated: 0, refused: 0 }with a warning, and the existing pin ("a refused SCAN answers both counters, not a half-built result") keeps that shape. It useskeysetWalkdirectly, notreadMembershipToCompletion: this is a projection scan rather than a holder read, and the holder helper's refusal sentence speaks of grants.recompute, thebeforeDeletestash) arelimit: 1existence probes that a bound cannot cut, so they are unchanged.Item 1, site 3: approvals
expandBusinessUnitUsers.readHoldersToCompletion('sys_business_unit_member', …). A failed read throws, the approver-spec resolver's directory catch takes it, and thedepartment:IDslot falls back to its literal with thecould not be expandedwarning and itserror. Before, a failed read logged "expanded to nobody".Item 3:
resolveAsexpansion failure warns.resolveExpressionApproverscaught a faileddepartment/position/teamexpansion intousers = []and pushed theKIND:VALUEliteral with no log line. It now logs the same warningresolveApproverSpeclogs, with theerrorand theexpressionsource.warnExpansionFailed, which both resolvers call, so the two cannot drift.Item 2: measured, reported, not changed. See the report on the card and the Acceptance notes. ⛔
ownership-queue-service.tsis untouched.Tests
plugin-sharing/src/business-unit-member-reads-complete.test.ts(14 cases, real ObjectQL + SqlDriver over in-memory SQLite, shippedSysBusinessUnit/SysBusinessUnitMember, row ids scrambled against user ids):expandUnitMembers: 10,001 members all in the set and nobody from another unit or organization (2 reads); 9,999 members in one read; a failed read throws naming the unit and the driver error; a failed read is not cached; the object unregistered, or its table dropped, answers[].expandUsers(members split across the unit and its child): 10,001 (2 reads); 9,999 (1 read); a failed read throws; unregistered answers[].primary-bu-projection.test.ts: fixture rows now carry theirid(the scan seeks by it). The refused-scan case also asserts the warning and its error, and a new case pins that a scan that cannot vouch for completion writes nothing and warns.business-unit-graph.test.ts(fixture triage, no assertion changed): its hand-rolled engine now stamps theida stored row carries and models$and,$gt,orderByandlimiton the member side, so its 25 cases keep pinning the two tenant screens and the two widths.plugin-approvals/src/business-unit-holders-complete.integration.test.ts(11 cases, real engine):expandBusinessUnitUsers: 10,001 (2 reads, nobody from another unit or organization); adepartmentapprover routes to all 10,001; 9,999 in one read; a failed read throws; what the caller sees isdepartment:bu_rootplus thecould not be expandedwarning with theSQLITE_IOERRerror; unregistered answers the literal with no failure warning; dropped table answers[].resolveAs: 'department'control expands to the members with no failure warning; a faileddepartmentread keeps{ id: 'department:bu_root', subGroup: 'bu_root' }and logs onecould not be expandedwarning carryingtype,value,organizationId,expressionand the error; a failedteamread throughresolveAswarns the same way; an empty unit keeps the literal with no failure warning.Ablation (source paths: these suites import the files under test relatively, so no build is in the path)
2324d29f), then a trapped script checked out the three source files at the base (4d8095ba) and confirmed the mutation landed (readMembers0 hits,keysetWalkin the projection 0,warnExpansionFailed0,limit: 10000back in approvals 1).Tests 9 failed | 11 passed (20), the red ones being every 10,001, failed-read, not-cached, truncated-scan and quiet-absence case. Approvals:Tests 6 failed | 5 passed (11), the red ones being the 10,001 cases, both failed-read cases and both item-3 warning cases.git checkout HEAD -- PATH:git diff HEAD0 bytes, and each file'sgit hash-objectequals itsHEADblob (ddbfccfd…,8ccd37de…,6d3a5423…).Acceptance notes
(narrowing)arm. Review face:node scripts/pm/record-recognisers.mjs --template --paths …names no path face and defers to the changeset'sClause-②:line, so it is--face narrowing(CONTRACT_REVIEW_TIER).QUEUE_READ_LIMITcan silently drop a holder. One authored queue with 10,001usermember rows:expandMembersanswered 10,000 and missedusr_010000,isMemberansweredfalsefor it,queuesOfanswered[]for it, and no warning was logged. 10,001 authored queues:queuesOfmissed a member of the 10,001st queue, no warning. 10,001teammember rows:queuesNamingmissed the queue past the cap, so rules naming it are not re-evaluated, no warning. ⛔ Not changed:ownership-queue-service.tsbelongs to plugins: a queue sharing recipient still revokes grants on a member-table outage, because the queue expander answers "nobody" for every consumer; refuse for the sharing-grant pass and the approver slate (ADR-0142 amendment first) #29604.APPROVER_INDEX_CAPinorganizationAdministratorscan drop an administrator, loudly. 10,001sys_memberrows with the one administrator on the last row:{"ids":[]}; on the first row:{"ids":["usr_010000"]}. Both runs loggedorganization-administrator probe hit its window — results may be truncated. The read has noorderBy, so which members fall off is driver order. It is a declared bound with a loud warning, so per the card it is reported and left; the choice is on the card report.sys_business_unithierarchy reads in both plugins. Sharingdescendants(limit: 1000, catch →[]) andseedIsUsable(catch →false, and the empty answer is cached for the pass), approvals' seed check (catch →[]) and descent (limit: 1000, catch →[]). A failed unit read still answers nobody there. The new not-cached pin had to key its fault to the member object for exactly this reason: a fault on the seed check is cached as[].protocol.ts(frozen),packages/specandownership-queue-service.tsare untouched.Verification
Head:
241a137f(mergesorigin/mainfa07650b; the incoming commit touches none of these packages). Every reading below was taken on that head, except the approvals suite, typechecks and the first derived-gate run, which were taken on0443a73e. The only commit after it changesbusiness-unit-graph.test.ts, and the sharing suite was re-run on241a137f.Package suites and typecheck
@objectstack/plugin-sharing: Test Files 63/63, Tests 1360/1360 (241a137f); typecheck exit 0.@objectstack/plugin-approvals: Test Files 110/110, Tests 1434/1434; typecheck exit 0.Full dogfood battery (the title carries
!and the declaration a(narrowing)arm)turbo run build --concurrency=2 --filter='./packages/**'under the lock,VERDICT command-exit 0, 78/78 tasks (covers the dogfood closure).pnpm --filter @objectstack/dogfood testunder the lock on241a137f:VERDICT command-exit 0, Test Files 398 passed and 3 skipped (401), Tests 3280 passed and 28 skipped (3308), 0 failed.Derived gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: 94 families derived (the same list on0443a73eand241a137f). All 94 were run, each with its exit code recorded.check-plugin-teardown-shape --self-testaftergit fetch --unshallow origin main;check:skill-examples,check:dual-build-cjs-loadsandcheck:i18nafterturbo run build --filter='./packages/**'(78/78 tasks). All four then exited 0.--ranreconciliation:94 derived, 94 run, 0 NOT-MEASURED, 0 UNRUN(a DERIVED zero).pnpm lint(repo-wide eslint,--no-inline-config) on241a137f: exit 0.Generated by Claude Code