You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
plugins: sharing expandRole and approvals role-tier recipients read sys_member at a 10,000-row bound and turn a failed read into an empty list, so a large or unreadable membership silently shrinks the set #29362
Filing gate: ① same-family observation of the #29281 dev (os-dev-report 6577634380, out_of_scope_findings), source reading only, not reproduced. Filed by domain:server seat 3. ⛔ Not a claim. Suggested-grade: p3 · the same defect class as #29281 (a membership read at its bound, or a failed read, silently shrinks a set), at a 10× larger bound.
Reach: an organization or role whose sys_member rows pass 10,000, or a moment when that read fails.
Reader who acts: a domain:server seat (plugin-sharing, plugin-approvals).
What happens (source reading)
plugin-sharingteam-graph.tsexpandRole (~L233) reads sys_member with limit: 10000, and a failed read is caught and becomes [].
plugin-approvalsapproval-service.ts role-tier recipients (~L3365) read the same way: limit: 10000, catch → [].
So past 10,000 rows the expanded set is silently short, and on a failed read it is silently empty. That means sharing grants or approval recipients go missing, with no signal. By contrast, the approvals member screen (MEMBER_SCREEN_READ_LIMIT, ~L2999) is already loud.
Wanted
Measure both first. Then, as #29281 does: page the read to completion when it comes back at its bound (or reuse #29281's completion read if it fits), and make a failed read loud instead of an empty set. Say per site whether that is a narrowing (a call that answered now refuses).
Claim: PM loop round 1 · card thread · 2026-11-02T15:19Z
Session: session_01ADHkmQJdgarUjPLNgFBY67 (the seat); the card thread is session_01ADHkmS2E1xdkLMJPnabYgC
Account: os-steve (the seat's linked user; the card's assignee)
Branch: claude/project-thread-qcavvn
Worktree: the card thread's own container checkout
Domain: domain:server
Seat: domain:server#3
Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface: packages/plugins/plugin-sharing/src/team-graph.ts (expandRole), packages/plugins/plugin-approvals/src/approval-service.ts (role-tier recipients) and their tests; packages/core/src/security/resolve-authz-context.ts and core index only if the completion helper is shared (declared in open_questions first); .changeset/.
Container & model: card thread cmsg_01FdL1XeUyvVNxLreh3F4Qi95XUAuk2vpvE7ZaE2sQ18jS, mode:thread, model: opus (--tier names no path mandate: PM's call, default), effort: high
Clause-②: no (narrowing)
Metadata-protocol: none
Responsibility: the card thread measures, implements and opens the draft PR; the seat rules open questions, reviews, ACCEPTs and lands. The Clause-② value assumes a failed read now refuses and nothing is exported; the seat amends it if the core helper is shared
Thread-read: 6580348462
Premise: holds (site re-read on origin/main7b1ae338 by the seat before dispatch)
Serial constraints cleared: #29281 (PR #29356) merged f72bc9e5; no other seat-3 card on plugin-sharing or plugin-approvals
Resume-from: none — fresh from origin/main
State: pm:queue → pm:dispatched, assigned to os-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29362-delta.md.
{
"issue": 29362,
"status": "done",
"branch": "claude/project-thread-qcavvn",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29497",
"session": "session_01ADHkmS2E1xdkLMJPnabYgC",
"premise_still_valid": true,
"summary": "plugin-sharing TeamGraphService.expandRoleUsers and plugin-approvals expandMembershipTierUsers now read sys_member to completion through the existing shared keysetWalk (@objectstack/types), in 10,000-row pages ordered by id. Under the bound this is still one read with the same set. A failed read, or a walk reporting truncated, now throws instead of answering []. Sharing: the expansion rejects, so that rule's reconcile or revoke fails, the existing per-rule catches warn, and the rule's grants stay (before, the retired-recipient revoke wiped them). Approvals: resolveApproverSpec's directory catch keeps the error, the slot falls back to its literal, and the warning says the read failed and carries error. A missing sys_member (isMissingTableError, or the engine's OBJECT_NOT_FOUND for sys_member) still answers []. Core is untouched and nothing new is exported; #29398 stays open.",
"tests": "head bf9ca7d7. New real-engine pins (ObjectQL + SqlDriver, better-sqlite3 :memory:, shipped SysMember): sharing team-graph-role-holders-complete.test.ts 8/8; approvals member-tier-holders-complete.integration.test.ts 7/7. Reds on origin/main 7b1ae338 with the tests committed first: sharing 5 failed / 2 passed (expected 10,000 to equal 10,001 holders; promise resolved [] instead of rejecting); approvals 5 failed / 1 passed. Full suites: plugin-sharing Test Files 61/61, Tests 1324/1324; plugin-approvals Test Files 108/108, Tests 1388/1388; both typechecks exit 0. Ablations via ablation-replace.mjs (anchor 1->0, blob moved, restored blob == HEAD, git diff HEAD empty; no dist leg, since the suites import plugin src): S1 sharing break-after-first-page 2 red; S2 sharing catch->[] 3 red; A1 approvals break 2 red; A2 approvals catch->[] 3 red.",
"gates": "dispatch-gates --commands on bf9ca7d7: 63 derived, 63 run, all exit 0. --ran: '63 run, 0 NOT-MEASURED'. Three first ran exit 3 (prerequisite) and were re-run green: teardown self-test (fetched fixture 621a4876), dual-build-cjs-loads (after turbo build ./packages/**), type-check-debt (was queued behind the dogfood lock). pnpm lint exit 0. PR check-runs: in_progress at report time.",
"dogfood_battery": "bf9ca7d7 · VERDICT batch-last-exit 1 (build && test; held 37m08s) · Test Files 2 failed / 395 passed / 3 skipped (400) · Tests 4 failed / 3261 passed / 28 skipped (3293). Failures: flow-runas.dogfood.test.ts x3 (trigger answers 404 RECORD_NOT_FOUND on runas_note) and declarative-endpoint-anonymous-guest.dogfood.test.ts x1 (flowContexts length 0). Not this change: with both src files restored to aca58cdb6 and both plugins force-rebuilt, the same two files gave the same 4 failures (4 failed / 8 passed). Probable main-red, carried by the seat.",
"files_changed": [".changeset/29362-role-holders-complete.md", "packages/plugins/plugin-sharing/src/team-graph.ts", "packages/plugins/plugin-sharing/src/team-graph-role-holders-complete.test.ts", "packages/plugins/plugin-sharing/src/position-graph.test.ts", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/member-tier-holders-complete.integration.test.ts"],
"line_budget": "+569 / -29 across 6 files (598 changed lines, under 3,000)",
"deviations": ["Forced test edit: position-graph.test.ts fake sys_member rows gain an id (keysetWalk treats a row without its key as truncated); no assertion changed. Named in the PR body.", "Dogfood control leg: the --absent dist preflight for plugin-sharing used a non-unique marker ('could not be read' also survives from pre-existing comments in sharing dist) and exited 1. The approvals --absent preflight exited 0. The sharing control rested on a --force rebuild from src with 0 occurrences of the new symbols.", "pr_create via relay carries no assignees (os-dev rule); the harness's assignee/reviewer step was skipped."],
"mcp_calls": "1 — mcp__github__pull_request_read (get), read only",
"api_writes": "2 — relay pr_create (POST pulls, #29497); this os-dev-report comment (post-stamped.mjs on #29362)",
"open_questions": [
{"question": "Share vs local paging: is keysetWalk (@objectstack/types) plus about 25 lines of per-site glue acceptable as the 'no third copy' answer, rather than exporting core's readOrganizationMembersToCompletion?", "options": ["A: keep as is. The cursor loop is the shared keysetWalk; core is not widened; Clause-② stays no (narrowing).", "B: generalise and export the core helper (yes (narrowing)); both plugins call it, and #29398 folds in."], "recommendation": "A, because the core helper is keyed to organization_id, throws a 503 authz error and warns about org_user_ids. keysetWalk is already the platform's one public walker, and both plugins already import it."},
{"question": "An unregistered sys_member (the engine's OBJECT_NOT_FOUND with object sys_member) is treated as no holders, like a missing table, following sys-metadata-repository.ts. Accept?", "options": ["A: accept. Approvals integration compositions without SysMember keep routing.", "B: only isMissingTableError counts as absence. Those compositions then refuse, and their tests need SysMember registered."], "recommendation": "A, because it is attributed to sys_member, comes from the registry rather than the driver, and cannot hide an outage."}
],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: source reading only, not reproduced · same family: plugin-sharing TeamGraphService.expandUsers reads sys_team_member at limit 10000 with catch -> [], and both plugins' expandPositionUsers sys_user_position limb reads at limit 10000 with the catch swallowed · dedupe words: sys_team_member limit 10000, sys_user_position limit 10000 catch, team expansion truncation · carrier: seat 3 (the family's close-out card)",
"class: a · reach: source reading only · plugin-sharing holdsRole / teamHasMember (limit 1, catch -> false) answer false on a failed read, where expandRoleUsers now throws, so a per-user recompute could revoke one user's grant on an outage · dedupe words: holdsRole catch false, recipient-membership-recompute failed read · carrier: seat 3",
"class: a · reach: dogfood on bf9ca7d7 and the control leg on aca58cdb6 src · flow-runas.dogfood.test.ts (3) and declarative-endpoint-anonymous-guest.dogfood.test.ts (1) fail with and without this change · carrier: seat 3 (main-red triage)"
]
}
Read: os-dev-report 6582502987; the CONTRACT_REVIEW_TIER record on PR #29497 (narrowing face, PASS on bf9ca7d7); main's CI runs 41477790925 (schedule, cafe8ee6, dogfood shards 1/3 and 3/3 red) and 41441845128 (schedule, 27715752, all green).
Open question 1: A. Keep keysetWalk plus the per-site glue. ⛔ No core export.
Open question 2: A. The registry's OBJECT_NOT_FOUND on sys_member counts as absence. It is raised before any driver is consulted, so it cannot hide an outage.
Dogfood: not this PR's. The same 4 tests (flow-runas ×3, declarative-endpoint-anonymous-guest ×1) fail on main's scheduled full battery at cafe8ee6. That main-red is 4-hourly full run: red on main (CI) #29494 (devx, p0). This PR's head runs no dogfood shard, which is the gate's design for pull_request. Nothing to do here.
Should-fixes from the review that ride this PR (small, and caused by the throw this PR introduces):
(a)SharingRuleService.evaluateAllForRecord (sharing-rule-service.ts around L1400-L1421): add per-rule isolation in the loop, as evaluateAllRulesForObject already has. Then one position rule that now refuses does not skip the later rules' reconcile for that record. Pin it: two rules, the first refusing, and the second still reconciles.
(b)content/docs/automation/approvals.mdx: one line naming the new "could not be expanded — a directory read failed" warning, beside the existing "expanded to nobody" one.
(c) The resolveApproverSpec warning wording: the try also wraps the read filter (bounded()), so say "a directory or read-filter lookup failed".
Declaration stands:Clause-②: no (narrowing), Metadata-protocol: none, patch on both plugins. The record's PASS covers bf9ca7d7. After your push the seat re-reads the delta and the check-runs on the new head before the ACCEPT.
Run both plugins' full suites, typecheck, and an ablation of (a) before pushing. Report the new head when done.
{
"issue": 29362,
"status": "done",
"branch": "claude/project-thread-qcavvn",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29497",
"session": "session_01ADHkmS2E1xdkLMJPnabYgC",
"premise_still_valid": true,
"summary": "Round 2 (seat fixes) on head 4c34c3c8, which merges origin/main after #29451. (a) SharingRuleService.evaluateAllForRecord now catches per rule, as evaluateAllRulesForObject does: a rule whose recipients cannot be read keeps its grants and warns '[sharing-rule] record reconcile failed for rule', and the record's other rules still reconcile. (b) approvals.mdx names the new warning. (c) The warning now reads 'a directory or read-filter lookup failed'. Round 1 is unchanged: role and membership-tier holder reads complete through keysetWalk, a failed or incomplete read throws, and a missing sys_member answers []. Both open questions are ruled A.",
"tests": "head 4c34c3c8 (plugin suites on 8fa01d1c; the merge touched neither plugin). New pin: recipient-width.test.ts › '#29362 — the per-record pass is best-effort per rule'. The first (position) rule's sys_member read fails, its grant stays, the second (user) rule restores its grant, and one warning names the failed rule. Ablation: rethrow in the catch via ablation-replace.mjs (anchor 1->0, blob moved, restored blob == HEAD, git diff HEAD empty). The pin goes red: '[sharing] the holders of role 'sales_rep' could not be read: SQLITE_IOERR'. Approvals caller-sees pin tightened to the new wording. Full suites: plugin-sharing Test Files 61/61, Tests 1325/1325; plugin-approvals Test Files 108/108, Tests 1388/1388; both typechecks exit 0. Round 1 readings stand: reds on 7b1ae338, and four ablation legs red then restored.",
"gates": "dispatch-gates --commands on 4c34c3c8: 93 derived (the docs file added 30 families), 93 run, all exit 0. --ran: '93 run, 0 NOT-MEASURED'. check:type-check-debt first ran exit 3 (queued behind the dogfood lock) and was re-run exit 0. pnpm lint exit 0. PR check-runs: not yet read for this head.",
"dogfood_battery": "4c34c3c8 · VERDICT batch-last-exit 0 (build && test; held 37m15s) · Test Files 397 passed / 3 skipped (400) · Tests 3277 passed / 28 skipped (3305) · no failures. The round-1 failures were main-red #29426, fixed by #29451.",
"files_changed": [
".changeset/29362-role-holders-complete.md",
"packages/plugins/plugin-sharing/src/team-graph.ts",
"packages/plugins/plugin-sharing/src/team-graph-role-holders-complete.test.ts",
"packages/plugins/plugin-sharing/src/position-graph.test.ts",
"packages/plugins/plugin-approvals/src/approval-service.ts",
"packages/plugins/plugin-approvals/src/member-tier-holders-complete.integration.test.ts",
"packages/plugins/plugin-sharing/src/sharing-rule-service.ts",
"packages/plugins/plugin-sharing/src/recipient-width.test.ts",
"content/docs/automation/approvals.mdx"
],
"line_budget": "+657 / -36 across 9 files (693 changed lines, under 3,000)",
"deviations": [
"Forced test edit: position-graph.test.ts fake sys_member rows gain an id (keysetWalk treats a row without its key as truncated); no assertion changed. Named in the PR body.",
"pr_create via relay carries no assignees (os-dev rule); the harness's assignee/reviewer step was skipped.",
"The first round-2 ablation attempt was a no-op: ablation-replace refused because the replacement still contained the anchor (anchor x1 -> x1), restored, and exited 1 with no test run. Re-run with an anchor the replacement removes, which landed (1 -> 0) and went red.",
"A gate run on 8fa01d1c was stopped at 57/93 (killed by recorded PID) when the seat asked for a main merge. All 93 were re-derived and re-run on 4c34c3c8.",
"The PR body is not patched by the card thread. A round-2 body patch is at /mnt/project-files/server-seat3/29497-body-r2-patch.md for the seat to send."
],
"mcp_calls": "1 (whole card) — mcp__github__pull_request_read (get), read only",
"api_writes": "3 (whole card) — relay pr_create (#29497); round-1 os-dev-report (issuecomment-6582502987); this round-2 os-dev-report",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"Filed by the seat as #29503 (sys_team_member and sys_user_position limbs, holdsRole/teamHasMember). No new findings this round."
]
}
git grep -n "async function readRoleHoldersToCompletion" origin/main -- packages/plugins/plugin-sharing/src/team-graph.ts
line 176
Landed: merged as 2789ba7d. Sharing-rule role expansion and approvals membership-tier recipients now read sys_member to completion (10,000-row pages, id keyset via keysetWalk). A failed or incomplete read now refuses instead of answering "nobody". Sharing recompute for one record is isolated per rule: a rule whose holders cannot be read keeps its existing grants and warns, and the other rules recompute as usual.
The seat ACCEPT is on the PR as 6584657484. The narrowing-face contract reviews are 6582769583 and 6584482384 (the second on the revised head). Fixes #29362 closed this card on merge.
Activity
objectstack-fleet commentedon Nov 2, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: p2 ·
domain:server·pm:queue· 2026-11-02T14:58Z⛔ Not a claim.
expandRoleand approvals role-tier recipients readsys_memberat a 10,000-row bound and turn a failed read into an empty list. Same class as core/security: org_user_ids is built from a sys_member read capped at 1000 rows, so in an organization with more than 1000 members the rest of the members' sys_user rows are invisible to every non-admin user #29281, so a large membership silently shrinks a recipient set. p2.Generated by Claude Code
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Claim: PM loop round 1 · card thread · 2026-11-02T15:19Z
Session:
session_01ADHkmQJdgarUjPLNgFBY67(the seat); the card thread issession_01ADHkmS2E1xdkLMJPnabYgCAccount:
os-steve(the seat's linked user; the card's assignee)Branch:
claude/project-thread-qcavvnWorktree: the card thread's own container checkout
Domain:
domain:serverSeat:
domain:server#3Ruling-ref: the maintainer, verbatim, in the project thread: 「开 server 2、3、5 号席位, 前任已经下班,接手所有开发,包括跟进之前未完成的开发落地。」
File surface:
packages/plugins/plugin-sharing/src/team-graph.ts(expandRole),packages/plugins/plugin-approvals/src/approval-service.ts(role-tier recipients) and their tests;packages/core/src/security/resolve-authz-context.tsand core index only if the completion helper is shared (declared in open_questions first);.changeset/.Container & model: card thread
cmsg_01FdL1XeUyvVNxLreh3F4Qi95XUAuk2vpvE7ZaE2sQ18jS,mode:thread,model: opus(--tiernames no path mandate: PM's call, default),effort: highClause-②: no (narrowing)
Metadata-protocol: none
Responsibility: the card thread measures, implements and opens the draft PR; the seat rules open questions, reviews, ACCEPTs and lands. The Clause-② value assumes a failed read now refuses and nothing is exported; the seat amends it if the core helper is shared
Thread-read: 6580348462
Premise: holds (site re-read on
origin/main7b1ae338by the seat before dispatch)Serial constraints cleared: #29281 (PR #29356) merged
f72bc9e5; no other seat-3 card on plugin-sharing or plugin-approvalsResume-from: none — fresh from
origin/mainState:
pm:queue→pm:dispatched, assigned toos-steve, recorded by this claim. Order: project file server-seat3/standard-order.md + server-seat3/29362-delta.md.objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmS2E1xdkLMJPnabYgC
os-dev-report
{ "issue": 29362, "status": "done", "branch": "claude/project-thread-qcavvn", "pr": "https://github.com/objectstack-ai/objectstack/pull/29497", "session": "session_01ADHkmS2E1xdkLMJPnabYgC", "premise_still_valid": true, "summary": "plugin-sharing TeamGraphService.expandRoleUsers and plugin-approvals expandMembershipTierUsers now read sys_member to completion through the existing shared keysetWalk (@objectstack/types), in 10,000-row pages ordered by id. Under the bound this is still one read with the same set. A failed read, or a walk reporting truncated, now throws instead of answering []. Sharing: the expansion rejects, so that rule's reconcile or revoke fails, the existing per-rule catches warn, and the rule's grants stay (before, the retired-recipient revoke wiped them). Approvals: resolveApproverSpec's directory catch keeps the error, the slot falls back to its literal, and the warning says the read failed and carries error. A missing sys_member (isMissingTableError, or the engine's OBJECT_NOT_FOUND for sys_member) still answers []. Core is untouched and nothing new is exported; #29398 stays open.", "tests": "head bf9ca7d7. New real-engine pins (ObjectQL + SqlDriver, better-sqlite3 :memory:, shipped SysMember): sharing team-graph-role-holders-complete.test.ts 8/8; approvals member-tier-holders-complete.integration.test.ts 7/7. Reds on origin/main 7b1ae338 with the tests committed first: sharing 5 failed / 2 passed (expected 10,000 to equal 10,001 holders; promise resolved [] instead of rejecting); approvals 5 failed / 1 passed. Full suites: plugin-sharing Test Files 61/61, Tests 1324/1324; plugin-approvals Test Files 108/108, Tests 1388/1388; both typechecks exit 0. Ablations via ablation-replace.mjs (anchor 1->0, blob moved, restored blob == HEAD, git diff HEAD empty; no dist leg, since the suites import plugin src): S1 sharing break-after-first-page 2 red; S2 sharing catch->[] 3 red; A1 approvals break 2 red; A2 approvals catch->[] 3 red.", "gates": "dispatch-gates --commands on bf9ca7d7: 63 derived, 63 run, all exit 0. --ran: '63 run, 0 NOT-MEASURED'. Three first ran exit 3 (prerequisite) and were re-run green: teardown self-test (fetched fixture 621a4876), dual-build-cjs-loads (after turbo build ./packages/**), type-check-debt (was queued behind the dogfood lock). pnpm lint exit 0. PR check-runs: in_progress at report time.", "dogfood_battery": "bf9ca7d7 · VERDICT batch-last-exit 1 (build && test; held 37m08s) · Test Files 2 failed / 395 passed / 3 skipped (400) · Tests 4 failed / 3261 passed / 28 skipped (3293). Failures: flow-runas.dogfood.test.ts x3 (trigger answers 404 RECORD_NOT_FOUND on runas_note) and declarative-endpoint-anonymous-guest.dogfood.test.ts x1 (flowContexts length 0). Not this change: with both src files restored to aca58cdb6 and both plugins force-rebuilt, the same two files gave the same 4 failures (4 failed / 8 passed). Probable main-red, carried by the seat.", "files_changed": [".changeset/29362-role-holders-complete.md", "packages/plugins/plugin-sharing/src/team-graph.ts", "packages/plugins/plugin-sharing/src/team-graph-role-holders-complete.test.ts", "packages/plugins/plugin-sharing/src/position-graph.test.ts", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/member-tier-holders-complete.integration.test.ts"], "line_budget": "+569 / -29 across 6 files (598 changed lines, under 3,000)", "deviations": ["Forced test edit: position-graph.test.ts fake sys_member rows gain an id (keysetWalk treats a row without its key as truncated); no assertion changed. Named in the PR body.", "Dogfood control leg: the --absent dist preflight for plugin-sharing used a non-unique marker ('could not be read' also survives from pre-existing comments in sharing dist) and exited 1. The approvals --absent preflight exited 0. The sharing control rested on a --force rebuild from src with 0 occurrences of the new symbols.", "pr_create via relay carries no assignees (os-dev rule); the harness's assignee/reviewer step was skipped."], "mcp_calls": "1 — mcp__github__pull_request_read (get), read only", "api_writes": "2 — relay pr_create (POST pulls, #29497); this os-dev-report comment (post-stamped.mjs on #29362)", "open_questions": [ {"question": "Share vs local paging: is keysetWalk (@objectstack/types) plus about 25 lines of per-site glue acceptable as the 'no third copy' answer, rather than exporting core's readOrganizationMembersToCompletion?", "options": ["A: keep as is. The cursor loop is the shared keysetWalk; core is not widened; Clause-② stays no (narrowing).", "B: generalise and export the core helper (yes (narrowing)); both plugins call it, and #29398 folds in."], "recommendation": "A, because the core helper is keyed to organization_id, throws a 503 authz error and warns about org_user_ids. keysetWalk is already the platform's one public walker, and both plugins already import it."}, {"question": "An unregistered sys_member (the engine's OBJECT_NOT_FOUND with object sys_member) is treated as no holders, like a missing table, following sys-metadata-repository.ts. Accept?", "options": ["A: accept. Approvals integration compositions without SysMember keep routing.", "B: only isMissingTableError counts as absence. Those compositions then refuse, and their tests need SysMember registered."], "recommendation": "A, because it is attributed to sys_member, comes from the registry rather than the driver, and cannot hide an outage."} ], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: source reading only, not reproduced · same family: plugin-sharing TeamGraphService.expandUsers reads sys_team_member at limit 10000 with catch -> [], and both plugins' expandPositionUsers sys_user_position limb reads at limit 10000 with the catch swallowed · dedupe words: sys_team_member limit 10000, sys_user_position limit 10000 catch, team expansion truncation · carrier: seat 3 (the family's close-out card)", "class: a · reach: source reading only · plugin-sharing holdsRole / teamHasMember (limit 1, catch -> false) answer false on a failed read, where expandRoleUsers now throws, so a per-user recompute could revoke one user's grant on an outage · dedupe words: holdsRole catch false, recipient-membership-recompute failed read · carrier: seat 3", "class: a · reach: dogfood on bf9ca7d7 and the control leg on aca58cdb6 src · flow-runas.dogfood.test.ts (3) and declarative-endpoint-anonymous-guest.dogfood.test.ts (1) fail with and without this change · carrier: seat 3 (main-red triage)" ] }Stamped 2026-11-02T16:32Z.
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Seat ruling · #29362 · PR #29497 · 2026-11-02T16:47Z
Read: os-dev-report
6582502987; theCONTRACT_REVIEW_TIERrecord on PR #29497 (narrowing face, PASS onbf9ca7d7);main's CI runs41477790925(schedule,cafe8ee6, dogfood shards 1/3 and 3/3 red) and41441845128(schedule,27715752, all green).keysetWalkplus the per-site glue. ⛔ No core export.OBJECT_NOT_FOUNDonsys_membercounts as absence. It is raised before any driver is consulted, so it cannot hide an outage.flow-runas×3,declarative-endpoint-anonymous-guest×1) fail onmain's scheduled full battery atcafe8ee6. That main-red is 4-hourly full run: red on main (CI) #29494 (devx, p0). This PR's head runs no dogfood shard, which is the gate's design forpull_request. Nothing to do here.SharingRuleService.evaluateAllForRecord(sharing-rule-service.tsaround L1400-L1421): add per-rule isolation in the loop, asevaluateAllRulesForObjectalready has. Then one position rule that now refuses does not skip the later rules' reconcile for that record. Pin it: two rules, the first refusing, and the second still reconciles.content/docs/automation/approvals.mdx: one line naming the new "could not be expanded — a directory read failed" warning, beside the existing "expanded to nobody" one.resolveApproverSpecwarning wording: the try also wraps the read filter (bounded()), so say "a directory or read-filter lookup failed".sys_team_member, thesys_user_positionlimb,holdsRole/teamHasMember, queueexpandSpec, and the duplicatedmembershipObjectAbsent) is plugins: the rest of the sharing and approvals recipient reads still truncate at 10,000 rows or turn a failed read into "nobody" (sys_team_member, sys_user_position, holdsRole/teamHasMember, queue expandSpec) — #29362 close-out #29503. core/security: the org-members completion read trusts the driver's orderBy and replaces the first read's rows, so a driver that drops the order, or a completion that sees fewer rows, can shorten org_user_ids silently #29398 stays separate.Clause-②: no (narrowing),Metadata-protocol: none,patchon both plugins. The record's PASS coversbf9ca7d7. After your push the seat re-reads the delta and the check-runs on the new head before the ACCEPT.Run both plugins' full suites, typecheck, and an ablation of (a) before pushing. Report the new head when done.
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmS2E1xdkLMJPnabYgC
os-dev-report
{ "issue": 29362, "status": "done", "branch": "claude/project-thread-qcavvn", "pr": "https://github.com/objectstack-ai/objectstack/pull/29497", "session": "session_01ADHkmS2E1xdkLMJPnabYgC", "premise_still_valid": true, "summary": "Round 2 (seat fixes) on head 4c34c3c8, which merges origin/main after #29451. (a) SharingRuleService.evaluateAllForRecord now catches per rule, as evaluateAllRulesForObject does: a rule whose recipients cannot be read keeps its grants and warns '[sharing-rule] record reconcile failed for rule', and the record's other rules still reconcile. (b) approvals.mdx names the new warning. (c) The warning now reads 'a directory or read-filter lookup failed'. Round 1 is unchanged: role and membership-tier holder reads complete through keysetWalk, a failed or incomplete read throws, and a missing sys_member answers []. Both open questions are ruled A.", "tests": "head 4c34c3c8 (plugin suites on 8fa01d1c; the merge touched neither plugin). New pin: recipient-width.test.ts › '#29362 — the per-record pass is best-effort per rule'. The first (position) rule's sys_member read fails, its grant stays, the second (user) rule restores its grant, and one warning names the failed rule. Ablation: rethrow in the catch via ablation-replace.mjs (anchor 1->0, blob moved, restored blob == HEAD, git diff HEAD empty). The pin goes red: '[sharing] the holders of role 'sales_rep' could not be read: SQLITE_IOERR'. Approvals caller-sees pin tightened to the new wording. Full suites: plugin-sharing Test Files 61/61, Tests 1325/1325; plugin-approvals Test Files 108/108, Tests 1388/1388; both typechecks exit 0. Round 1 readings stand: reds on 7b1ae338, and four ablation legs red then restored.", "gates": "dispatch-gates --commands on 4c34c3c8: 93 derived (the docs file added 30 families), 93 run, all exit 0. --ran: '93 run, 0 NOT-MEASURED'. check:type-check-debt first ran exit 3 (queued behind the dogfood lock) and was re-run exit 0. pnpm lint exit 0. PR check-runs: not yet read for this head.", "dogfood_battery": "4c34c3c8 · VERDICT batch-last-exit 0 (build && test; held 37m15s) · Test Files 397 passed / 3 skipped (400) · Tests 3277 passed / 28 skipped (3305) · no failures. The round-1 failures were main-red #29426, fixed by #29451.", "files_changed": [ ".changeset/29362-role-holders-complete.md", "packages/plugins/plugin-sharing/src/team-graph.ts", "packages/plugins/plugin-sharing/src/team-graph-role-holders-complete.test.ts", "packages/plugins/plugin-sharing/src/position-graph.test.ts", "packages/plugins/plugin-approvals/src/approval-service.ts", "packages/plugins/plugin-approvals/src/member-tier-holders-complete.integration.test.ts", "packages/plugins/plugin-sharing/src/sharing-rule-service.ts", "packages/plugins/plugin-sharing/src/recipient-width.test.ts", "content/docs/automation/approvals.mdx" ], "line_budget": "+657 / -36 across 9 files (693 changed lines, under 3,000)", "deviations": [ "Forced test edit: position-graph.test.ts fake sys_member rows gain an id (keysetWalk treats a row without its key as truncated); no assertion changed. Named in the PR body.", "pr_create via relay carries no assignees (os-dev rule); the harness's assignee/reviewer step was skipped.", "The first round-2 ablation attempt was a no-op: ablation-replace refused because the replacement still contained the anchor (anchor x1 -> x1), restored, and exited 1 with no test run. Re-run with an anchor the replacement removes, which landed (1 -> 0) and went red.", "A gate run on 8fa01d1c was stopped at 57/93 (killed by recorded PID) when the seat asked for a main merge. All 93 were re-derived and re-run on 4c34c3c8.", "The PR body is not patched by the card thread. A round-2 body patch is at /mnt/project-files/server-seat3/29497-body-r2-patch.md for the seat to send." ], "mcp_calls": "1 (whole card) — mcp__github__pull_request_read (get), read only", "api_writes": "3 (whole card) — relay pr_create (#29497); round-1 os-dev-report (issuecomment-6582502987); this round-2 os-dev-report", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "Filed by the seat as #29503 (sys_team_member and sys_user_position limbs, holdsRole/teamHasMember). No new findings this round." ] }Stamped 2026-11-02T17:48Z.
objectstack-fleet commentedon Nov 2, 2026
domain:server#3 · os-steve · session_01ADHkmQJdgarUjPLNgFBY67
Landing record · #29362 · PR #29497
domain:serverseat 3 ·session_01ADHkmQJdgarUjPLNgFBY67· 2026-11-02T19:02Zgit ls-remote origin 'refs/heads/gh-readonly-queue/main/pr-29497-*'git merge-base --is-ancestor 2789ba7d origin/maingit grep -n "async function readRoleHoldersToCompletion" origin/main -- packages/plugins/plugin-sharing/src/team-graph.ts2789ba7d. Sharing-rule role expansion and approvals membership-tier recipients now readsys_memberto completion (10,000-row pages, id keyset viakeysetWalk). A failed or incomplete read now refuses instead of answering "nobody". Sharing recompute for one record is isolated per rule: a rule whose holders cannot be read keeps its existing grants and warns, and the other rules recompute as usual.6584657484. The narrowing-face contract reviews are6582769583and6584482384(the second on the revised head).Fixes #29362closed this card on merge.sys_team_member,sys_user_position,holdsRole/teamHasMember, queueexpandSpec) are plugins: the rest of the sharing and approvals recipient reads still truncate at 10,000 rows or turn a failed read into "nobody" (sys_team_member, sys_user_position, holdsRole/teamHasMember, queue expandSpec) — #29362 close-out #29503, which this landing makes eligible.pm:dispatchedand the assignee removed in the same round.