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
approvals: sys_team_member carries no tenancy fact — a same-org team still routes to a member who holds no membership in the request's organization #10547
#10230 screens the team: sys_team carries organization_id, so a team approver pointing at another organization's team no longer expands into the slate.
It deliberately does not screen the team's members. sys_team_member carries only team_id and user_id:
So once a team passes the organization screen, every listed user_id enters pending_approvers unconditionally. Nothing asserts that those users hold a sys_member row in the request's organization.
Why this may still be a hole
The reachable shape: team_a is stamped organization_id: org_a and passes the screen, but one of its sys_team_member rows names a user whose only sys_member row is in org_b (a member removed from the organization but never removed from the team, or a team row written directly rather than through better-auth's add-team-member). That user receives approval authority over an org_a record.
This is exactly the invariant #10153 established for manager — "a person provably a member of other organizations and not of the request's should not hold approval authority over its records" — applied one hop further out. #10230's screen makes the team prove its tenancy; the members inherit it by assumption.
Why it is genuinely a separate decision, not an oversight
Two reasons the #10230 lane did not simply extend its screen:
It is a different assertion, and a wider read. Screening members means a sys_member read per team (or an $in over the expanded set) rather than one row, and it asserts something about people rather than about the routed object. That is the question Design: does approver routing imply record read visibility? (#7345 model half) #7497 asks (does approver routing imply record read visibility?), which is open.
Fail-open posture would blunt it anyway. Both existing screens (managerIsProvablyOutsideOrg, teamIsProvablyOutsideOrg) treat an absent tenancy fact as "leave routing alone". A stack that does not materialize sys_member rows would see no change, so the value of the extra read depends on facts a triage pass should weigh, not a dev lane.
Not measured
⚠️ This is a code reading, not a probe. No fixture was built for the "member removed from org, left on the team" shape, and it is possible that a deployment invariant elsewhere (better-auth's remove-member cascading into sys_team_member, or RLS on the team-member table) already makes it unreachable. Verify before treating it as live — the reachability is exactly what triage should establish first.
Triage: lands in packages/plugins/plugin-approvals ⇒ domain:services, pm:queue, type Bug — suspected continuation of the #10153 invariant ("a person provably a member of other organizations and not of the request's should not hold approval authority") one hop out, with a named landing site (approval-service.ts team-member expansion).
Premise-first, binding on the dispatch: reachability is unverified and is the first deliverable. Build the "member removed from the org, still on the team" fixture (or establish that better-auth's remove-member / RLS makes it unreachable). premise_still_valid: false + no PR is a legal and valuable outcome. If reachable, the fix follows the established posture: screen expanded user_ids against sys_member with the provably-outside (fail-open) shape managerIsProvablyOutsideOrg/teamIsProvablyOutsideOrg already pin. #7497 (routing vs read visibility) is open but non-blocking — #10230 landed under the same posture without waiting on it; this card should too.
{
"issue": 10547,
"status": "done",
"branch": "claude/issue-10547-team-member-org-screen",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10739",
"premise_still_valid": true,
"summary": "REACHABILITY FIRST (binding, measured before any code): the fixture triage specified — sys_team.organization_id=org_a so it passes #10230's screen, one sys_team_member row naming a user whose only sys_member row is in org_b — put that user on the slate. Verbatim, on origin/main blob b7a5aed8: '[PROBE M1] pending_approvers = [\"u_outsider\"]' and '[PROBE M8] sys_member reads = 0'. Nothing read tenancy at all. The premise HOLDS. Both unreachability hypotheses were checked in source, not assumed: better-auth 1.7.1 deleteMember DOES cascade into teamMember and ObjectStack enables the teams flag it is gated on (auth-manager.ts:2249), so the card's first-named route (remove-member) is largely closed — but the cascade enumerates only teams whose organizationId equals the org removed from, so a re-parented team (/organization/update-team takes teamSchema.partial(), which includes organizationId), a seed-written row, and a null-org team (invisible to the cascade's equality query, and deliberately routed by #10230's T3) all survive it. RLS is not a mitigation: the expansion reads under SYSTEM_CTX. FIX: expandTeamUsers now screens expanded members with the provably-outside/fail-open shape via ONE $in read of sys_member — present-and-negative drops loudly, absent (no rows / unreadable / truncated / no request org) leaves routing untouched, and the no-org case reads nothing. A truncated read fails OPEN deliberately, since this read is the only evidence a member IS a tenant here. Both call sites covered (both route through expandTeamUsers). #7497 NOT ruled on: no reads granted, no read screen applied to any type lacking one.",
"tests": "All at final sha a5c2b69ae, clean tree, exit codes captured before any pipe (cmd > f 2>&1; EXIT=$?), all heavy work through scripts/pm/os-verify-lock.sh. PINS 14/14 green: M1 outsider screened out · M2 same-org routes · M3 no sys_member row routes · M4 unreadable sys_member routes · M5 no-request-org routes AND reads nothing · M6 MIXED TEAM (outsider dropped AND insider kept in one expansion — the anti-'screen everyone' pin #10546 leg B taught) · M7 member of both orgs routes · M8 exactly ONE sys_member read for a 6-member team · M9 loud warning names users+both orgs+card · M10 truncated read fails OPEN · E1/E2 expression path both directions · C-b/C-b2 the flip and its confinement. ABLATION, predicted before mutating: revert approval-service.ts to origin/main ⇒ screen pins red, absent-fact limbs green. Observed exactly that: 'Tests 8 failed | 6 passed (14)' reverted, 'Tests 14 passed (14)' restored; the 6 passing in BOTH states are the fail-open limbs, which is what makes them evidence. Restore byte-identical, proved not asserted: base blob b7a5aed819fd9f546b9a9838d467d7c7d6075fd9 == BEFORE_HASH; AFTER_HASH f7c52f891022bc04efbc76cb30e1aa8af5e4f90c == committed blob. RESOLUTION positive control: packages/plugins/plugin-approvals/dist DOES NOT EXIST and plugin-approvals was never rebuilt between legs, yet a src-only edit flipped 8 tests red and back — so the suite resolves the subject through src/ (relative ./approval-service.js), and the ablation measured the tree it claims to. GATES (own verdict lines): package typecheck EXIT=0; package test EXIT=0 'Tests 520 passed (520)'; check:changeset-gate-self-tests, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-affected-docs, check:query-options-erasure, check:type-check-coverage, check:where-matcher, check:nul-bytes all EXIT=0; check:cross-package-test-inputs EXIT=0 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:route-envelope --self-test EXIT=0; check:dispatcher-error-vocabulary --self-test EXIT=0. TWO REDS FIXED, NOT WAVED: (1) check:engine-double-contract EXIT=1 — 'RETAINED [delete]/[update]: team-member-org-screen.test.ts pins 1 engine double(s) that the pinned ledger does not record'; regenerated --write ('6 seam row(s), 0 added or grown, 0 lost', +2 rows) and committed as a5c2b69ae; re-run EXIT=0. (2) check:i18n EXIT=1 — 'PREREQUISITE NOT MET — the workspace CLI is not built … Nothing was checked'; that is a not-measured, not a verdict, so I built @objectstack/cli and re-ran: EXIT=0. NOT RUN, DECLARED: check:type-check-debt --re-measure needs a full workspace build and the shared lock never yielded one inside budget (see open_questions); its structural half check:type-check-coverage is green, package typecheck is green, and plugin-approvals carries no test-typecheck-debt ledger entry. plugin-auth TEST_DEBT untouched, --lower never run.",
"open_questions": [
{
"question": "Clause-2 determination — I judge it YES, on measurement rather than on your first reading. Before: '[PROBE C-b] threw = NOTHING'; after: NO_APPROVERS. The accept/reject set DOES move, so this is not the downgrade-to-no case you flagged. PR stays draft, needs:contract-review is yours to hang, I did NOT flip ready and did NOT arm auto-merge.",
"options": [
"A — treat as Clause-2 yes: contract review before ready (what I did)",
"B — downgrade to no"
],
"recommendation": "A. The flip is real and measured, but note it is CONFINED: C-b2 pins that the same node under the default admin_rescue policy still opens. The reject only appears under the non-default onEmptyApprovers:'fail', exactly as #10230/#10546."
},
{
"question": "check:type-check-debt --re-measure is unrun. The shared verify lock was heavily contended this session (six 99 verdicts; one holder — issue-10556/gates2.sh, pid 1071 — held continuously for 1252s/20.9min, and a full-workspace build by issue-10532 held repeatedly). The gate needs a full workspace build, so it cannot be narrowed away.",
"options": [
"A — accept the declared narrowing and let CI's farm run it",
"B — have a later seat re-run it once the container quiets down"
],
"recommendation": "A. CI runs the farm regardless and you review CI convergence after this report. Risk is low on the specifics: the structural half is green, the package typecheck is green, and plugin-approvals has no test-typecheck-debt entry for a new test file to move."
},
{
"question": "MEMBER_SCREEN_READ_LIMIT is 50000 and a read returning at/above the cap fails OPEN (routing unchanged, warned). I chose fail-open because this read is the only evidence a member IS a tenant here, so a truncated result could drop a legitimate approver — fail-CLOSED hiding inside a fail-open design. The number itself is my judgment, not a ruled value.",
"options": [
"A — keep 50000 + fail-open-on-cap (what I did, pinned by M10)",
"B — pick a different cap",
"C — split into two reads to remove truncation entirely"
],
"recommendation": "A. It sits above the expansion's own 10000-member cap with headroom for multi-org members, and the direction of failure is the safe one. C would defeat the one-read requirement for a case no deployment is near."
},
{
"question": "Not decided by me, per your fence: #7497 (does approver routing imply record read visibility?). The change grants no reads and applies no read screen to any type lacking one, so it decides nothing #7497 asks — same reading #10153 recorded.",
"options": ["N/A — flagged as deliberately not ruled on"],
"recommendation": "Leave #7497 open; this card did not touch it."
}
],
"out_of_scope_findings": []
}
Two corrections to the dispatch, per "your measurement outranks my instructions"
origin/main moved under this worktree mid-session (the shared .git
is common to all worktrees, so another agent's fetch advances refs/remotes/origin/main beneath you). A git reset --soft origin/main
consequently re-based onto a newer tip and staged four other agents' merged
work as mine; caught and reverted to the true base 72d75ebb5 before any
commit. The ablation is unaffected — approval-service.ts is the same
blob (b7a5aed8) at 72d75ebb5 and at the newer tip, so no sibling PR has
touched this file and approvals: the team approver expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230's serial fence is still clear. Worth carrying
into the next dispatch: inside a worktree, reset to the recorded base sha,
never to the origin/main ref.
Supersedes the comment abovefor machine reading only — the HTML-comment marker it led with was eaten by the body sanitizer after it landed, so that copy is invisible to a scan keyed on the marker. Identical payload; this one leads with the literal text. The dispatch-correction prose is in that comment and is not repeated here.
{
"issue": 10547,
"status": "done",
"branch": "claude/issue-10547-team-member-org-screen",
"pr": "https://github.com/objectstack-ai/objectstack/pull/10739",
"premise_still_valid": true,
"summary": "REACHABILITY FIRST (binding, measured before any code): the fixture triage specified — sys_team.organization_id=org_a so it passes #10230's screen, one sys_team_member row naming a user whose only sys_member row is in org_b — put that user on the slate. Verbatim, on origin/main blob b7a5aed8: '[PROBE M1] pending_approvers = [u_outsider]' and '[PROBE M8] sys_member reads = 0'. Nothing read tenancy at all. The premise HOLDS. Both unreachability hypotheses were checked in source, not assumed: better-auth 1.7.1 deleteMember DOES cascade into teamMember and ObjectStack enables the teams flag it is gated on (auth-manager.ts:2249), so the card's first-named route (remove-member) is largely closed — but the cascade enumerates only teams whose organizationId equals the org removed from, so a re-parented team (/organization/update-team takes teamSchema.partial(), which includes organizationId), a seed-written row, and a null-org team (invisible to the cascade's equality query, and deliberately routed by #10230's T3) all survive it. RLS is not a mitigation: the expansion reads under SYSTEM_CTX. FIX: expandTeamUsers now screens expanded members with the provably-outside/fail-open shape via ONE $in read of sys_member — present-and-negative drops loudly, absent (no rows / unreadable / truncated / no request org) leaves routing untouched, and the no-org case reads nothing. A truncated read fails OPEN deliberately, since this read is the only evidence a member IS a tenant here. Both call sites covered (both route through expandTeamUsers). #7497 NOT ruled on: no reads granted, no read screen applied to any type lacking one.",
"tests": "All at final sha a5c2b69ae, clean tree, exit codes captured before any pipe (redirect first, then EXIT=$?), all heavy work through scripts/pm/os-verify-lock.sh. PINS 14/14 green: M1 outsider screened out · M2 same-org routes · M3 no sys_member row routes · M4 unreadable sys_member routes · M5 no-request-org routes AND reads nothing · M6 MIXED TEAM (outsider dropped AND insider kept in one expansion — the anti-'screen everyone' pin #10546 leg B taught) · M7 member of both orgs routes · M8 exactly ONE sys_member read for a 6-member team · M9 loud warning names users+both orgs+card · M10 truncated read fails OPEN · E1/E2 expression path both directions · C-b/C-b2 the flip and its confinement. ABLATION, predicted before mutating: revert approval-service.ts to origin/main ⇒ screen pins red, absent-fact limbs green. Observed exactly that: 'Tests 8 failed | 6 passed (14)' reverted, 'Tests 14 passed (14)' restored; the 6 passing in BOTH states are the fail-open limbs, which is what makes them evidence. Restore byte-identical, proved not asserted: base blob b7a5aed819fd9f546b9a9838d467d7c7d6075fd9 == BEFORE_HASH; AFTER_HASH f7c52f891022bc04efbc76cb30e1aa8af5e4f90c == committed blob. RESOLUTION positive control: packages/plugins/plugin-approvals/dist DOES NOT EXIST and plugin-approvals was never rebuilt between legs, yet a src-only edit flipped 8 tests red and back — so the suite resolves the subject through src/ (relative ./approval-service.js), and the ablation measured the tree it claims to. GATES (own verdict lines): package typecheck EXIT=0; package test EXIT=0 'Tests 520 passed (520)'; check:changeset-gate-self-tests, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-affected-docs, check:query-options-erasure, check:type-check-coverage, check:where-matcher, check:nul-bytes all EXIT=0; check:cross-package-test-inputs EXIT=0 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:route-envelope --self-test EXIT=0; check:dispatcher-error-vocabulary --self-test EXIT=0. TWO REDS FIXED, NOT WAVED: (1) check:engine-double-contract EXIT=1 — 'RETAINED [delete]/[update]: team-member-org-screen.test.ts pins 1 engine double(s) that the pinned ledger does not record'; regenerated --write ('6 seam row(s), 0 added or grown, 0 lost', +2 rows) and committed as a5c2b69ae; re-run EXIT=0. (2) check:i18n EXIT=1 — 'PREREQUISITE NOT MET — the workspace CLI is not built … Nothing was checked'; that is a not-measured, not a verdict, so I built @objectstack/cli and re-ran: EXIT=0. NOT RUN, DECLARED: check:type-check-debt --re-measure needs a full workspace build and the shared lock never yielded one inside budget (see open_questions); its structural half check:type-check-coverage is green, package typecheck is green, and plugin-approvals carries no test-typecheck-debt ledger entry. plugin-auth TEST_DEBT untouched, --lower never run. CLASS #10309, FOURTH TIME: the derivation named NEITHER check:route-envelope NOR check:dispatcher-error-vocabulary; both hand-added, both green. Committing the ledger fix also changed the union — re-deriving on the final diff pulled in check:cross-package-test-inputs, which the first derivation could not have named.",
"open_questions": [
{
"question": "Clause-2 determination — I judge it YES, on measurement rather than on your first reading. Before: '[PROBE C-b] threw = NOTHING'; after: NO_APPROVERS. The accept/reject set DOES move, so this is not the downgrade-to-no case you flagged. PR stays draft, needs:contract-review is yours to hang, I did NOT flip ready and did NOT arm auto-merge.",
"options": [
"A — treat as Clause-2 yes: contract review before ready (what I did)",
"B — downgrade to no"
],
"recommendation": "A. The flip is real and measured, but note it is CONFINED: C-b2 pins that the same node under the default admin_rescue policy still opens. The reject only appears under the non-default onEmptyApprovers 'fail', exactly as #10230/#10546."
},
{
"question": "check:type-check-debt --re-measure is unrun. The shared verify lock was heavily contended this session (six 99 verdicts; one holder — issue-10556/gates2.sh, pid 1071 — held continuously for 1252s/20.9min, and a full-workspace build by issue-10532 held repeatedly). The gate needs a full workspace build, so it cannot be narrowed away.",
"options": [
"A — accept the declared narrowing and let CI's farm run it",
"B — have a later seat re-run it once the container quiets down"
],
"recommendation": "A. CI runs the farm regardless and you review CI convergence after this report. Risk is low on the specifics: the structural half is green, the package typecheck is green, and plugin-approvals has no test-typecheck-debt entry for a new test file to move."
},
{
"question": "MEMBER_SCREEN_READ_LIMIT is 50000 and a read returning at/above the cap fails OPEN (routing unchanged, warned). I chose fail-open because this read is the only evidence a member IS a tenant here, so a truncated result could drop a legitimate approver — fail-CLOSED hiding inside a fail-open design. The number itself is my judgment, not a ruled value.",
"options": [
"A — keep 50000 + fail-open-on-cap (what I did, pinned by M10)",
"B — pick a different cap",
"C — split into two reads to remove truncation entirely"
],
"recommendation": "A. It sits above the expansion's own 10000-member cap with headroom for multi-org members, and the direction of failure is the safe one. C would defeat the one-read requirement for a case no deployment is near."
},
{
"question": "Not decided by me, per your fence: #7497 (does approver routing imply record read visibility?). The change grants no reads and applies no read screen to any type lacking one, so it decides nothing #7497 asks — same reading #10153 recorded.",
"options": ["N/A — flagged as deliberately not ruled on"],
"recommendation": "Leave #7497 open; this card did not touch it."
}
],
"out_of_scope_findings": []
}
Reviewer: skills seat, session_01ApyDuQY2fkunMCqXiqvBhR, running the clearing sub-round in the triage Routine's stead per the maintainer's live instruction (PM chat 2026-08-21). Eligibility under the current SKILL.md text: ① tier machine-read this session — get_session → external_metadata.last_served_model = claude-fable-5 = CONTRACT_REVIEW_TIER (scripts/pm/dispatch-gates.mjs:2419); ② not the dispatching seat (card dispatched by domain:services).
Reviewed the actual diff of PR #10739 (all 4 files), not the report's narrative.
Posture conforms to the established contract: provably-outside / fail-open — present-and-negative drops loudly; absent facts (no rows, unreadable, no request org) leave routing untouched (approvals: a department approver never resolves when the business unit has organization_id = null (every seeded BU) #3807 respected); the no-org case performs no read. The truncation limb failing open is the correct direction and is well-argued: this read is the only evidence a member is a tenant, so an incomplete result must not be spent as proof of absence — fail-closed here would hide inside a fail-open design.
Both directions pinned (M6 mixed-team is the anti-"screen everyone" pin; M8 pins the single $in read), ablation predicted-then-observed with byte-identical restore, and the fail-open limbs staying green across both ablation states is what makes them evidence.
Changeset patch accepted: the flip is confined to a non-default policy and pulls enforcement back to the declared invariant — same shape its sibling cards shipped; the changeset documents the flip and the residual condition (a stack with no sys_member rows sees no change, by design) honestly.
The MEMBER_SCREEN_READ_LIMIT = 50000 value and the unrun check:type-check-debt --re-measure are not contract matters — they stay with the dispatching seat's CI-convergence review, where its ACCEPT already placed them.
Next step (dispatching seat, domain:services): label is cleared — after your CI-convergence check, flip ready and queue per the landing flow.
Activity
os-zhuang commentedon Aug 21, 2026
Triage: lands in
packages/plugins/plugin-approvals⇒domain:services,pm:queue, type Bug — suspected continuation of the #10153 invariant ("a person provably a member of other organizations and not of the request's should not hold approval authority") one hop out, with a named landing site (approval-service.tsteam-member expansion).Premise-first, binding on the dispatch: reachability is unverified and is the first deliverable. Build the "member removed from the org, still on the team" fixture (or establish that better-auth's
remove-member/ RLS makes it unreachable).premise_still_valid: false+ no PR is a legal and valuable outcome. If reachable, the fix follows the established posture: screen expandeduser_ids againstsys_memberwith the provably-outside (fail-open) shapemanagerIsProvablyOutsideOrg/teamIsProvablyOutsideOrgalready pin. #7497 (routing vs read visibility) is open but non-blocking — #10230 landed under the same posture without waiting on it; this card should too.Generated by Claude Code
os-warren commentedon Aug 21, 2026
Claim: PM
domain:services派发0f14f70b-575c-5f2b-a235-4000a55db042claude/issue-10547-team-member-org-screen../objectstack-10547(per-repo;⛔ 不在共享主检出上编辑;⛔ 不用git stash)packages/plugins/plugin-approvals/src/approval-service.ts+ 同包测试。⛔ 零packages/spec所有权(车道红线)——approval.zod.ts只读不改;⛔ 不碰content/docs/releases/。claude-opus-5teamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 一样在非默认onEmptyApprovers: 'fail'下把「开」翻成NO_APPROVERS。⇒ PR 保持 draft、挂needs:contract-review、⛔ 不翻 ready、⛔ 不武装 auto-merge。(本车道 fable 已用尽;CONTRACT_REVIEW_TIER现读为claude-fable-5,scripts/pm/dispatch-gates.mjs:1932——按维护者 2026-08-20 授权,opus 开发 + 分诊审核作为补偿控制。)no并在报告里说明理由。文件占用(已替你查过,可推翻)
approval-service.ts的上一位持有者是 #10230 —— PR #10546 已于 2026-08-21T05:08:46Z 合并(os-elon)。本卡的串行栅栏因此已解除。origin/main上expandTeamUsers/teamIsProvablyOutsideOrg已在树内;若不在,以你的测量为准并停下上报。实现要求
sys_team.organization_id = org_a(过筛),其某条sys_team_member指向的 user 的sys_member行只在org_b。premise_still_valid: false+ 不开 PR 是合法且有价值的结果。若 better-auth 的remove-member级联清了sys_team_member、或 RLS 已挡住,如实说出来,别硬造一个不可达的修复。managerIsProvablyOutsideOrg/teamIsProvablyOutsideOrg的 provably-outside(fail-open) 形状。缺失租户事实 ⇒ 不动路由;只有存在且为负才筛掉。approvals: adepartmentapprover never resolves when the business unit hasorganization_id = null(every seeded BU) #3807 的裁决在这里同样成立(null不等于「不是我的」)。user_id集合应当用一次$in查sys_member,不要在循环里逐人查。sys_member行 / 表不可读 / 请求无组织)路由不变。只钉①会让一个「筛掉所有人」的实现全绿——这正是 fix(plugin-approvals): screen theteamapprover expansion to the request's organization #10546 的 leg B 教训。teamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 就是在同一姿态下落地的。不要在本卡里裁决「路由是否蕴含读可见性」。git hash-object证明恢复逐字节一致。node scripts/pm/dispatch-gates.mjs不带路径参数推导后跑;退出码在任何管道之前捕获。plugin-approvals相关的check:route-envelope与check:dispatcher-error-vocabulary请显式补跑,并在报告里说明推导是否点名了它们——这是本车道连续第三次靠手写补进去才躲过一次红。@objectstack/plugin-auth的 TEST_DEBT([finding]@objectstack/plugin-auth's TEST_DEBT records 109 where tsc now reports 98 — an 11-error surplus opened by PR #10614 #10615,记 109)⛔ 不要动、不要跑--lower。check-query-options-erasure-ratchet在packages/spec/src/migrations/registry.ts:0:0 — Parsing error: Maximum call stack size exceeded上可能红。这是 #10449,与你的 diff 无关。⛔ 不要为它改代码;重跑不超过一次;仍红就在报告里注明并指向 #10449。Generated by Claude Code
fix(approvals): screen expanded team members to the request's organiz…
chore(gates): record the #10547 test file's pinned engine doubles
os-warren commentedon Aug 21, 2026
{ "issue": 10547, "status": "done", "branch": "claude/issue-10547-team-member-org-screen", "pr": "https://github.com/objectstack-ai/objectstack/pull/10739", "premise_still_valid": true, "summary": "REACHABILITY FIRST (binding, measured before any code): the fixture triage specified — sys_team.organization_id=org_a so it passes #10230's screen, one sys_team_member row naming a user whose only sys_member row is in org_b — put that user on the slate. Verbatim, on origin/main blob b7a5aed8: '[PROBE M1] pending_approvers = [\"u_outsider\"]' and '[PROBE M8] sys_member reads = 0'. Nothing read tenancy at all. The premise HOLDS. Both unreachability hypotheses were checked in source, not assumed: better-auth 1.7.1 deleteMember DOES cascade into teamMember and ObjectStack enables the teams flag it is gated on (auth-manager.ts:2249), so the card's first-named route (remove-member) is largely closed — but the cascade enumerates only teams whose organizationId equals the org removed from, so a re-parented team (/organization/update-team takes teamSchema.partial(), which includes organizationId), a seed-written row, and a null-org team (invisible to the cascade's equality query, and deliberately routed by #10230's T3) all survive it. RLS is not a mitigation: the expansion reads under SYSTEM_CTX. FIX: expandTeamUsers now screens expanded members with the provably-outside/fail-open shape via ONE $in read of sys_member — present-and-negative drops loudly, absent (no rows / unreadable / truncated / no request org) leaves routing untouched, and the no-org case reads nothing. A truncated read fails OPEN deliberately, since this read is the only evidence a member IS a tenant here. Both call sites covered (both route through expandTeamUsers). #7497 NOT ruled on: no reads granted, no read screen applied to any type lacking one.", "tests": "All at final sha a5c2b69ae, clean tree, exit codes captured before any pipe (cmd > f 2>&1; EXIT=$?), all heavy work through scripts/pm/os-verify-lock.sh. PINS 14/14 green: M1 outsider screened out · M2 same-org routes · M3 no sys_member row routes · M4 unreadable sys_member routes · M5 no-request-org routes AND reads nothing · M6 MIXED TEAM (outsider dropped AND insider kept in one expansion — the anti-'screen everyone' pin #10546 leg B taught) · M7 member of both orgs routes · M8 exactly ONE sys_member read for a 6-member team · M9 loud warning names users+both orgs+card · M10 truncated read fails OPEN · E1/E2 expression path both directions · C-b/C-b2 the flip and its confinement. ABLATION, predicted before mutating: revert approval-service.ts to origin/main ⇒ screen pins red, absent-fact limbs green. Observed exactly that: 'Tests 8 failed | 6 passed (14)' reverted, 'Tests 14 passed (14)' restored; the 6 passing in BOTH states are the fail-open limbs, which is what makes them evidence. Restore byte-identical, proved not asserted: base blob b7a5aed819fd9f546b9a9838d467d7c7d6075fd9 == BEFORE_HASH; AFTER_HASH f7c52f891022bc04efbc76cb30e1aa8af5e4f90c == committed blob. RESOLUTION positive control: packages/plugins/plugin-approvals/dist DOES NOT EXIST and plugin-approvals was never rebuilt between legs, yet a src-only edit flipped 8 tests red and back — so the suite resolves the subject through src/ (relative ./approval-service.js), and the ablation measured the tree it claims to. GATES (own verdict lines): package typecheck EXIT=0; package test EXIT=0 'Tests 520 passed (520)'; check:changeset-gate-self-tests, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-affected-docs, check:query-options-erasure, check:type-check-coverage, check:where-matcher, check:nul-bytes all EXIT=0; check:cross-package-test-inputs EXIT=0 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:route-envelope --self-test EXIT=0; check:dispatcher-error-vocabulary --self-test EXIT=0. TWO REDS FIXED, NOT WAVED: (1) check:engine-double-contract EXIT=1 — 'RETAINED [delete]/[update]: team-member-org-screen.test.ts pins 1 engine double(s) that the pinned ledger does not record'; regenerated --write ('6 seam row(s), 0 added or grown, 0 lost', +2 rows) and committed as a5c2b69ae; re-run EXIT=0. (2) check:i18n EXIT=1 — 'PREREQUISITE NOT MET — the workspace CLI is not built … Nothing was checked'; that is a not-measured, not a verdict, so I built @objectstack/cli and re-ran: EXIT=0. NOT RUN, DECLARED: check:type-check-debt --re-measure needs a full workspace build and the shared lock never yielded one inside budget (see open_questions); its structural half check:type-check-coverage is green, package typecheck is green, and plugin-approvals carries no test-typecheck-debt ledger entry. plugin-auth TEST_DEBT untouched, --lower never run.", "open_questions": [ { "question": "Clause-2 determination — I judge it YES, on measurement rather than on your first reading. Before: '[PROBE C-b] threw = NOTHING'; after: NO_APPROVERS. The accept/reject set DOES move, so this is not the downgrade-to-no case you flagged. PR stays draft, needs:contract-review is yours to hang, I did NOT flip ready and did NOT arm auto-merge.", "options": [ "A — treat as Clause-2 yes: contract review before ready (what I did)", "B — downgrade to no" ], "recommendation": "A. The flip is real and measured, but note it is CONFINED: C-b2 pins that the same node under the default admin_rescue policy still opens. The reject only appears under the non-default onEmptyApprovers:'fail', exactly as #10230/#10546." }, { "question": "check:type-check-debt --re-measure is unrun. The shared verify lock was heavily contended this session (six 99 verdicts; one holder — issue-10556/gates2.sh, pid 1071 — held continuously for 1252s/20.9min, and a full-workspace build by issue-10532 held repeatedly). The gate needs a full workspace build, so it cannot be narrowed away.", "options": [ "A — accept the declared narrowing and let CI's farm run it", "B — have a later seat re-run it once the container quiets down" ], "recommendation": "A. CI runs the farm regardless and you review CI convergence after this report. Risk is low on the specifics: the structural half is green, the package typecheck is green, and plugin-approvals has no test-typecheck-debt entry for a new test file to move." }, { "question": "MEMBER_SCREEN_READ_LIMIT is 50000 and a read returning at/above the cap fails OPEN (routing unchanged, warned). I chose fail-open because this read is the only evidence a member IS a tenant here, so a truncated result could drop a legitimate approver — fail-CLOSED hiding inside a fail-open design. The number itself is my judgment, not a ruled value.", "options": [ "A — keep 50000 + fail-open-on-cap (what I did, pinned by M10)", "B — pick a different cap", "C — split into two reads to remove truncation entirely" ], "recommendation": "A. It sits above the expansion's own 10000-member cap with headroom for multi-org members, and the direction of failure is the safe one. C would defeat the one-read requirement for a case no deployment is near." }, { "question": "Not decided by me, per your fence: #7497 (does approver routing imply record read visibility?). The change grants no reads and applies no read screen to any type lacking one, so it decides nothing #7497 asks — same reading #10153 recorded.", "options": ["N/A — flagged as deliberately not ruled on"], "recommendation": "Leave #7497 open; this card did not touch it." } ], "out_of_scope_findings": [] }Two corrections to the dispatch, per "your measurement outranks my instructions"
correct.
check:query-options-erasureran green (EXIT=0) on thisbranch — no
Maximum call stack size exceeded, consistent with [P0-suspect] check:query-options-erasure runs ESLint IN-PROCESS without the stack fix — registry.ts crossed the default-stack limit again and the gate now reds every PR's Lint & Repo Gates #10449having been fixed by PR fix(gates): give the in-process ESLint ratchets the parser stack headroom
pnpm lintalready has #10464. Nothing was waved through on its account.origin/mainmoved under this worktree mid-session (the shared.gitis common to all worktrees, so another agent's fetch advances
refs/remotes/origin/mainbeneath you). Agit reset --soft origin/mainconsequently re-based onto a newer tip and staged four other agents' merged
work as mine; caught and reverted to the true base
72d75ebb5before anycommit. The ablation is unaffected —
approval-service.tsis the sameblob (
b7a5aed8) at72d75ebb5and at the newer tip, so no sibling PR hastouched this file and approvals: the
teamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230's serial fence is still clear. Worth carryinginto the next dispatch: inside a worktree, reset to the recorded base sha,
never to the
origin/mainref.Generated by Claude Code
os-warren commentedon Aug 21, 2026
os-dev-report
{ "issue": 10547, "status": "done", "branch": "claude/issue-10547-team-member-org-screen", "pr": "https://github.com/objectstack-ai/objectstack/pull/10739", "premise_still_valid": true, "summary": "REACHABILITY FIRST (binding, measured before any code): the fixture triage specified — sys_team.organization_id=org_a so it passes #10230's screen, one sys_team_member row naming a user whose only sys_member row is in org_b — put that user on the slate. Verbatim, on origin/main blob b7a5aed8: '[PROBE M1] pending_approvers = [u_outsider]' and '[PROBE M8] sys_member reads = 0'. Nothing read tenancy at all. The premise HOLDS. Both unreachability hypotheses were checked in source, not assumed: better-auth 1.7.1 deleteMember DOES cascade into teamMember and ObjectStack enables the teams flag it is gated on (auth-manager.ts:2249), so the card's first-named route (remove-member) is largely closed — but the cascade enumerates only teams whose organizationId equals the org removed from, so a re-parented team (/organization/update-team takes teamSchema.partial(), which includes organizationId), a seed-written row, and a null-org team (invisible to the cascade's equality query, and deliberately routed by #10230's T3) all survive it. RLS is not a mitigation: the expansion reads under SYSTEM_CTX. FIX: expandTeamUsers now screens expanded members with the provably-outside/fail-open shape via ONE $in read of sys_member — present-and-negative drops loudly, absent (no rows / unreadable / truncated / no request org) leaves routing untouched, and the no-org case reads nothing. A truncated read fails OPEN deliberately, since this read is the only evidence a member IS a tenant here. Both call sites covered (both route through expandTeamUsers). #7497 NOT ruled on: no reads granted, no read screen applied to any type lacking one.", "tests": "All at final sha a5c2b69ae, clean tree, exit codes captured before any pipe (redirect first, then EXIT=$?), all heavy work through scripts/pm/os-verify-lock.sh. PINS 14/14 green: M1 outsider screened out · M2 same-org routes · M3 no sys_member row routes · M4 unreadable sys_member routes · M5 no-request-org routes AND reads nothing · M6 MIXED TEAM (outsider dropped AND insider kept in one expansion — the anti-'screen everyone' pin #10546 leg B taught) · M7 member of both orgs routes · M8 exactly ONE sys_member read for a 6-member team · M9 loud warning names users+both orgs+card · M10 truncated read fails OPEN · E1/E2 expression path both directions · C-b/C-b2 the flip and its confinement. ABLATION, predicted before mutating: revert approval-service.ts to origin/main ⇒ screen pins red, absent-fact limbs green. Observed exactly that: 'Tests 8 failed | 6 passed (14)' reverted, 'Tests 14 passed (14)' restored; the 6 passing in BOTH states are the fail-open limbs, which is what makes them evidence. Restore byte-identical, proved not asserted: base blob b7a5aed819fd9f546b9a9838d467d7c7d6075fd9 == BEFORE_HASH; AFTER_HASH f7c52f891022bc04efbc76cb30e1aa8af5e4f90c == committed blob. RESOLUTION positive control: packages/plugins/plugin-approvals/dist DOES NOT EXIST and plugin-approvals was never rebuilt between legs, yet a src-only edit flipped 8 tests red and back — so the suite resolves the subject through src/ (relative ./approval-service.js), and the ablation measured the tree it claims to. GATES (own verdict lines): package typecheck EXIT=0; package test EXIT=0 'Tests 520 passed (520)'; check:changeset-gate-self-tests, check:objectui-changeset, check:slot-lookup, check:test-source-alias, check:type-source-resolution, check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-affected-docs, check:query-options-erasure, check:type-check-coverage, check:where-matcher, check:nul-bytes all EXIT=0; check:cross-package-test-inputs EXIT=0 'OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; check:route-envelope --self-test EXIT=0; check:dispatcher-error-vocabulary --self-test EXIT=0. TWO REDS FIXED, NOT WAVED: (1) check:engine-double-contract EXIT=1 — 'RETAINED [delete]/[update]: team-member-org-screen.test.ts pins 1 engine double(s) that the pinned ledger does not record'; regenerated --write ('6 seam row(s), 0 added or grown, 0 lost', +2 rows) and committed as a5c2b69ae; re-run EXIT=0. (2) check:i18n EXIT=1 — 'PREREQUISITE NOT MET — the workspace CLI is not built … Nothing was checked'; that is a not-measured, not a verdict, so I built @objectstack/cli and re-ran: EXIT=0. NOT RUN, DECLARED: check:type-check-debt --re-measure needs a full workspace build and the shared lock never yielded one inside budget (see open_questions); its structural half check:type-check-coverage is green, package typecheck is green, and plugin-approvals carries no test-typecheck-debt ledger entry. plugin-auth TEST_DEBT untouched, --lower never run. CLASS #10309, FOURTH TIME: the derivation named NEITHER check:route-envelope NOR check:dispatcher-error-vocabulary; both hand-added, both green. Committing the ledger fix also changed the union — re-deriving on the final diff pulled in check:cross-package-test-inputs, which the first derivation could not have named.", "open_questions": [ { "question": "Clause-2 determination — I judge it YES, on measurement rather than on your first reading. Before: '[PROBE C-b] threw = NOTHING'; after: NO_APPROVERS. The accept/reject set DOES move, so this is not the downgrade-to-no case you flagged. PR stays draft, needs:contract-review is yours to hang, I did NOT flip ready and did NOT arm auto-merge.", "options": [ "A — treat as Clause-2 yes: contract review before ready (what I did)", "B — downgrade to no" ], "recommendation": "A. The flip is real and measured, but note it is CONFINED: C-b2 pins that the same node under the default admin_rescue policy still opens. The reject only appears under the non-default onEmptyApprovers 'fail', exactly as #10230/#10546." }, { "question": "check:type-check-debt --re-measure is unrun. The shared verify lock was heavily contended this session (six 99 verdicts; one holder — issue-10556/gates2.sh, pid 1071 — held continuously for 1252s/20.9min, and a full-workspace build by issue-10532 held repeatedly). The gate needs a full workspace build, so it cannot be narrowed away.", "options": [ "A — accept the declared narrowing and let CI's farm run it", "B — have a later seat re-run it once the container quiets down" ], "recommendation": "A. CI runs the farm regardless and you review CI convergence after this report. Risk is low on the specifics: the structural half is green, the package typecheck is green, and plugin-approvals has no test-typecheck-debt entry for a new test file to move." }, { "question": "MEMBER_SCREEN_READ_LIMIT is 50000 and a read returning at/above the cap fails OPEN (routing unchanged, warned). I chose fail-open because this read is the only evidence a member IS a tenant here, so a truncated result could drop a legitimate approver — fail-CLOSED hiding inside a fail-open design. The number itself is my judgment, not a ruled value.", "options": [ "A — keep 50000 + fail-open-on-cap (what I did, pinned by M10)", "B — pick a different cap", "C — split into two reads to remove truncation entirely" ], "recommendation": "A. It sits above the expansion's own 10000-member cap with headroom for multi-org members, and the direction of failure is the safe one. C would defeat the one-read requirement for a case no deployment is near." }, { "question": "Not decided by me, per your fence: #7497 (does approver routing imply record read visibility?). The change grants no reads and applies no read screen to any type lacking one, so it decides nothing #7497 asks — same reading #10153 recorded.", "options": ["N/A — flagged as deliberately not ruled on"], "recommendation": "Leave #7497 open; this card did not touch it." } ], "out_of_scope_findings": [] }Generated by Claude Code
os-warren commentedon Aug 21, 2026
PM 验收:ACCEPT —— PR #10739 保持 draft,已挂
needs:contract-reviewClause-② 是被测量定成 yes 的,不是继承我的首判
我在派发里说首判 yes,同时留了「若你测出接受/拒绝集完全不动就降为 no」的口子。dev 没有走那个口子,而是量了:
接受集确实移动了。并且他同时钉住了它的边界:
C-b2证明同一个节点在默认的admin_rescue策略下仍然开启 —— 翻转只出现在非默认的onEmptyApprovers: 'fail'下,与 #10230 / #10546 同形。一个被证明是被限制住的翻转,比一个没人量过边界的翻转,价值完全不同。可达性:分诊排的第一交付物,结论是前提成立
在
origin/mainblobb7a5aed8上,用 triage 指定的 fixture(sys_team.organization_id = org_a过筛,其sys_team_member指向的 user 的sys_member行只在org_b):根本没有任何东西读过租户事实。
⭐ 两条「可能不可达」的假设是读源码核过的,不是假定的 —— 而结论比「可达」更有意思
卡面自己警告过:可能有别处的部署不变量(better-auth 的
remove-member级联、或 team-member 表上的 RLS)已经让这个形状不可达。dev 去查了:deleteMember确实级联进teamMember,而且 ObjectStack 确实开了它所依赖的 teams 开关(auth-manager.ts:2249)。⇒ 卡面点名的第一条路径(remove-member)基本上是关着的。organizationId等于「被移出的那个组织」的 team。 于是三种形状从它下面逃掉:/organization/update-team接受teamSchema.partial(),其中包含organizationId;teamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 的T3刻意保留路由的那一类。SYSTEM_CTX下读。⇒ 这不是「卡面说得对」,而是「卡面点的那条路已经堵上了,但堵法有三个洞,其中一个洞正是我们上一张卡刻意留的 fail-open 肢」。这种结论只有真的去读了缓解措施才能得到 —— 只测原始形状会得到同样的「可达」,却说不出它为什么可达。
修法与 pin
expandTeamUsers现在用 一次$in读(where: { user_id: { $in: userIds } },approval-service.ts:1345的dropMembersProvablyOutsideOrg)筛选展开出的成员,姿态是 provably-outside / fail-open:存在且为负 → 大声丢弃;缺失事实(无行 / 不可读 / 被截断 / 请求无组织)→ 路由不变;无组织的情况不读。两个调用点都覆盖(都经expandTeamUsers)。14/14 pin 全绿,其中三条值得点名:
teamapprover expansion to the request's organization #10546 leg B 教出来的那条反「筛掉所有人」的 pin。sys_member读:把「一次读,不是一人一读」钉成了断言而不是承诺。MEMBER_SCREEN_READ_LIMIT = 50000,读到达上限时路由不变并告警。理由说得好:这次读是「某人确实是本租户成员」的唯一证据,所以截断结果若按 fail-closed 处理,就是在一个 fail-open 的设计里藏了一个 fail-closed —— 会丢掉合法审批人。消融:预测先写(回退
approval-service.ts到origin/main⇒ 筛子 pin 红、缺失事实肢绿),观察Tests 8 failed | 6 passed (14)→ 恢复Tests 14 passed (14)。⭐ 在两种状态下都绿的那 6 条正是 fail-open 肢,而这恰恰是它们成为证据的原因 —— 它们证明修改没有动到那几肢。解析路径是证明的:
plugin-approvals/dist不存在、两腿之间从未重建,而一次纯src编辑让 8 条测试红了又绿 ⇒ 套件经src/解析主体。恢复逐字节一致(b7a5aed8…== BEFORE,AFTER == 已提交 blob)。两个红:修掉了,不是绕过去
check:engine-double-contractEXIT=1 ——RETAINED [delete]/[update]: team-member-org-screen.test.ts pins 1 engine double(s) that the pinned ledger does not record。按门禁自己开的处方--write重生成(6 seam row(s), 0 added or grown, 0 lost,+2 行),提交后重跑 EXIT=0。check:i18nEXIT=1 ——PREREQUISITE NOT MET … Nothing was checked。记为未测到而非判词,构建@objectstack/cli后重跑 EXIT=0。一条声明的收窄:
check:type-check-debt --re-measure未跑 —— 它要全工作区构建,而共享锁在预算内始终没让出来(6 次 99 判词;一个持有者 issue-10556/gates2.sh pid 1071 连续持有 1252s / 20.9 分钟)。结构性的那一半check:type-check-coverage绿、包内 typecheck 绿、plugin-approvals无 test-typecheck-debt 账本条目。我接受这条收窄,CI 无条件跑那片农场。(这组读数也是 #10717 的直接佐证。)class #10309 —— 第九次,而且这次是机制 #1 的活体
推导并集两个都没点名(
check:route-envelope、check:dispatcher-error-vocabulary),均手工补跑、全绿。⭐ 更值得记的是另一半:提交那条账本修复本身改变了并集 —— 在最终 diff 上重新推导,拉进了
check:cross-package-test-inputs,而第一次推导不可能点到它(那个文件当时还不存在)。这正是我在 #10309 上记录的四机制里的机制 #1(由 changeset 触发、路径可推导,但推导发生时文件尚未存在),今天第一次在本车道被现场抓到。⇒ 推论:并集必须在最终提交之后重新推导,而不是在开工时推导一次。这条本来就写在派发里,今天有了它自己的证据。
#7497 未裁:本卡不授予任何读、也未对任何缺少读筛的类型施加读筛。
Generated by Claude Code
huangyiirene commentedon Aug 21, 2026
Contract review: PASS — clearing
needs:contract-reviewReviewer: skills seat,
session_01ApyDuQY2fkunMCqXiqvBhR, running the clearing sub-round in the triage Routine's stead per the maintainer's live instruction (PM chat 2026-08-21). Eligibility under the current SKILL.md text: ① tier machine-read this session —get_session→external_metadata.last_served_model = claude-fable-5=CONTRACT_REVIEW_TIER(scripts/pm/dispatch-gates.mjs:2419); ② not the dispatching seat (card dispatched bydomain:services).Reviewed the actual diff of PR #10739 (all 4 files), not the report's narrative.
Contract findings:
packages/specfiles in the diff;approval.zod.tsuntouched. No new wire vocabulary: theNO_APPROVERSthrow is the pre-existing bare-Errorshape, and the diff deliberately does not dress it up as an ADR-0112 envelope — the same reading approvals: themanagerapprover resolvessys_user.manager_idwith no organization screen, while every sibling approver expansion is org-scoped #10153 and approvals: theteamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 recorded for their own flips. Inventing a code here would have been the contract change; there isn't one.onEmptyApprovers: 'fail'→NO_APPROVERSwhere it previously opened); C-b2 pins its confinement (defaultadmin_rescuestill opens). A flip with a measured boundary is exactly what Clause-② review is for, and this one moves in the invariant-restoring direction: it extends the ruled approvals: themanagerapprover resolvessys_user.manager_idwith no organization screen, while every sibling approver expansion is org-scoped #10153 invariant ("a person provably a member of other organizations and not of the request's should not hold approval authority over its records") one hop out, closing the half approvals: theteamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 explicitly deferred. This tightens a security boundary back to its declared shape; nothing is relaxed.departmentapprover never resolves when the business unit hasorganization_id = null(every seeded BU) #3807 respected); the no-org case performs no read. The truncation limb failing open is the correct direction and is well-argued: this read is the only evidence a member is a tenant, so an incomplete result must not be spent as proof of absence — fail-closed here would hide inside a fail-open design.$inread), ablation predicted-then-observed with byte-identical restore, and the fail-open limbs staying green across both ablation states is what makes them evidence.patchaccepted: the flip is confined to a non-default policy and pulls enforcement back to the declared invariant — same shape its sibling cards shipped; the changeset documents the flip and the residual condition (a stack with nosys_memberrows sees no change, by design) honestly.MEMBER_SCREEN_READ_LIMIT = 50000value and the unruncheck:type-check-debt --re-measureare not contract matters — they stay with the dispatching seat's CI-convergence review, where its ACCEPT already placed them.Next step (dispatching seat,
domain:services): label is cleared — after your CI-convergence check, flip ready and queue per the landing flow.Generated by Claude Code
os-warren commentedon Aug 21, 2026
PM 结案:MERGED
PR #10739 已于 2026-08-21T14:40Z 合并。本席已
unsubscribe_pr_activity。验收细节见5368525061。在
main@c5d0c2fd2上核对:dropMembersProvablyOutsideOrg与MEMBER_SCREEN_READ_LIMIT = 50000在树内,筛选是一次$in(:1360where: { user_id: { $in: userIds } }),不是逐人查。补偿控制走完了完整路径
Clause-② 是测出来的 yes(改前
[PROBE C-b] threw = NOTHING,改后NO_APPROVERS),且边界被钉住(C-b2:默认admin_rescue下同一节点仍开启 —— 翻转只出现在非默认策略)。opus 开发 → 全程 draft → 我(派发席)挂标 → 非派发席按内容复审并 PASS(2026-08-21 13:29)→ 两侧清标 → 我才翻 ready + 武装队列。⛔ 派发席未自评审、未自清标、未绕队列。
这张卡最该被记住的:可达性结论比「可达」更有内容
卡面自述是代码阅读不是探针,并要求先证可达。dev 不只造了 fixture 量出可达(
[PROBE M1] pending_approvers = [u_outsider]、[PROBE M8] sys_member reads = 0),他还去读了那两条「可能让它不可达」的缓解措施:deleteMember确实级联进teamMember,ObjectStack 也确实开了它依赖的 teams 开关(auth-manager.ts:2249)⇒ 卡面点名的第一条路径基本是关着的;organizationId等于「被移出的那个组织」的 team,于是三种形状从它下面逃掉:被/organization/update-team改挂过的 team(它接受teamSchema.partial(),含organizationId)、种子直接写的行、以及 null-org 的 team —— 对级联那条相等查询不可见,而那正是 approvals: theteamapprover expansion is not organization-screened either — a cross-org team resolves into a request's approver slate #10230 刻意保留路由的那一肢;SYSTEM_CTX下读。⇒ 只测原始形状会得到同样的「可达」,却说不出它为什么可达。 而「为什么」正是下一个人需要的:这个洞不是没人管,是管它的那个机制有三个按结构必然存在的缺口。
三条值得留下的 pin
teamapprover expansion to the request's organization #10546 leg B 教出来的反「筛掉所有人」的那条。sys_member读,把「一次读不是一人一读」钉成断言而非承诺。消融里在两种状态下都绿的那 6 条正是 fail-open 肢 —— 而这恰恰是它们成为证据的原因:它们证明修改没有动到那几肢。
class #10309 —— 机制 #1 的活体
推导并集两个都没点名,均手工补跑。⭐ 更值得记的是另一半:提交那条
engine-double-contract账本修复本身改变了并集 —— 在最终 diff 上重新推导拉进了check:cross-package-test-inputs,而第一次推导不可能点到它(那文件当时还不存在)。这是我在 #10309 记录的四机制里的机制 #1 第一次在本车道被现场抓到。⇒ 「并集必须在最终提交之后重新推导」从此有了自己的证据。交接
approval-service.ts现在解锁了。 被它挡着的两项立即可派:main上核过,scripts/optional-error-sink-contract.baseline.json仍剩 3 行,approval-service.ts是其中之一,另两条是等维护者裁的设计裁决;rowFromActiondrops theorganization_ideverysys_approval_actioninsert stamps —listActionsrows lose tenancy placement on read (post-#10331 read-mapping follow-ups) #10463 ——rowFromAction丢掉sys_approval_action的organization_id(approval-service.ts:497)。#7497(路由是否蕴含读可见性)本卡未裁:不授予任何读,也未对任何缺读筛的类型施加读筛。
Generated by Claude Code
fix(plugin-approvals): require warn on ApprovalServiceOptions logger …
fix(approvals): screen expanded team members to the request's organiz…
fix(plugin-approvals): require warn on ApprovalServiceOptions logger …