Repository navigation
feat(spec): IOwnershipQueueService lets a caller ask for a refusal on a failed member read (onFailedRead) - #30318
Conversation
… a failed member read (onFailedRead) ADR-0142 D2 (as amended) says each queue consumer declares at call time whether a failed member read answers nobody or refuses. expandMembers gains an optional onFailedRead beside purpose, isMember gains an optional trailing options argument with the same key, both typed OwnershipQueueFailedReadAnswer (nobody | refuse, default nobody). Contract only; the producer and the D6/D7 callers land with #29604. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfHeHezsXgrF5MEs4N
📓 Docs Drift Check4 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 170 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 4e0943281d97d0993a2f89f8a35ad5f6f978674c && git checkout 4e0943281d97d0993a2f89f8a35ad5f6f978674c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c1240a38ac2c23c8d526e476a20edeabbbefd6f6 41db2e116df54c05dfb38be4cfc0a0d5d58a5a24 && git checkout -B drift-repro c1240a38ac2c23c8d526e476a20edeabbbefd6f6 && git merge --no-ff 41db2e116df54c05dfb38be4cfc0a0d5d58a5a24
node scripts/docs-audit/affected-docs.mjs --json c1240a38ac2c23c8d526e476a20edeabbbefd6f6 |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract-review-claim: session_01WGufzfgvGexKn9vLHsy89K · head 12fb7d3 · 2026-11-05T05:59Z Full review of the first head at |
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentThe signal is the one D2 says the consumer sends, spelled once as an exported value type both methods read, trailing and optional on both, and kept off the Implemented-by: VERDICT: PASS |
… the code half, and where the activation-ledger read stands Contract review should-fixes: the 'refuse' bullet says an implementation written before the key answers 'nobody' and plugin-sharing honours 'refuse' from the code half on; the table list names sys_business_unit_member and sys_business_unit and says the sys_metadata_activation read is not a failed read in this sense (its failure still reads switched on). Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WGufzfHeHezsXgrF5MEs4N
|
domain:spec#3 · os-support-ai · session_01WGufzfgvGexKn9vLHsy89K 维护者速读
席位意见:fable 契约复审 PASS,没有必须修的。两条注释准确性建议已推送采纳,席位亲自核对了差异。 席位核对 — ACCEPT · domain:spec#3 · head
|
Requested by Yibo Wang · project thread
domain:spec#3 · os-support-ai · session_01WGufzfHeHezsXgrF5MEs4N
Fixes #30251
Part of #29604 (this PR is the contract only; the producer in
plugin-sharingand the D6/D7 callers that pass'refuse'stay with #29604's code half, part 2)Clause-②: yes (widening: an optional
onFailedReadonexpandMembers' options and an optional options argument onisMember; precedent PR #30015)Metadata-protocol: none
Skills-docs: none — a call-time option of a kernel service contract that no metadata author writes;
skills/**andcontent/docs/**name the ownership-queue service but none of its methods (git grep -n -i "expandMembers|IOwnershipQueueService" -- skills content/docs: 0 hits)Prior rulings read: onfailedread,ownership queue,expandmembers → 0 hits; none; thread: none; repo: objectstack-ai/objectstack
维护者速读
IOwnershipQueueService)新增一个可选开关onFailedRead,调用方可以说明「成员表读失败时」要的是「当作没人」还是「直接报错」。只改类型和文档注释,不改任何运行逻辑。'nobody'),所有现有调用和实现原样编译、原样运行;回滚即还原本 PR。真正改行为的是 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 的第二部分。packages/spec/src/contracts/ownership-queue-service.ts(新类型OwnershipQueueFailedReadAnswer的注释和两个方法签名)、ownership-queue-failed-read.pin.test.ts。pnpm --filter @objectstack/spec check:test-typecheck,预期 OK;pin 文件里四个@ts-expect-error证明非法取值编译不过。What changes
OwnershipQueueFailedReadAnswer = 'nobody' | 'refuse'(@objectstack/spec/contracts). Its docblock carries the whole rule, citing ADR-0142, D2 "Membership", the amended paragraph "An empty expansion is a fact; a failed read is a fault":sys_queue,sys_queue_member,sys_team_member,sys_user_position,sys_memberthrough the position transition limb, and the business-unit membership and tree (D2 names the BU reads too, so the list keeps them); a table the composition does not carry is absence, not failure;'nobody': the default and today's answer, logged atwarn; taken by the access path (D3), the claim admission (D4) and the assignment pool (D5);'refuse': a failed read rejects with a failure that names the read, never[], a partial set orfalse; taken by the approver slate (D6) and the sharing-recipient grant recompute (D7);[], and a name that resolves nowhere is still[]/false;QueueExpansionPurpose(contract review 6600699486, carried in 6600766777).expandMembers(name, organizationId, options?: { purpose?; onFailedRead? }): the new key sits besidepurpose; the method docblock points at the type.isMember(userId, name, organizationId, options?: { onFailedRead? }): a new optional trailing argument, so every three-argument call compiles unchanged. Under'refuse',falseis answered only when every read behind it succeeded.api-surface/contracts.jsonandexport-origins/contracts.jsongain the one new type (check:generated --fix, rebuilt dist).@objectstack/specpatch(Metadata-protocol: nonegrades every entrypatch).Caller census
git grep -n "IOwnershipQueueService\|expandMembers(\|isMember("at959c26be, every hit outside generated JSON and the contract itself:plugin-sharing/src/ownership-queue-service.ts:127OwnershipQueueService implements IOwnershipQueueServiceexpandMembers(…, options?: { purpose? }),isMember(userId, name, org)plugin-sharing/src/sharing-rule-service.ts:432, :1747, :2186(D7)Pickof 'expandMembers' | 'isMember', calls without options'refuse'plugin-sharing/src/sharing-service.ts:557, :952(D3)Pickof 'queuesOf' | 'isMember', 3-argisMember'nobody'plugin-sharing/src/index.ts:177plugin-approvals/src/approval-service.ts:1638, :3347(D6)Pickof 'expandMembers',{ purpose: 'addressing' }onFailedRead: 'refuse'hereobjectql/src/assignment-rules.ts:74, :293(D5)Pickof 'resolveQueue' | 'expandMembers', 2-arg call'nobody'plugin-security/src/ownership-queue-gate.ts:32,security-plugin.ts:2670,security-plugin-authz-middleware.ts:1602(D4),security-plugin-explain-access.ts:253, :512Pickof 'resolveQueue' | 'isMember', 3-argisMemberplugin-security/src/kernel-queue-membership.ts:44,security-plugin.ts:2683Pickof 'queuesOf'plugin-hono-server/src/current-user-endpoints.ts:317Pickof 'hasAnyQueue'runtime/src/domains/catalog-activation.ts:166Pickof 'declaredQueueNamed'plugin-sharing/src/ownership-queue.test.ts,plugin-security/src/kernel-queue-membership.test.ts:113No caller breaks: the key is optional on an options bag that was already optional, and the
isMemberargument is a new optional trailing one.Tests
packages/spec/src/contracts/ownership-queue-failed-read.pin.test.ts, compiled bycheck:test-typecheckundertsconfig.test.json:Assert/Eqpins: the answer type is exactly'nobody' | 'refuse'; both members'onFailedReadread it;expandMembers' option keys are exactlypurpose | onFailedReadandisMember's exactlyonFailedRead;purposekeeps its type; the two value sets are disjoint;expandMemberswith{ purpose? }only, a 3-paramisMember) is still anIOwnershipQueueService;{},{ purpose },{ onFailedRead: 'refuse' }, both keys, the 3-arg and 4-argisMember;@ts-expect-errorshapes that must not compile:onFailedRead: 'throw','empty','addressing', andpurposeonisMember;Reverse verification (two mutations, each restored by scripts/ablation-replace.mjs to the HEAD blob)
isMember'soptions?: { onFailedRead?: OwnershipQueueFailedReadAnswer },→check:test-typecheckred,ownership-queue-failed-read.pin.test.ts: 4 type error(s); restored, blob2bd06d9682c3== HEAD,git diff HEADempty.'nobody' | 'refuse' | 'throw'→ red,2 type error(s)(theEqpin and the now-unused@ts-expect-error); restored, blob == HEAD.Both read through
tsconfig.test.json, which resolves the contract fromsrc, so no dist rebuild was needed for the legs.Local runs, all on head
12fb7d30, each underos-verify-lockwithNODE_OPTIONS=--max-old-space-size=4096:pnpm --filter @objectstack/spec typecheck(tsc, scripts,check:test-typecheck) and the new pin file: VERDICT command-exit 0;Test Files 1 passed (1).pnpm --filter @objectstack/spec test: VERDICT command-exit 0;Test Files 813 passed (813),Tests 25576 passed | 1 todo (25577).^...closure built first, 36 turbo tasks):typecheckof@objectstack/plugin-sharing(the implementer),@objectstack/plugin-approvals,@objectstack/plugin-securityand@objectstack/objectql: VERDICT command-exit 0, eachcheck:test-typecheck: OK.plugin-hono-serverandruntimepick onlyhasAnyQueue/declaredQueueNamed, members this PR does not touch, so they were not re-typechecked (CI's full typecheck covers them).check:generated --fixrebuiltpackages/spec/distand regeneratedapi-surface/andexport-origins/(the two artifacts it proved stale); a later plaincheck:generatedis green.Acceptance notes
switchedOff, answered "on" when its read fails) is not named in the new docblock: contract review 6600766777 leaves that posture for 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's code half to decide and say.queuesOfgets noonFailedRead: no ADR-0142 recompute consumer reads it, and the card does not ask for it.Verification
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 80 families at12fb7d30; all 80 ran, every one exit 0.--ranreconciliation:80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED.check-plugin-teardown-shape.mjs --self-testexit 3: shallow clone missing the pinned fixture commit; fetched it, then exit 0 (48 cases pass);spec check:generatedexit 1: the runner had noNODE_OPTIONS, andcheck:test-typecheck's tsc ran out of the default 2 GB heap; re-run with 4 GB, exit 0;check:dual-build-cjs-loadsexit 3 (no dist for 38 packages); after a fullturbo run build(120 tasks), exit 0: 116 require entry points across 71 packages load.check-changeset-no-major.mjs --base origin/mainexit 0 with the@objectstack/specpatchentry underMetadata-protocol: none.test:repoare CI's runs.Human Minimum
packages/specholds no runtime logic. (a TypeScript contract type only)pnpm lintpasses. (repo-wide lint is CI's run)main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.🤖 Generated with Claude Code
https://claude.ai/code/session_01WGufzfHeHezsXgrF5MEs4N