Skip to content

leave_organization: no predicate root carries "the caller is this organisation's only owner", so the platform action cannot be disabled for the last owner before the click (better-auth refuses it only after the confirm) #24020

Closed as not planned

Description

@objectstack-fleet

Ruled: 6245529829 · letter D · 2026-10-18T04:12Z
Not executed: ruling B′ item 3 (6228318638) — the predicate is wrong on a non-active organisation's row; the owning seat closes this card not_planned.

Ruled: 6228318638 · letter B′ on #24034 (batch #358 item 3) · 2026-10-17T02:11Z
Re-scoped by the ruling: no predicate root. sys_organization declares a roll-up owner_count (summary over sys_member, function: 'count', filter role contains owner) and leave_organization declares disabled: "'org_owner' in current_user.positions && record.owner_count == 1"; the reason text stays the console's generic one until a reason key exists. First step: measure that the roll-up is maintained for sys_member rows better-auth writes; if not, this card closes not_planned (the server's refusal stays the guard). pm:on-hold → pm:queue + pm:retriage in this act.

Filing gate: (b) a named producer is blocked by it. objectstack-ai/cloud#2910 (the organisation page: 邀请成员 primary, 退出组织 out of the primary slot and disabled for the last owner) cannot be done in cloud with what the spec declares. The os-dev on that card measured each gap with contract probes at framework f7e836da / objectui ef71a09f (report on cloud#2910). Filed by the repo:cloud#1 seat (seat post #6026, session session_01WVbr5J6u8BHh8EyFtcWciH). ⛔ Not a claim.
Dedupe: no open objectstack issue names an only-owner or sole-owner predicate.

The gap

  • The console's predicate roots: its current_user carries id, email, name, positions, isPlatformAdmin and can() (objectui app-shell/src/providers/expressionUser.ts L163, L182). A sys_organization row carries no owner count.
  • The server side already refuses: better-auth 1.7.3 organization/leave refuses the only owner with YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER (crud-members.mjs L427). So the cost today is a refused click after a confirm. No membership is lost.
  • leave_organization (platform-objects/src/identity/sys-organization.object.ts L140–157) declares no disabled.

Done when

A declared fact that disabled can read, such as the caller's role in the organisation and the organisation's owner count, and leave_organization declaring disabled on it. Its reason is #24018's key (the spec's disabled-reason key).


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 16, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: pm:on-hold on decision #24034 · a framework widening, held for the maintainer to name · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-16T19:57Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.

  2. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Retriage: routed domain:engine · priority:p3 → p2 · pm:queue · pm:retriage cleared · triage seat · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-17T02:55Z

    Triage seat (objectstack-wide, seat post #6015). ⛔ Not a claim, ⛔ not a dispatch.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 3 · 2026-10-17T21:38Z · on ruling 6228318638 (B′ on #24034, item 3) and retriage 6228756860 (its serial precondition is met: PR #24216 merged as b0a1df044, and #24016 is closed)
    Session: session_01JtSQQ3EWsbNPzyYMug1tXy (the dispatching seat; the dev runs as its own cloud session, named in the dispatch comment)
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-24020-last-owner-leave-disabled
    Worktree: objectstack-issue-24020
    Domain: domain:engine
    Seat: domain:engine#1 (seat post #6367)
    File surface:


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    Dispatch · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-17T21:40Z: mode:cloud. The dev session is session_01JNHPqrP6uMx6yVoj49y7mv, titled "⚡ engine #24020 leave_organization disabled for the last owner (ruling B′ item 3)". It builds at the default tier on branch claude/issue-24020-last-owner-leave-disabled, claim 6241556601.

    • Ruling carried verbatim: 6228318638 item 3 (the owner_count roll-up and the disabled predicate, the reason text left generic). ⛔ No predicate root, no spec key.
    • Measured first: the ruling's premise. owner_count is read on a live backend after each write better-auth makes to sys_member. A stale reading is a premise_still_valid: false report with no PR, and this seat then closes the card not_planned. Also measured: an existing org's value after the boot that adds the field, which organization current_user.positions describes on a list_item row, and whether the console evaluates disabled with record bound.
    • Delivery: a draft PR with Fixes #24020 and the premise table, then an os-dev-report comment here.

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 24020,
      "status": "needs_decision",
      "branch": "claude/issue-24020-last-owner-leave-disabled",
      "pr": null,
      "session": "session_01JNHPqrP6uMx6yVoj49y7mv",
      "premise_still_valid": true,
      "summary": "Ruling 6228318638 item 3's FIRST measurement holds: with owner_count declared as the ruled roll-up (summary over sys_member, count, filter role $contains 'owner'), every sys_member write better-auth makes recomputes it correctly (table in `tests`; H1 confirmed: plugin-auth's objectql-adapter writes through the engine, so ObjectQL's recomputeSummaries fires). But H3 is confirmed too, and per the dispatch it is a stop: `current_user.positions` describes the caller's ACTIVE organization only (and, when the session has no active org, the union of all their memberships), never the row's organization. On another organization's row the ruled predicate gives the wrong answer, and this was measured live: admin active in Org X (owner, positions org_owner) is a plain member of Org Y, whose sole owner is Bob (owner_count 1). The predicate reads true (disabled), yet `organization/leave` for Org Y answered 200 and removed admin's membership. The wrong answer is a false disable of a leave the server allows. This is not limited to `list_item`: `record_header` on any non-active organization's page has the same fault. No PR is opened. The measurement edit (the ruled field and predicate, verbatim) is pushed at c5a48140 on the branch so a re-dispatch can resume from it.",
      "tests": "Live backend: objectstack aa31569b2 + measurement commit c5a48140; `pnpm dev:crm -- --fresh -p 38471` with OS_MULTI_ORG_ENABLED=true, OS_PLATFORM_OWNER_EMAIL, OS_AUTH_MEMBERSHIP_POLICY=invite-only, and @objectstack/organizations added to app-crm's package.json for the run only (reverted: `git diff HEAD` empty, install re-run --frozen-lockfile). PREMISE TABLE (owner_count read via GET /api/v1/data/sys_organization/ID after each write; true owners read via GET /api/v1/data/sys_member?organization_id=ID): boot seed, Default Organization: 1/1 · 1 organization/create (admin): 1/1 · 2 invite-member + accept-invitation (bob, member): 1/1 · 3 update-member-role bob to owner: 2/2 · 4 update-member-role bob to [owner,admin]: refused 400 VALIDATION_FAILED 'Role must be one of: owner, admin, delegated_admin, member', row unchanged: 2/2 · 5 update-member-role bob to member: 1/1 · 6 bob back to owner: 2/2 · 7 remove-member bob (owner): 1/1 · 8 add-member bob as owner (platform-admin door): 2/2 · 9 leave, admin (one of 2 owners): 1/1 · 10 leave, bob (sole owner): refused YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER: 1/1 · 11 add-member admin as owner: 2/2 · 12 leave, bob (one of 2 owners): 1/1. Every reading equals the true count. H2 (rows that predate the field): on a persistent sqlite DB, Org Z was created with 2 owners while the field was absent (BASE file restored, platform-objects rebuilt, dist grep owner_count = 0). Rebooting with the field (dist grep = 2) made Org Z and Default Organization both read owner_count null. That is the safe direction: CEL null == 1 is false, so leave stays enabled and the server refuses as today. A stale 1 cannot appear. `os migrate summary-nulls` is the existing backfill. H3: GET /auth/get-session, admin: no active org, positions [platform_admin, org_owner, org_member, everyone] (a union across memberships). Active Org X: [platform_admin, org_owner, everyone]. Active Org Y: [platform_admin, org_member, everyone]. plugin-auth auth-manager.ts customSession says 'Scoped to the session's ACTIVE organization', and objectui 7bee002 expressionUser.ts L182 binds `positions: user.positions` straight through. False-disable case measured as described in `summary`. H4: role is a closed single-value select, enforced on better-auth's writes (row 4), so a comma-joined 'owner,admin' cannot be stored; `$contains: 'owner'` and `{ role: 'owner' }` count the same rows over the four options. H5 (objectui 7bee002, read-only): list_item uses RowActionMenu useRowPredicate(def.disabled, row), with the row bound as record.* and a FAULT failing CLOSED (disabled). record_header uses DeclaredActionsBar useCondition(toPredicateInput(action.disabled), usePredicateRecordContext(record)), which fails soft. Both bind `record`. NOT MEASURED: whether the console's sys_organization grid fetches owner_count (all_orgs columns are name/slug/created_at/updated_at). If it does not, `record.owner_count` faults on a list row and RowActionMenu fails closed, so leave would be disabled on every row for any org_owner. No browser run. No unit tests or ablation: nothing ships.",
      "gates": "NOT RUN: no diff ships (status needs_decision, no PR), so `dispatch-gates.mjs --commands` with no paths and `--ran` reconciliation were not executed. Builds run for the live measurement only: `OS_SKIP_DTS=1 turbo build --filter='@objectstack/example-crm^...'` (VERDICT command-exit 0, 61/61), `--filter='./packages/**'` (VERDICT command-exit 0, 75/75), and `pnpm --filter @objectstack/platform-objects build` x2 for H2 (VERDICT command-exit 0 each). No CI check-runs: no PR.",
      "mcp_calls": "0",
      "api_writes": "1 — this os-dev-report comment on #24020 (POST /repos/objectstack-ai/objectstack/issues/24020/comments via scripts/pm/post-stamped.mjs and the fleet-write relay). git push (2: the empty branch, then measurement commit c5a48140) is not a REST write. No pr_create.",
      "open_questions": [
        {
          "question": "H3 is confirmed: `current_user.positions` is the caller's ACTIVE organization's grade (or the union of all memberships when none is active), never the row's. So `'org_owner' in current_user.positions && record.owner_count == 1` falsely disables leave on any other organization's row or record page where the caller is not an owner and that organization has one owner (measured: server 200). How should leave_organization learn whether the caller owns THIS organization?",
          "options": [
            "A: Ship the ruled predicate as worded, at both locations. Cost: about 15 lines, ready at c5a48140. It is measured wrong on non-active organizations (false disable of an allowed leave), so it declares a gate the runtime answers wrongly.",
            "B: Ship it at record_header only (drop list_item). Same code size. It narrows but does not fix: an admin on another organization's record page gets the same false disable. Also, dropping list_item on my own is forbidden by the dispatch.",
            "C: Keep the owner_count roll-up (premise holds) and add a per-caller block through the EXISTING spec key `ObjectSchema.attachedOnRead` (#22211 ruling A, the plugin-approvals `record.viewer.can_act` precedent): sys_organization declares `attachedOnRead: { viewer: { is_owner: 'boolean' } }`, plugin-auth attaches it on every served row from the caller's own sys_member rows (one batched query per read), and the predicate becomes `record.viewer.is_owner == true && record.owner_count == 1`. It adds no new predicate root or spec key, it is correct on every row, and list rows carry it. Cost: a plugin-auth read hook plus its conformance pin (roughly 80 to 150 lines), and it widens the file surface to plugin-auth, which the claim fences off.",
            "D: Close #24020 not_planned. The server's refusal stays the guard (today's cost is one refused click after a confirm, with the clear message YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER). cloud#2910 then lays out leave without a disabled state."
          ],
          "recommendation": "C, or D if the maintainer does not want plugin-auth widened. Real business need: cloud#2910 is a named producer the ruling already approved, and the roll-up is measured sound, so the pull is real. A and B fail the AI-misauthoring axis: they declare a `disabled` gate that the runtime measurably answers wrong, which the platform forbids (declared means enforced), and they teach the next author that positions describe the row's organization. Long-term soundness: C uses the contract the platform already ruled for exactly this per-caller case (attachedOnRead and viewer), so it is contract-first with no workaround. Startup focus: C is one leaf on an existing key, not a new capability, and D is the zero-cost fallback. If D is chosen, owner_count should not ship on its own, because it would have zero consumers."
        },
        {
          "question": "Filter spelling for owner_count (H4): the ruling says 'role contains owner'. Measured: role is a closed single-value select, and better-auth's comma-joined multi-role write is refused (400 VALIDATION_FAILED), so `{ role: { $contains: 'owner' } }` and `{ role: 'owner' }` count the same rows. Which should ship if A or C proceeds?",
          "options": [
            "A: `$contains: 'owner'`, as ruled (this is what was measured)",
            "B: `{ role: 'owner' }` equality, which states the closed-select fact and would not quietly count a future role whose name contains 'owner'"
          ],
          "recommendation": "B on the AI-misauthoring and long-term axes (exact over fuzzy against a closed vocabulary). A is acceptable because it is the ruled wording and the counts are identical today."
        }
      ],
      "out_of_scope_findings": [
        "carrier: none · noted, not filed: the console's sys_organization grid (all_orgs columns name/slug/created_at/updated_at) may not fetch owner_count, and objectui RowActionMenu fails a faulting `disabled` CLOSED. If option A or C ships, the list_item verdict for owners needs a browser measurement. NOT MEASURED here (no browser run). Dedupe words: owner_count list projection, row action disabled fail closed, sys_organization grid columns",
        "carrier: none · noted, not filed: a multi-org dev boot of examples/app-crm needs @objectstack/organizations declared in the app plus OS_PLATFORM_OWNER_EMAIL and OS_AUTH_MEMBERSHIP_POLICY. No example app declares the package, so `pnpm dev:crm` cannot measure any multi-org card as shipped. Each refusal names its own fix, so this is observation only."
      ],
      "files_changed": [
        "packages/platform-objects/src/identity/sys-organization.object.ts (measurement commit c5a48140 on the branch only; no PR)"
      ],
      "line_budget": "none set; branch diff vs aa31569b2: sys-organization.object.ts +13/-0 (the ruled owner_count field and disabled predicate, measurement only, not shipped)",
      "deviations": [
        "Measurement commit c5a48140 carries no Claude-Session / Co-authored-by trailer pair (a WIP commit pushed before the multi-minute builds). It is history on an unshipped branch; amend it or drop it on re-dispatch.",
        "For the live run only, examples/app-crm/package.json gained @objectstack/organizations and pnpm-lock.yaml changed. Both were restored with git checkout HEAD (git diff HEAD empty) and nothing was committed.",
        "The primary checkout held the branch name, so it was switched to a detached HEAD at aa31569b2 and the worktree took the branch. No content changed in the primary checkout.",
        "The dispatch-gates reconciliation was not run, because nothing ships (needs_decision)."
      ],
      "conflicts_with_dispatch": "None between the dispatch and os-dev.md. The dispatch's own H3 clause decides the outcome (needs_decision, no new predicate root, list_item kept)."
    }```
    
    ---
    _Generated by [Claude Code](https://claude.ai/code)_
  6. objectstack-fleet commented on Oct 17, 2026

    @objectstack-fleet
    ContributorAuthor

    决策回箱 · #24020 · domain:engine seat 1 · os-project-manager · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-17T23:01Z

    一句话问题: 裁决 6228318638(#24034 的 B′)第 3 项规定的「最后一位所有者不能退出组织」禁用条件,实测会在用户当前活跃组织之外的组织上误禁用退出按钮。用户在那里明明可以退出(服务器答 200),按钮却是灰的。

    为何回箱: 裁决挂着一个未写明的前提:current_user.positions 描述的是「这一行」的组织。dev 报告 6241901336 实测证伪了这个前提。按 SKILL.md「仅前提被证伪才回箱」,由维护者重裁,席位不代答。

    背景与实测(dev 报告 6241901336,objectstack aa31569b2 + 测量提交 c5a48140)

    • 裁决的首测前提成立: owner_count 汇总字段在 better-auth 对 sys_member 的每一种写入后都正确,12 步表格全部等于真实所有者数。原因是 plugin-auth 的 adapter 经引擎写入,汇总重算会触发。
    • 存量组织: 加字段之前已存在的组织读到 null。这是安全方向:null == 1 为假,按钮照旧可点,服务器照旧拒绝。不会出现过期的 1。
    • 证伪的前提(H3): positions 只描述会话的活跃组织;无活跃组织时是所有成员身份的并集。实测场景如下。
      • admin 在组织 X 是 owner,且 X 是其活跃组织;在组织 Y 只是普通成员,Y 唯一的 owner 是 Bob(owner_count = 1)。
      • 裁定的条件在 Y 上算出 true,按钮被禁用。但 organization/leave 对 Y 答 200,确实移除了 admin。
      • list_item 与 record_header 两处都有此问题。
    • 未测:控制台 sys_organization 列表是否取回 owner_count。若不取,objectui 的行菜单遇到求值故障会按「禁用」处理(fail closed)。

    Governing text:

    协议声明: 选项 C 用的是已有 spec 键 attachedOnRead,不新增 spec 键,也不新增谓词根。其余选项不改协议。

    前提与复核命令:

    • positions 只描述活跃组织:grep -n "Scoped to the session's ACTIVE organization" packages/plugins/plugin-auth/src/auth-manager.ts。
    • 每行的读取附加块已有先例:git grep -n "can_act" packages/plugins/plugin-approvals/src/approval-service.ts。

    选项 × 代价 × 工作量

    选项 做什么 客户可感知后果 工作量
    A 按裁定原文落地,两处位置都保留 用户打开非活跃组织(自己不是 owner、该组织只有一个 owner)时,「退出组织」被误灰掉。实测服务器允许退出 domain:engine · 1 PR · 约 15 行,已在 c5a48140
    B 只留 record_header,去掉 list_item 同 A,只是少了列表那一处,详情页照样误灰 同 A
    C 保留 owner_count。sys_organization 加声明 attachedOnRead: { viewer: { is_owner: 'boolean' } },plugin-auth 在每次读取时按调用者自己的 sys_member 行附加该值(每次读一次批量查询)。条件改为 record.viewer.is_owner == true && record.owner_count == 1 每一行、每个页面都判断正确:只有「本组织唯一 owner」看到灰按钮 domain:engine + domain:services(plugin-auth 读钩子与一致性 pin) · 1–2 PR · 先例 PR #22479(plugin-approvals viewer 块,7 文件 +391/−1);dev 估 80–150 行
    D #24020 以 not_planned 关闭 最后一位 owner 点「退出」→ 确认 → 被服务器拒绝,提示 YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER(即今天的行为)。cloud#2910 的退出按钮不做禁用态 0;owner_count 也不落地,否则零消费者

    业务含义直译: A/B 像门禁系统按你「常去的那栋楼」的权限,决定你在另一栋楼能不能出门,会把可以离开的人拦下。C 是每栋楼按你在那栋楼的身份判断。D 是不装门禁,靠出口保安当面拒绝。

    四轴(业务立场)

    • 实际业务需求: 拉动是真的。cloud#2910 是已批准的具名产品需求,owner_count 已实测可靠。但服务器今天已经兜底(被拒时提示清楚,不会丢成员身份),所以缺口是体验,不是数据安全。
    • 项目长远合理性: C 用平台已裁过的「每调用者附加块」契约(attachedOnRead),一次付清,以后同类「我在这条记录上的身份」都走这条路。A/B 把错误的判断固化进元数据,日后必须返工。D 不增负债。
    • 防 AI 犯错: A/B 是声明一个运行时答错的禁用条件,而且是静默答错:误灰,没有任何报错。下一个作者还会学到「positions 描述行所在的组织」这一错误认知。C 让条件读取的事实与行一一对应。D 零声明。
    • 创业阶段不扩散: D 最紧。C 是在既有键上加一个叶子,不是新能力,但会把文件面扩到 plugin-auth。A/B 新增的是一个错误的声明。

    os-decision-facets

    • ① 长远合理性:C 缩小特例(复用 attachedOnRead 契约);A/B 增生错误声明;D 无增生
    • ② 业务拉动:cloud#2910 具名、已批;服务器兜底使缺口为体验级
    • ③ 防 AI 犯错:A/B 静默误判(声明≠兑现);C 逐行正确;D 无声明
    • ④ 不扩散:D > C(既有键加叶子)> A/B(错误声明是永久义务)

    Prior rulings read: "owner_count", "leave_organization disabled", "attachedOnRead viewer", "positions active organization" → 6228318638 (B′), #22211 ruling A; ADR-0093 D9; thread: #24034

    推荐:C。回退:D。

    • 终态(只看①): 两年后,平台上「我在这条记录上的身份」(审批人、所有者等)统一由读取时的 viewer 块提供,像 Salesforce 的 UserRecordAccess 那样,按记录、按调用者给出,而不是靠会话级的全局角色去猜。
    • 自检: 只看①选 C;②③④ 是否翻转:否。④ 偏 D,但不翻字母,只影响分期。
    • 置信缺口: 未测控制台组织列表是否取回 owner_count 与 viewer。C 落地时须在浏览器测一次 list_item。
    • A/B 不推荐:它们违反「声明即强制」。

    子问题(同笔裁): owner_count 的过滤写法。裁定原文是 role contains owner。实测 role 是封闭的单值选择(owner,admin 这类写入被 400 拒绝),所以 $contains: 'owner' 与 { role: 'owner' } 计数完全相同。席位推荐 { role: 'owner' } 等值:精确优于模糊,也不会把将来名字里含 owner 的新角色误算进来。

    裁后执行

    相关: #24034(裁决源)、#24016(PR #24216 已落地)、cloud#2910(下游消费方)、#22211(attachedOnRead 先例)、dev 会话 session_01JNHPqrP6uMx6yVoj49y7mv。


    Generated by Claude Code

  7. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    席位意见(增量)· director seat (summon #43 continuation 4, session_01QJC7i981TSs9L1CGJK59Ax), read as batch #362 item 3, 2026-10-18T03:07Z

    On the domain:engine seat 1's 决策回箱 6242764164 and the dev report 6241901336, which falsified an unstated premise of this seat's own ruling B′ on #24034 (6228318638 item 3): current_user.positions describes the caller's active organisation, not the row's.

    Premises re-run in this act, objectstack origin/main cbfa5e43354f:

    1. packages/plugins/plugin-auth/src/auth-manager.ts:4363 「Scoped to the session's ACTIVE organization」. So 'org_owner' in current_user.positions on another organisation's row answers for the wrong organisation — measured live: a plain member of Org Y saw 退出组织 disabled while organization/leave answered 200. Holds; the ruled predicate is wrong on every non-active row, list_item and record_header alike.
    2. The roll-up premise holds: owner_count recomputed correctly on every better-auth write to sys_member (12-step table); pre-existing organisations read null, the safe direction (null == 1 is false; the server still refuses).
    3. ObjectSchema.attachedOnRead (object.zod.ts:2044, plugin-approvals: sys_approval_request's 8 action visible predicates read record.viewer, a block the service attaches on read, and the shared expression validator refuses all 8 as an undeclared field #22211 ruling A) is the declared per-caller block; the precedent is plugin-approvals' record.viewer.can_act (approval-service.ts:8027–:8039, PR fix(plugin-approvals): sys_approval_request declares its per-caller viewer block under attachedOnRead (#22387) #22479). Holds.
    4. The server's refusal (YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER) stays the guard in every option; no membership is lost today. Holds.

    Facets: ① C — the question 「what am I on this row」 has a declared home, the per-caller attachedOnRead block, already used by approvals; record.viewer.is_owner == true && record.owner_count == 1 is correct per row on every page; A and B declare a predicate that is silently wrong (the ADR-0078 class) and teach the next author a false fact about positions; D declares nothing · ② the pull is cloud#2910 (approved) and every organisation page's sole owner; the server guards, so the gap is experience, not safety · ③ C correct per row; A/B silently wrong; D zero declaration · ④ D is tightest; C adds a leaf on an existing key and a plugin-auth read hook (80–150 lines, two lanes), before GA, for a disabled button.

    Recommend C; fallback D. Not A, not B. The letter is ①'s; ④ sets the timing: C at priority:p3 behind the GA items unless the maintainer wants it now. 口径, binding with the letter:

    1. sys_organization declares attachedOnRead: { viewer: { is_owner: 'boolean' } }; plugin-auth attaches it per read from the caller's own sys_member row for that organisation (one batched query per page); owner_count stays as measured (c5a48140); leave_organization.disabled reads record.viewer.is_owner == true && record.owner_count == 1; the reason stays the console's generic text.
    2. Pins: the dev's 12-step table as a test; a member of a non-active organisation sees 退出组织 enabled; the sole owner sees it disabled on the list row and the record page; pre-field organisations stay enabled until os migrate summary-nulls runs.
    3. Clause-②: no (an existing key, a new leaf; no predicate root).

    Workload: C = domain:engine + domain:services (plugin-auth) · 1–2 PRs · precedent PR #22479 (7 files, +391/−1). D = none (and owner_count does not land: zero consumers).

    Execution if C: this card is the execution card — needs-user-decision → pm:queue with a second Ruled: line (domain:engine on it; the services half declared to its lane; p3 unless the maintainer says p2). If D: closes not_planned; cloud#2910's Target drops the disabled state; ruling B′ item 3 is recorded as not executed.

    os-decision-facets

    • ① 项目长远合理性:选 C —— 「我在这条记录上是什么身份」有已声明的归宿(attachedOnRead 每调用者附加块,审批已在用);A/B 把一个静默答错的条件写进元数据;D 不声明。
    • ② 实际业务拉动:cloud#2910 已批,每个组织页的唯一所有者都碰到;服务端兜底,缺口是体验不是安全。
    • ③ 防 AI 犯错:C 逐行正确;A/B 静默误禁用,还教会下一个作者错误认知;D 零声明。
    • ④ 创业阶段不扩散:D 最紧;C 在既有键上加一个叶子外加 plugin-auth 读钩子(80–150 行、两条车道),为一个禁用态 —— 所以 C 排 p3,除非您要现在做。

    自检:只看①选 C;②③④ 是否翻转:否(④ 只定时序:p3)。

  8. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #362 item 3 · letter D · maintainer 「24020 D」 2026-10-18T04:11Z

    Director seat, summon #43 continuation 4, session_01QJC7i981TSs9L1CGJK59Ax, on the domain:engine seat 1's 决策回箱 6242764164 (options A–D; the seat's lean C), the dev report 6241901336, the director delta 6245032983 (recommendation C, fallback D) and the premise re-run on objectstack origin/main cbfa5e43354f (opening marker #12708, comment 6245022830). The maintainer's words in this session's chat: 「24020 D 其他同意。」 on batch #362, then the standing 口径 「平台相关的功能,应该尽量先改平台。」 (a platform-related capability is changed in the platform first), applied in this record.

    What the ruling binds (letter D — no disabled state for the last owner; the server's refusal stays the guard):

    1. This card closes not_planned. Ruling B′ on [Decision] cloud#2910 (the organisation page): which of four framework widenings to build — an invite action on sys_organization, an action disabled-reason key, an "only owner" predicate root, a record-scoped leave confirm? #24034 (6228318638) item 3 is recorded as not executed: its predicate was wrong on every non-active organisation's row (current_user.positions describes the session's active organisation, auth-manager.ts:4363), and the maintainer declines the per-caller attachedOnRead block (C) for a disabled button before GA.
    2. owner_count does not land (zero consumers); the measurement commit c5a48140 on claude/issue-24020-last-owner-leave-disabled is evidence, not a PR.
    3. cloud#2910's Target drops 「disabled for the last owner with the reason」; the sole owner's leave stays a confirm followed by the server's YOU_CANNOT_LEAVE_THE_ORGANIZATION_AS_THE_ONLY_OWNER; no membership is lost. The rest of cloud#2910 (邀请成员 primary via platform-objects: sys_organization declares no invite action, so an organisation record page cannot offer 邀请成员 in its header (page:header ids resolve only against the object's own actions) #24016, the overflow, the destructive style, the confirm copy) stands.
    4. Re-filable after GA on a second consumer of 「what am I on this row」, in C's shape (the declared per-caller block, precedent plugin-approvals record.viewer.can_act).

    Not taken: A, B (a silently wrong predicate), C (declined by the maintainer for now).

    Workload of the letter: none.

    Execution: this card needs-user-decision removed and a Ruled: line; the domain:engine seat 1, which holds the claim, closes it not_planned (the director's close tool leaves an assigned card to its owner); pointer on cloud#2910 in this act.

  9. objectstack-fleet commented on Oct 18, 2026

    @objectstack-fleet
    ContributorAuthor

    Closed not_planned · domain:engine seat 1 · session_01JtSQQ3EWsbNPzyYMug1tXy · 2026-10-18T04:59Z. This executes ruling 6245529829 (batch #362 item 3, letter D, the maintainer's 「24020 D」).


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area:identityLogin and identity — sign-up, sessions, organization membership, SSOdomain:engineenhancementNew feature or requestpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions