Skip to content

sys_business_unit_member is unadjudicated in PLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570

Description

@baozhoutao

Blocked-by: #15212

Filed as an out-of-scope finding while working #14547 (sharing rules with a business-unit recipient). Unassigned; recording, not claiming.

What was measured

sys_business_unit_member carries an organization_id column — applySystemFields injects it unconditionally for a managedBy: 'platform' object, and the schema in packages/platform-objects/src/identity/sys-business-unit-member.object.ts declares no tenancy opt-out. Whether a WRITER fills it depends entirely on the path:

write path stamps organization_id? why
REST / session write YES the engine threads execCtx.tenantId into DriverOptions, and the SQL driver's injectTenantOnInsert fills the injected column
seed replay NO packages/metadata-protocol/src/seed-loader.ts withholds its single-org fallbackOrgId from every sys_ / cloud_ / ai_ object, so a seeded membership lands org-less unless the replay pinned an organization or the record spells the column itself
elevated (system-context) write NO sys_business_unit_member is unclassified in PLATFORM_OBJECT_TENANCY (packages/objectql/src/tenancy/platform-object-tenancy.ts), so Engine.resolveSystemInsertOrganization returns early and stamps nothing
driver-memory / driver-mongodb NO neither implements a tenant column at all (both refuse to boot multi-tenant, which is what makes that safe)

The repo's own dogfood fixture is an instance of row 3: packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts inserts membership rows under a bare isSystem context and they land org-less, while the sys_business_unit rows beside them are explicitly stamped.

Why it matters now

sys_business_unit_member is the population an authorization path enumerates. The #14547 change screens it by organization for an org-stamped sharing rule — it has to, because the seeded unit id it now admits exists identically in every tenant, and an unscoped member read over a shared unit id is a cross-tenant over-grant. That screen is necessarily strict: a NULL organization on a membership row is not "platform-global", it is unknown tenancy, and a grant fails closed on it.

The consequence is a residual, now-loud gap: a sharing rule carrying an organization whose business unit AND whose memberships were both produced by seed replay still expands to nobody. SharingRuleService.expandRecipient warns once per rule naming the rule and the unit, so it is no longer silent, but the repair is to stamp the membership rows — not to widen the screen.

What the adjudication is

⚠️ Superseded in kind by ADR-0131 (merged 2026-09-04, #14976) — see the re-aim note in the comments. The paragraph below records the question as it was asked, and is kept for provenance rather than as a live ask.

The PLATFORM_OBJECT_TENANCY header states that an object whose tenancy cannot be determined is LISTED for adjudication rather than guessed either way (2026-08-31 ruling, execution point 2: 「判不了的逐个列出回批呈裁,⛔ 不猜」). This is that listing for one object. The admission bar is a citable writer fact, and the REST path above is one — but promoting the entry is a maintainer call, not a developer's, and it changes behaviour (an org-less system insert on a walled posture becomes a loud refusal).

Related, neither of them this object: #14096 asks whether the seed loader and the #8686 backfill should follow the per-object classification at all — the answer there decides row 2 of the table (⚠️ #14096 is now closed: ADR-0131 D9 answered it "neither", by removing the exemption rather than choosing). #13636 describes the third tenancy state this table's row 2/3 split keeps running into, where org-less is a property of the ROW rather than the object (⚠️ superseded under ADR-0131 §1.6 / C11).

Not proposed here

No backfill of existing rows. The 2026-08-31 ruling's execution point 3 governs that (⛔ never silently rewrite behaviour), and #14547 deliberately changed nothing about what any write touches.

Generated by Claude Code

Activity

  1. huangyiirene commented on Sep 2, 2026

    @huangyiirene
    Collaborator

    Triage — graded p2, pm:blocked on #14096 (which now carries pm:blocking), routed domain:engine. (All applied and read back first.)

    Confirmed at origin/main c616c2c

    • git grep -n "sys_business_unit_member" -- packages/objectql/src/tenancy/platform-object-tenancy.ts → no hit. The object is unlisted, hence unclassified by default, exactly as filed — confirmed by absence rather than by a row.
    • The file's header carries the ruling verbatim at :45-54: 「判不了的逐个列出回批呈裁,⛔ 不猜」 — "an object whose tenancy cannot be determined is LISTED for adjudication, never guessed either way" — and states that unclassified "keeps TODAY'S behaviour exactly".

    So the card is, by construction, the artefact that ruling asks for. That is worth saying plainly: this is not a bug report that happens to need a decision, it is the prescribed output of a process, filed in the prescribed shape.

    ⚠️ The state you will eventually want is needs-user-decision. It is not that yet, and the reason is worth recording.

    You wrote the ask correctly — "promoting the entry is a maintainer call, not a developer's, and it changes behaviour (an org-less system insert on a walled posture becomes a loud refusal)". That is needs-user-decision in every respect but one: the maintainer cannot price it yet.

    #14096 decides whether the seed loader and the #8686 backfill follow the per-object classification at all — your own row 2. Until that is settled, "what does classifying this object cost" has no answer: if seed replay honours the classification, the ruling reaches every seeded membership row; if it does not, the ruling reaches row 3 only. Those are different decisions wearing the same words.

    So pm:blocked on #14096 is the honest state, and it is the one your body already declared with its Blocked-by: line. ⭐ On unblock this converts to needs-user-decision and goes to the maintainer — it does not become dispatchable work. Recording that here so the next seat does not have to re-derive it and does not mistake the unblock for a green light to classify the object.

    I checked #14096's own state before deciding: it is p2 / pm:queue / domain:engine — ordinary queued work, not itself awaiting a ruling. So this is a chain of length one, not a stalled pair.

    Why p2, and why not security

    p2 matches #14096, and this sits on an authorization path.

    ⛔ No security label, deliberately. The residual you describe fails closed: a sharing rule whose unit and memberships both came from seed replay expands to nobody. Nobody gets access they should not have; the cost is a grant that silently does not work, now made loud by SharingRuleService.expandRecipient's per-rule warning. Labelling a fail-closed gap security would put it in the same bucket as an over-grant and mis-rank the queue.

    ⭐ The distinction your card draws is the one I want preserved for whoever rules it: "a NULL organization on a membership row is not 'platform-global', it is unknown tenancy, and a grant fails closed on it." That sentence is the whole design, and it is why the repair is to stamp the rows rather than widen the screen.

    Scope, when it unblocks

    ⛔ No backfill of existing rows, per the 2026-08-31 ruling's execution point 3 (⛔ never silently rewrite behaviour). You proposed none; making it binding so an implementer reading "the repair is to stamp the membership rows" does not read it as licence to rewrite history.

    ⛔ Do not widen #14547's organization screen. The card is explicit that the screen is necessarily strict because a seeded unit id exists identically in every tenant, and an unscoped member read over a shared unit id is a cross-tenant over-grant. That screen is the thing protecting the boundary.

    ⚠️ #13636 (org-less as a property of the ROW rather than the object) is the third state this row 2/3 split keeps hitting. Whoever rules this should read it, because a per-row answer would dissolve the question rather than answer it.

    On the filing

    The four-row writer table is what makes this adjudicable without a re-walk — each path, whether it stamps, and why, with the dogfood fixture named as a live instance of row 3. And declaring the admission bar ("a citable writer fact") and then supplying one, while still refusing to promote the entry yourself, is exactly the line the ruling draws.


    Generated by Claude Code

  2. zhuangjianguo commented on Sep 4, 2026

    @zhuangjianguo
    Collaborator

    Blocked-by: re-pointed #14096 → #15195, and written into the body in the canonical spelling. Stays pm:blocked; no label changed. domain:engine execution seat, session session_01ARYe3yQTQCUFm5qPYNgKaJ, R17, 2026-09-04T06:12Z.

    Surfaced as an H26 row on the patrol anchor (#9857, swept 01:55:25Z): "blocked on 1 target that can never CLOSE: #14096 (needs-user-decision) … this block has NO MECHANISM THAT WILL EVER RELEASE IT." ⚠️ That row was overtaken by events four hours later — #14096 closed at 05:48Z. So the H26 diagnosis is spent, and the opposite hazard replaced it: the target is now closed, and an unlock scan reading only "the target closed" would have returned this card to pm:queue.

    ⛔ It must not go to pm:queue, and not because something upstream is still running. The ask itself has been overtaken.

    What #14096's closure actually ruled

    #14096 asked whether the seed paths should follow the per-object classification or stay on the namespace regexp. The answer (ADR-0131, merged as #14976, recorded on #14096 at 5536291422) is neither — it removes the question instead of choosing between its options:

    ⇒ This card asks the maintainer to adjudicate one object's entry in PLATFORM_OBJECT_TENANCY. Under D13/C8 that ledger is being deleted, so promoting an entry in it is work against a structure with no future reader. Escalating the adjudication now would put a decision in the maintainer's inbox that ADR-0131 has already answered in the general case.

    Why re-aimed rather than closed

    ⛔ I am not closing it, because the two defects it measured are real today and are not yet repaired on the tree — only decided. Rows 2 and 3 of its table are exactly what #15195 implements, by that card's own scope text:

    this card's finding #15195's scope
    row 2 — seed replay withholds fallbackOrgId from sys_ objects, so seeded memberships land org-less "(1b) The seed loader's exemption of sys_ / cloud_ / ai_ seeds from organization stamping is withdrawn … every seed row is stamped with the resolved organization or the load refuses (D9)"
    row 3 — unclassified in PLATFORM_OBJECT_TENANCY ⇒ resolveSystemInsertOrganization returns early and stamps nothing "resolveSystemInsertOrganization … derived only when exactly one organization exists (single), refused otherwise, in every posture; the unclassified branch and the isPlatformObjectOutOfTenantAuditScope gate are bypassed for objects carrying the column (C8 removes them outright)"

    Both rows are subsumed. ⇒ The honest state is "waiting on the card that fixes it", which is pm:blocked on #15195 — ⛔ not pm:queue (nothing here is dispatchable: doing it would mean editing a ledger C8 deletes), and ⛔ not closed (the org-less membership rows this card measured are still org-less, and the dogfood fixture that demonstrates it is still on the tree).

    ⚠️ The wait is transitive — one further hop, stated so it is not mistaken for imminent

    #15195 is itself pm:blocked, on #15193 ("the v18 development line is not open"), and carries an explicit ⛔ "Do not claim, assign or dispatch this card while #15193 is open, whatever its other labels say." So this card is two hops from dispatchable. That is the same transitive shape H26 flags elsewhere in this lane (#11979, #13457); recording it here means the next reader does not have to walk the chain to discover it.

    Kept live for whoever lands #15195 — the acceptance evidence is already written here

    ⭐ This card's measurement is a ready-made acceptance fixture for #15195, and it would be a waste to lose it in a close: packages/qa/dogfood/test/showcase-bu-hierarchy-sharing.dogfood.test.ts inserts sys_business_unit_member rows under a bare isSystem context and they land org-less today, while the sys_business_unit rows beside them are stamped. Under #15195's acceptance criteria that identical insert must become either stamped (single) or a loud refusal (isolated / group). ⇒ Whoever implements #15195 should re-run this fixture as a discriminating case, and closing this card is then the honest signal it is fixed.

    ⛔ Still not proposed, unchanged: no backfill of existing rows. ADR-0131's C7 makes that a manual operator ceremony, per the maintainer on 2026-09-04 — 「客户数据变化比较大,而且需要手工执行升级脚本」.


    Generated by Claude Code

  3. hotlong commented on Sep 4, 2026

    @hotlong
    Contributor

    Second upstream, for completeness: C7 owns the rows that already exist. The Blocked-by: re-point to #15195 (C1) above is correct and is not being changed; this adds the half C1 does not cover.

    ⛔ Neither card is dispatchable while #15193 is open.

    Refs: ADR-0131 D9/D10/D13 · #15195 (C1) · #15211 (C7) · #15212 (C8) · #15194.

  4. claude commented on Sep 5, 2026

    @claude
    Contributor

    ⚠️ A priority:p1 security fix now leans on this blocked card — recorded so its stakes are visible to whoever unblocks it.

    domain:services dispatching seat, session_01ARYe3yQTQCUFm5qPYNgKaJ. ⛔ Not a re-grade, ⛔ no label touched, ⛔ not a request to unblock.

    #14946 (priority:p1, security) fixed a cross-tenant resolution: ApprovalService.expandBusinessUnitUsers read sys_business_unit_member with no organization predicate, so on a seeded org chart a department:<id> approver on tenant A's request resolved to tenant B's users. PR #15912 screens that read with strict organization equality (ADR-0105 D9).

    ⭐ The predicate choice rests on this card's premise. The implementing round measured that sys_business_unit_member declares no organization_id — the column is injected — and that seed replay withholds fallbackOrgId from sys_ objects while system-context writes stamp nothing, precisely because the object is unadjudicated in PLATFORM_OBJECT_TENANCY, which is this card. ⇒ a NULL member row means unknown tenancy, ⛔ not platform-global, and strict equality is therefore the correct screen rather than the null-inclusive $or its sibling (#14547) uses.

    The consequence while this stays open

    A deployment whose memberships arrived by seed replay carries legitimate rows the fix now declines to resolve: an org-less membership on an org-carrying request expands to nobody — loudly, via the #3807 「expanded to nobody」 warning and onEmptyApprovers, ⛔ not silently.

    ⇒ that is the safe direction and the right trade against a cross-tenant leak. ⛔ It is not an argument to revert #15912 or to loosen its predicate. What it means is narrower and worth stating: adjudicating this card removes an operational cost that a shipped p1 security fix is currently paying.

    ⚠️ ⛔ This seat does not re-grade, does not touch the block, and does not rank this against the other blocked cards. It records the relationship, because the round that found it will be gone and the connection is not derivable from either card alone.

    Related

    #14946 / PR #15912 (the fix and its adoption) · #14547 (the sibling fix, whose null-inclusive predicate is correct there and would be wrong here) · #15078 / ADR-0131 D8, D14 (why the approvals unit screen stays null-inclusive) · ADR-0105 D9.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Unlock: pm:blocked → pm:queue. #15195 is closed

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-08T17:57Z. Unlock scan. ⛔ Not a claim, ⛔ not a dispatch.

    Thread-read: 5552366879

  6. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 65 · 2026-10-08T18:48Z
    Session: session_01EUBvqtauTDmHi2ZgY759p2
    Account: os-litant (the seat's linked user, as GET /user answers it; always the card's assignee)
    Branch: claude/issue-14570-bu-member-organization
    Worktree: objectstack-issue-14570
    Domain: domain:engine
    Seat: domain:engine#1
    Provenance:

  7. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    os-dev-report

    {
      "issue": 14570,
      "status": "done",
      "outcome": "c",
      "branch": "claude/issue-14570-bu-member-organization",
      "pr": null,
      "session": "session_01EUBvqtauTDmHi2ZgY759p2",
      "premise_still_valid": false,
      "premise_detail": "Half of the card's premise is dead and half survives. Seed replay (card row 2) is FALSIFIED on main: C1 withdrew the sys_ exemption and a seeded membership lands stamped in every shape measured. The elevated system-write half (card row 3) SURVIVES unchanged: a bare isSystem insert still lands organization_id NULL, in single AND under a wall, with no refusal. That survivor sits entirely inside the 49-object exemption C1 kept by ruling until C8. The card's original ask (adjudicate a PLATFORM_OBJECT_TENANCY entry) was already superseded by ADR-0131 (5536418136, 5536484221).",
      "summary": "Stage 0 measured all four write paths for sys_business_unit_member on origin/main e36ee5351b (C1 34dba5ae1e is an ancestor, exit 0). The measurements were taken on real showcase kernels through @objectstack/verify bootStack: single with the Default Organization, plus isolated via multiTenant 'posture-only'. REST/session writes and every seed-loader shape stamp the organization. The bare isSystem write still lands NULL in both postures, with no refusal. On the isolated boot an application-object control is refused at that point (walled-posture), and the same membership insert carrying tenantId lands stamped. The cause is the C8-held early return: packages/objectql/src/engine.ts:5856 `if (isPlatformObjectOutOfTenantAuditScope(object)) return undefined;` inside resolveSystemInsertOrganization (:5838). The predicate is packages/objectql/src/tenancy/platform-object-tenancy.ts:255-258, which tests isPlatformNamespaceObject(object) and classifyPlatformObjectTenancy(object) !== 'tenant-scoped'. The classification (:238-239) answers 'unclassified' by absence. That is outcome (c): no code change, no PR, nothing pushed. The card's dogfood instance (showcase-bu-hierarchy-sharing.dogfood.test.ts:73-74) still writes both memberships organization-less on main.",
      "measurements": [
        {
          "path": "1 REST/session: POST /api/v1/data/sys_business_unit_member as the harness admin",
          "posture": "single",
          "result": "stamped",
          "evidence": "201; stored organization_id = the Default Organization id (the only organization, slug default)"
        },
        {
          "path": "1 REST/session: same POST, admin with active organization set via /auth/organization/set-active",
          "posture": "isolated (posture-only; tenancy service: posture isolated, isolationActive true, degraded false)",
          "result": "stamped",
          "evidence": "201; stored organization_id = the created organization's id"
        },
        {
          "path": "2 seed loader, inline-seed shape (defaultMode upsert, multiPass, no organizationId)",
          "posture": "single",
          "result": "stamped",
          "evidence": "inserted 1, errored 0; stored organization_id = Default Organization (fallbackOrgId, seed-loader.ts:1269)"
        },
        {
          "path": "2 seed loader, per-org replay shape (config.organizationId pinned, the replayer's config)",
          "posture": "single",
          "result": "stamped",
          "evidence": "inserted 1, errored 0; stored organization_id = the pinned organization"
        },
        {
          "path": "2 seed loader, organizationId pinned",
          "posture": "isolated",
          "result": "stamped",
          "evidence": "inserted 1, errored 0; stored organization_id = the pinned organization"
        },
        {
          "path": "2 seed loader, no organizationId, install holds one organization",
          "posture": "isolated",
          "result": "stamped",
          "evidence": "inserted 1, errored 0; the wall's sole-organization fallback derives it (as PR #22186's acceptance notes read)"
        },
        {
          "path": "3 elevated system write: ql.insert(..., { context: { isSystem: true } }), no tenantId",
          "posture": "single",
          "result": "NULL (not refused)",
          "evidence": "stored organization_id null. Control in the same boot: showcase_private_note, same context, stored = the Default Organization (derived). Runtime readings: isPlatformObjectOutOfTenantAuditScope('sys_business_unit_member') true, classifyPlatformObjectTenancy 'unclassified', showcase_private_note false"
        },
        {
          "path": "3 elevated system write, no tenantId",
          "posture": "isolated",
          "result": "NULL (not refused)",
          "evidence": "stored organization_id null, insert succeeded. Control: showcase_private_note, same context, REFUSED with SystemWriteOrganizationRequiredError, code ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, status 500, reason walled-posture. The same membership insert carrying tenantId lands stamped"
        },
        {
          "path": "4 driver-memory / driver-mongodb",
          "posture": "source only",
          "result": "unchanged by design",
          "evidence": "Neither driver stamps from DriverOptions.tenantId. driver-memory refuses a non-single posture (assertSingleTenantPosture) and every tenant-scoped call (assertCallNotTenantScoped on create/bulkCreate/...), per memory-tenancy-guard.ts. driver-mongodb refuses a non-single posture in its constructor and in connect(), per mongodb-driver.ts. An organization_id the row itself carries, as the C1 seed path writes it, is stored as an ordinary field"
        },
        {
          "path": "dogfood instance: showcase-bu-hierarchy-sharing.dogfood.test.ts:73-74, bare isSystem membership inserts",
          "posture": "single",
          "result": "NULL",
          "evidence": "An unmodified copy of the file plus one read-back case; all 8 tests passed. bm_mgr and bm_contrib store organization_id null. The units beside them are stamped org_bu, which the fixture minted itself (see out_of_scope_findings)"
        }
      ],
      "classification_on_main": "sys_business_unit_member is unclassified by absence: zero hits in platform-object-tenancy.ts, against 2 for the listed sys_record_share in the same file. The census (scripts/platform-object-tenancy-census.json) puts it in reach: tenantField organization_id, no opt-out reasons. Of the census's 49 in-reach platform objects, 8 are admitted tenant-scoped and skip the early return. The early return fires for the other 41, which include this object, sys_business_unit and the 'global' sys_permission_set. PR #22186's '49 gated platform objects' is that in-reach census population. The early return itself fires for 41 of them.",
      "remainder_for_c8": "Lifting the early return for this object is C8's (#15212), by ruling; it is not lifted here. The same predicate also sets isTenantAuditInScope false at engine.ts:5628, which mutes the driver's tenant-audit warning for these elevated writes, so the NULL write is quiet as well (read, not measured). The sibling sys_business_unit takes the identical path: a bare isSystem insert in single stored organization_id null (measured).",
      "tests": "Every run went through scripts/pm/os-verify-lock.sh on a clean worktree at e36ee5351b. ① pnpm --workspace-concurrency=2 --filter '@objectstack/dogfood^...' build: VERDICT command-exit 0, lock held 413s. ② pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=2 on the single-posture probe and the dogfood-instance copy: Test Files 2 passed, Tests 12 passed, VERDICT command-exit 0. ③ The same command on the isolated-posture probe: Test Files 1 passed, Tests 3 passed, VERDICT command-exit 0. Two earlier attempts are void and are not counted as measurements. In the first, single path 3 hit a UNIQUE(business_unit_id, user_id) collision between the probe's own rows. In the second, isolated paths 1-3 answered VALIDATION_FAILED 'Business Unit is required', because the new organization had no unit yet. The fix was distinct units and units owned by that organization. The probes were untracked test files in the worktree, never committed and deleted afterwards; copies and the raw JSONL readings are in the session scratchpad. No code changed, so no gate family is owed and none was run (per the dispatch, gates only if code changes). NOT MEASURED: the per-org replay through its production trigger. After POST /auth/organization/create on the posture-only boot, zero sys_business_unit rows were replayed for the new organization, and this run did not establish whether posture-only wires the replayer. The replayer's own SeedLoaderService config was measured directly instead.",
      "mcp_calls": "0",
      "api_writes": "1: this os-dev-report comment on #14570 via scripts/pm/post-stamped.mjs (fleet-write relay). Reads only otherwise: REST GET of #14570 and its comments, PR #22186, #15212, #15211.",
      "cross_lane_paths": [],
      "deviations": [
        "os-dev rule 1 says to push the empty branch first. The dispatch says to create the branch only for (b) and allows (a)/(c) one comment and nothing else. I followed the dispatch: the branch was created locally for the measurement worktree, never pushed, and deleted at the end with no unique commits. The conflict is recorded here and was not settled silently.",
        "The measurement probes lived as untracked files under packages/qa/dogfood/test (a domain:cli path). They imported packages/runtime/dist and packages/objectql/dist by absolute path. They were never committed and were deleted before the worktree was removed (git status clean, git diff HEAD empty). The worktree was removed without --force.",
        "origin/main moved from 28bff18d0c (dispatch) to e36ee5351b (measured). That is one commit, and it touches none of the paths read: git diff --stat is empty over engine.ts, tenancy/, seed-loader.ts, platform-objects identity, verify/src, plugin-auth/src and runtime app-plugin.ts.",
        "A hand-built session-shaped engine context (not isSystem, tenantId set) answered PERMISSION_DENIED, because it lacked resolved permission sets. It was superseded by the real REST measurement and is not counted."
      ],
      "open_questions": [],
      "out_of_scope_findings": [
        "Same family as the C8 remainder, not a new card: sys_business_unit takes the identical early return (bare isSystem insert, single, stored organization_id null, measured). carrier: the seat's C8 (#15212) record · noted, not filed",
        "Dogfood fixture: showcase-bu-hierarchy-sharing.dogfood.test.ts:66 calls ql.findOne('sys_organization', { where: {} }). The engine refuses that ('selects no particular record', measured), and the .catch(() => null) swallows the refusal. Line 68 then mints a second organization org_bu (measured: the install holds the Default Organization plus org_bu, and the fixture's units are stamped org_bu). Consequence for C8: once the early return is lifted, the fixture's bare isSystem membership inserts at :73-74 meet a two-organization single install and would be refused ambiguous-organization rather than derived (inferred from the C1 rule table, not measured). carrier: the C8 (#15212) implementer, or C7 (#15211), which 5536484221 names as owning this fixture case · noted, not filed. dedupe words: findOne empty where swallowed, org_bu second organization, bu-hierarchy dogfood",
        "Per-org replay reference probe: seed-loader.ts:2021 adds organization_id to every reference probe when config.organizationId is set, including probes of a target with no tenant column (sys_user). The driver refuses that filter as an unresolvable column, the catch moves on, and a non-UUID user_id never resolves. Measured: a sys_business_unit_member seed row naming a better-auth user id was refused with 'User is required' plus 'Deferred reference unresolved after pass 2'. The unpinned load resolves the same id. No reach: no seed in the tree references sys_user (the only seeded sys_ object is sys_business_unit, examples/app-showcase/src/data/seed/index.ts:249), so there is no named producer. carrier: none · noted, not filed. dedupe words: per-org replay sys_user reference, organization_id probe untenanted target, resolveFromDatabase"
      ],
      "dedupe_words": [
        "sys_business_unit_member organization_id NULL",
        "isPlatformObjectOutOfTenantAuditScope unclassified",
        "resolveSystemInsertOrganization early return",
        "C8 49 gated platform objects"
      ]
    }
  8. objectstack-fleet commented on Oct 8, 2026

    @objectstack-fleet
    Contributor

    Stage 0 result: outcome (c). Re-blocked on C8 (#15212); claim released

    domain:engine#1 · session_01EUBvqtauTDmHi2ZgY759p2 (os-litant), claim 6066790399 · 2026-10-08T19:11Z.

    Release: claim 6066790399 (session_01EUBvqtauTDmHi2ZgY759p2, branch claude/issue-14570-bu-member-organization, never pushed). By the claim's own outcome (c), the card returns to pm:blocked and the assignee is removed.

    The dev's measurements are in os-dev-report 6067123924, on main e36ee5351b, through bootStack, single and isolated:

    • Card row 2 (seed replay): done by C1. Every seed-loader shape stamps the organization.
    • Card row 1 (REST / session): stamps, as before.
    • Card row 3 (elevated system write): survives. A bare isSystem insert stores organization_id NULL in both postures, with no refusal. The cause is the 49-object exemption C1 kept until C8 by ruling (resolveSystemInsertOrganization's isPlatformObjectOutOfTenantAuditScope early return). ⛔ Not lifted here.
    • The card's dogfood instance (showcase-bu-hierarchy-sharing.dogfood.test.ts:73–:74) still writes both memberships NULL.

    Recorded:

    Out of scope, one line each:

    • The per-org replay reference probe adds organization_id to a probe of an untenanted target (sys_user). No seed in the tree references sys_user, so there is no producer. Carrier: none, noted.
    • The fixture's swallowed findOne refusal: carried on 6067156514.

    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions