Skip to content

revert(plugin-sharing): drop the NULL-inclusive business-unit screen before 17.3; keep the strict member screen - #15078

Merged
os-sales merged 4 commits into
mainfrom
claude/issue-15030-revert-null-inclusive-unit-screen
Sep 3, 2026
Merged

revert(plugin-sharing): drop the NULL-inclusive business-unit screen before 17.3; keep the strict member screen#15078
os-sales merged 4 commits into
mainfrom
claude/issue-15030-revert-null-inclusive-unit-screen

Conversation

@claude

@claude claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Fixes #15030

Maintainer-ruled partial revert of unreleased code (the v18 org-ownership decision (PR #14976), D8). priority:p1 / target:v17 — must land before the 17.3 tag.

Clause-②: no

Re-derived from this diff, not inherited: orgScope is a private method, the diff adds no exported symbol, no payload key, no spec surface and no new accepted shape. It narrows an accept set back to exactly what @objectstack/account@17.2.0 already ships.

What was reverted

BusinessUnitGraphService.orgScope in packages/plugins/plugin-sharing/src/business-unit-graph.ts is restored to the strict organization_id = RULE_ORG equality, and the docblock arguing for the NULL-inclusive unit screen is deleted.

The restored method body is byte-identical to its form before e560b4d51 (PR #14949) — verified rather than retyped:

diff <(git show e560b4d51^:...business-unit-graph.ts | sed -n '/private orgScope/,/^  }$/p') \
     <(sed -n '/private orgScope/,/^  }$/p' ...business-unit-graph.ts)
  => IDENTICAL to pre-e560b4d51

⚠️ Falsifier checked, did not fire. The card spells the pre-e560b4d51 form as { ...filter, organization_id: this.organizationId }. The actual parent commit spells the guard the other way round (if (this.organizationId) return {…}; return filter;) while returning exactly that expression. It was plain strict equality, so the revert is a true revert; this PR restores the parent's spelling, not the card's paraphrase.

What was deliberately KEPT

Why memberScope is still load-bearing with the unit screen strict: a strict unit screen narrows which units are reachable, but other organizations' member rows sit on org-stamped (visible) units too. memberScope is the only thing that answers there.

The two grep counts, reported separately

git grep -n 'organization_id: null' -- packages/plugins/plugin-sharing/src

population count
all hits 38
test-file hits (*.test.ts) 34
non-test hits 4
non-test hits in business-unit-graph.ts 0

⚠️ The card's acceptance ("returns no non-test hit") is not literally reachable, and this is falsifier #3 firing. The four remaining non-test hits are all in sharing-rule-service.ts:

sharing-rule-service.ts:268   * longer mint an `organization_id: null` rule (a platform-global one, whose
sharing-rule-service.ts:515   * stamps `organization_id: null`) before any org id exists. A strict
sharing-rule-service.ts:556   return { ...where, $or: [{ organization_id: orgId }, { organization_id: null }] };
sharing-rule-service.ts:602   return (await first({ name, organization_id: orgId })) ?? (await first({ name, organization_id: null }));

They are not mine to revert, proved by measurement rather than by blame (the checkout was shallow, so blame attributed them to a boundary commit):

:556 is SharingRuleService.adminOrgScope (#7676), which the deleted docblock itself cited as prior art. It ships in 17.2.0, so it is outside "the screen added after 17.2.0" that this card retires. I did not touch it, and I did not touch the test fixtures either. The acceptance is met in the sense that matters — zero non-test hits in the reverted file.

Ancestry check, with its positive control

Both legs run and quoted; the control is what makes "not an ancestor" a reading rather than an artifact of a truncated graph.

leg commit git merge-base --is-ancestor X @objectstack/account@17.2.0 reading
the revert 2f24c89058 exit 1 NOT an ancestor of the 17.2.0 tag
positive control 98ea3443ff exit 0 IS an ancestor — traversal works

Tag commit e7d2cc67fdef7fee9d2c6d65d7363fe1c78ce6a4. Also confirmed: e560b4d51 (the code being reverted) is not an ancestor of the tag either, i.e. it really was added after 17.2.0.

⚠️ The clone was shallow (383 commits) and merge-base silently answered "not an ancestor" for the tag itself. I deepened to 7191 commits before taking any ancestry reading; the control leg above is what proves the deepened graph actually traverses.

Tests

Baseline before any edit: 32 files / 798 tests green. Final: 32 files / 801 tests green (pnpm --filter @objectstack/plugin-sharing test).

⚠️ Falsifier #1 FIRED — the memberScope pins could not be left untouched

The card's constraint 2 ("keep every test that pins memberScope… not touched at all") is not satisfiable as written, and I am flagging it rather than quietly working around it.

With only the source revert applied and no test file edited, 13 tests reddened. Five of them are memberScope pins:

× WIDE — a subtree expansion never crosses into another organization      (business-unit-graph)
× NARROW — the single-unit expansion does not cross either                (business-unit-graph)
× WIDE — no sys_record_share row is ever materialised for another org's member  (recipient-width)
× NARROW — the single-unit width does not cross either                    (recipient-width)
× an org-LESS membership row is not admitted to an org-stamped rule       (recipient-width)

Mechanism: both expandUnitMembers and expandUsers gate on seedIsUsable(), which reads through orgScope. Those five pins were written on a seeded (org-less) unit — a fixture only #14949's widening made reachable. With the unit screen strict again, the unit screen answers first and the member read never runs.

This is a fixture dependency, not a semantic inseparability. The two halves are separable, and here is the evidence: the pre-existing pin an org-scoped rule never reaches another org's MEMBER of a unit it can see — which anchors on an org-stamped unit — stayed green through the bare revert. memberScope is still strict, still called from both reads, still enforced.

Resolution: those five pins are re-anchored from a seeded unit onto an org-stamped one. Same assertions, same security property, a route that exists in 17.2.0. This is strictly stronger than leaving them: on a seeded unit they would now pass with memberScope deleted, because the unit screen would answer first — they would have become phantom pins. (One of them, an org-LESS membership row is NOT a member of an org-scoped rule in business-unit-graph.test.ts, was already phantom under the bare revert: it stayed green while measuring nothing.)

⛔ I did not skip, disable or quarantine anything. Test count went 798 → 801.

Tests rewritten to pin the restored behaviour

Per scope item 3, the cases that pinned the NULL-inclusive unit screen now pin the 17.2.0 behaviour and name #14547 as the defect they reproduce, with an explicit note that they assert a reproduction, not a fix, and that the v18 org-ownership decision (PR #14976), C1 is where it gets fixed. Added controls so "strict" cannot be confused with "refuses everything": an org-stamped unit IS visible — the screen is strict, not broken, and the SAME rule against an org-stamped tree grants normally.

Ablation — direction predicted in writing before running

Predicted: restoring the NULL arm reddens exactly 7 — the 5 unit-screen pins plus the 2 rewritten recipient-width defect cases — and the memberScope pins do not move, because they are now anchored on org-stamped units.

Measured: exactly those 7.

× the narrow width is org-predicated exactly like the wide one
× [#14547] NEITHER width reaches a SEEDED unit tree, even with stamped members
× an org-scoped rule does NOT see an env-wide (null-org) seeded unit
× the seed check and the subtree walk BOTH refuse the seeded rows
× `headOf` does not resolve the manager of a seeded unit either
× WIDE — `unit_and_subordinates` still materialises NOTHING, and says so
× NARROW — `business_unit` materialises nothing either
                                       Tests  7 failed | 44 passed (51)

The memberScope pins stayed green under the mutation — which is the point of the re-anchoring: the two screens are now pinned independently of each other.

Mutation proved on disk by literal-text counts before reading any result: strict=0 (want 0), nullarm=1 (want 1), and blob cfef2985… differing from HEAD blob 5402c265…. No rebuild was needed — both test files import ./business-unit-graph.js, a relative same-package specifier resolved from source, so no dist/ sits between the mutation and the assertion.

Restore proved three ways (not by an exit code): blob hash back to 5402c265fef80051fab31988da81aa1494e1b7dd and equal to git rev-parse HEAD:PATH; git diff HEAD 0 bytes; literal counts back to strict=1 / nullarm=0. The script carried trap restore EXIT INT TERM with an absolute REPO_ROOT path, and git checkout HEAD -- PATH (never a bare git checkout -- PATH, which restores from a possibly-mutated index).

Gates

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived after the second commit (identical family — same 4 paths vs merge base 8a96e666b), and the union re-run on the final head 2f24c89058. Every exit code captured by redirect-then-read, never across a pipe.

41 of 42 green (exit 0). 1 NOT MEASURED.

Plus pnpm --filter @objectstack/plugin-sharing typecheck green — including check:test-typecheck, which compiles the test layer under tsconfig.test.json, so the two rewritten test files were genuinely measured, not silently excluded:

check:test-typecheck: OK — @objectstack/plugin-sharing's test layer compiles under packages/plugins/plugin-sharing/tsconfig.test.json; 2 file(s) / 3 error(s) / 3 pinned signature(s) held in test-typecheck-debt.json

Three gates first exited 3, and I built the closure so they would actually measure

All three initially reported PREREQUISITE NOT MET because no dist/ existed. Rather than record them as NOT MEASURED, I ran the closure build CI runs before this step (pnpm exec turbo run build --filter='./packages/*' --filter='./packages/*/*', 9m05s, exit 0) and re-ran them. All three are green:

  • pnpm check:type-check-debt — exit 0. This is the one worth calling out: a sibling card was reddened in CI today by exactly this gate after its author honestly recorded exit 3 locally. Its verdict here:

    check-type-check-coverage --re-measure: OK — 17 ledger entr(ies) re-measured in 309.6s, 217 raw tsc error(s) total, none above its recorded number.
    surplus: none — every entry sits exactly at its measurement, so any new error is red.

  • pnpm check:i18n — exit 0. check-i18n-bundles: OK (9 package(s) — all bundles in sync, no undeclared authoring keys).
  • pnpm check:dual-build-cjs-loads — exit 0. provenance — entries/packages/cjsFiles/probes: this run 102/66/610/1 · floors 90/58/520/1.

NOT MEASURED — neither green nor red, quoting the gate's own verdict

  • node scripts/check-test-completeness.mjs — exit 3. It needs a saved turbo run test log, which a local family run does not produce; the gate itself prescribes this record:

    "Fix: pass a saved turbo run test log — or, running the family locally, record this gate as NOT MEASURED. Nothing was measured: this gate exited before parsing a single summary line, so this result says NOTHING about whether every test vitest counted actually ran… ⛔ It is NOT a finding."

    I cannot move this one from here — it is a property of how the suite is invoked, not of the tree. CI runs it with the log it needs.

One footnote on invocation, not on the tree: the bare node scripts/pm/check-half-states.mjs hit my own 300 s timeout (exit 124) — it re-execs for the proxy and is network-bound. That is not a gate verdict. Its packaged form pnpm check:pm-half-states ran green (exit 0), so the family is measured.

⚠️ One reading was thrown away rather than reported: an early test run used --reporter=basic, which does not exist in vitest 4. It exited 1 in 1 s having run nothing. That is the bad-invocation class, not a red suite, so it is recorded as NOT MEASURED and was re-run with the default reporter.

Deviation from the card's verbatim wording — disclosed, not smoothed over

The card's changeset wording (and my first three commits) cite ADR-0131. That ADR does not exist on main: docs/adr/ stops at 0130 (control: 134 files, so the directory read works and the zero is a reading), and PR #14976, which would author it, is open and draft. scripts/check-adr-anchors.mjs reddened Lint & Repo Gates on head 2676f0f74, correctly — it treats an unresolvable ADR id as a citation squat: whoever later writes a real ADR-0131 would retroactively falsify all of those citations at once (#6634, where one number accumulated 77).

There is no legitimate forward-reference form. The gate's only citation exemption is CROSS_REPO_QUALIFIERS = {objectui, object-ui, cloud} — a sibling repo qualifier, not a PR — so ADR-0131 (PR #14976) would still be caught, and the allowlist is shrink-only and documented to stay empty.

⇒ All 22 citations now read "the v18 org-ownership decision (PR #14976)", keeping the decision letters (D8 / C1) that carry the actual meaning. Substance unchanged; the ADR id can be restored once #14976 lands.

⛔ Deliberately not done: no allowlist entry, and no docs/adr/0131-*.md created here — that file is PR #14976's and docs/adr/** is a governed surface.

Issue/PR references (#14547, #14949, #10103, #13564) are untouched — they resolve. check-adr-anchors is now green:

check-adr-anchors: OK (53 anchored file(s), every governing ADR still referenced; 128 decision number(s) …; 31871 citation(s) across 4202 file(s) resolve; 965 decision-letter citation(s) on the anchored surface name a decision the record makes).

Its two residual notes (ADR-0024 D4/D5.2 in plugin-auth, the allowlisted shared ADR-0057 number in objectql / plugin-security) are pre-existing and in files this PR never touches.

⚠️ One thing I did beyond the card's file list, and why

.changeset/bu-tenant-screen-relanding.md#14949's own changeset — is corrected, not reverted.

It is still on main, unreleased and unconsumed, and it announces as shipped fact the very half this PR reverts: "orgScope now applies the platform's null-inclusive screen", under the headline "Fix: a sharing rule with a business-unit recipient granted nothing when the unit came from seed data". Both changesets compile into the same 17.3 release notes, so leaving it would ship two contradictory patch entries for one package and tell users a tenancy fix landed that does not.

Corrected surgically: the headline and the orgScope paragraph now say the unit half was reverted and the seed-data symptom remains as in 17.2.0. The member-screen and warn paragraphs are kept — those do ship — with one sentence rewritten, since the leak they close does not depend on the unit screen having been widened.

This is a judgement call and I am flagging it for ratification rather than burying it. It touches no code from #14949.

Over-revert check — this is not git revert e560b4d51

#14949 touched 7 files; this PR touches 3 of them plus its own changeset. Measured:

file in #14949 this PR
business-unit-graph.ts reverted (orgScope only)
business-unit-graph.test.ts pins rewritten
recipient-width.test.ts pins rewritten
.changeset/bu-tenant-screen-relanding.md corrected (see above)
sharing-rule-service.ts untouched
sharing-rule.test.ts untouched
content/docs/permissions/system-context.mdx untouched

Coupling checked before finishing, as asked: sharing-rule-service.ts never references orgScope — it consumes the graph service only through the public expandUnitMembers / expandUsers, whose contract already includes "may expand to nobody" (that is exactly what warnOnEmptyUnitExpansion exists for; its own docblock says it "covers the case the screens deliberately leave empty"). So there is no dependency on NULL-inclusiveness to break, and none was found. Empirically confirmed: sharing-rule.test.ts is among the 32 files in the green suite.

Docs and the census gate

No doc prose to revert — confirmed independently of the drift bot (which reports nothing while warning that is not a clean bill of health): no hand-written page describes the NULL-inclusive unit screen, and this PR touches no file under content/docs/.

check:system-context-census did NOT redden. The predicted line rot did not materialise — measured green on every head, including the final one:

check-system-context-census: OK — 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read.

So neither remediation path was taken, because neither was needed: no --fix (there was no line rot to repair) and no conflict resolution (git status clean, not in MERGE state — verified). No hand-written prose was dropped, because no prose file was touched at all.

What I declined to do, and why

  1. I did not touch the four remaining non-test organization_id: null hits in sharing-rule-service.ts, even though the card's acceptance grep reads as if they should be zero. They ship in 17.2.0 and fix(plugin-sharing): a seeded business unit is a usable sharing-rule recipient, and its members are tenant-screened #14949 did not author them — that is falsifier Implement ObjectStack protocol specification with Zod schemas and TypeScript interfaces #3, and the instruction is to report it, not to work around it by deleting someone else's code.
  2. I did not touch any test fixture's organization_id: null outside the two files in scope (backfill-sys-record-share-organizations.test.ts, bu-tree-recompute.test.ts, sharing-rule.test.ts, sharing-service.test.ts, record-share-organization-stamp.test.ts, rule-criteria-org-scope.test.ts are untouched).
  3. I did not reopen Sharing rules with a business-unit recipient silently grant nothing when the unit row has organization_id = NULL #14547 — the card forbids it. ⚠️ But flagging it for the maintainer: Sharing rules with a business-unit recipient silently grant nothing when the unit row has organization_id = NULL #14547 is closed / state_reason: completed, closed by fix(plugin-sharing): a seeded business unit is a usable sharing-rule recipient, and its members are tenant-screened #14949, whose unit half this PR reverts. Its tracker state therefore no longer matches the 17.x runtime, where the symptom stands. The tests and the source docblock say so plainly rather than asserting a status; whether the issue is reopened is a maintainer's call, not mine.
  4. I did not add a NULL arm anywhere, did not touch SqlDriver.applyTenantScope, packages/spec, or content/docs/releases/**, and did not skip, disable or quarantine any test.
  5. I did not wait for CI — reporting at draft-PR time per the dispatch contract.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…before 17.3

Restore `BusinessUnitGraphService.orgScope` to the strict
`organization_id = <rule org>` equality 17.2.0 ships, byte-identical to its
form before e560b4d (#14949), and delete the docblock that argued for the
NULL-inclusive shape. Maintainer ruling, ADR-0131 D8: that shape re-implements
the predicate `SqlDriver.applyTenantScope` already owns, a second time in a
second place, which is the duplication ADR-0131 exists to retire (#10103
cause 1) — and it had not shipped, so reverting costs nothing while releasing
it would owe v18 a breaking change and a migration.

KEPT, untouched: `memberScope` (byte-identical to origin/main) and
`SharingRuleService.warnOnEmptyUnitExpansion`. Both member reads previously
carried no organization predicate at all; a strict unit screen narrows which
units are reachable but does not close that, because other organizations'
member rows sit on org-stamped units too.

#14547 therefore remains open and behaves as in 17.2.0. It is fixed
structurally on the v18 line by ADR-0131 C1.

Tests: the cases that pinned the NULL-inclusive unit screen now pin the
restored behaviour and name #14547 as the open defect they reproduce. The
member-screen pins are re-anchored from a seeded (org-less) unit onto an
org-stamped one — with the unit screen strict, an org-less unit is invisible to
an org-stamped rule, so those assertions would otherwise pass with
`memberScope` deleted. See the PR body.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
… reverted screens

#14547 is CLOSED as completed — closed by #14949, whose unit half this branch
reverts — so calling it "the OPEN defect" in the pins would have been wrong on
a checkable fact. The tests name #14547 as the defect they reproduce and say
plainly that its tracker state no longer matches the 17.x runtime, leaving the
reopen decision to the maintainer rather than asserting it.

Comment-only; no assertion, fixture or method body moves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions github-actions Bot added the size/m label Sep 3, 2026
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to listnot a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 8 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json d261cefaacf16024419af95fb64fa72fe2d2e864packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9b6a01ef483e6c474d215c9b3c41dcff0069a2c0 — the merge of head 2f24c89058203d23d91d71a35982517214df6245 into base d261cefaacf16024419af95fb64fa72fe2d2e864, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9b6a01ef483e6c474d215c9b3c41dcff0069a2c0 && git checkout 9b6a01ef483e6c474d215c9b3c41dcff0069a2c0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin d261cefaacf16024419af95fb64fa72fe2d2e864 2f24c89058203d23d91d71a35982517214df6245 && git checkout -B drift-repro d261cefaacf16024419af95fb64fa72fe2d2e864 && git merge --no-ff 2f24c89058203d23d91d71a35982517214df6245

node scripts/docs-audit/affected-docs.mjs --json d261cefaacf16024419af95fb64fa72fe2d2e864

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…hat 17.3 ships

`.changeset/bu-tenant-screen-relanding.md` is still on main and unconsumed, and
it announces as shipped fact the half this branch reverts: "orgScope now applies
the platform's null-inclusive screen", under a headline claiming the seed-data
under-grant is fixed. Both changesets compile into the same 17.3 notes, so
leaving it would ship two contradictory patch entries for one package and tell
users a tenancy fix landed that did not.

Corrected surgically: the headline and the orgScope paragraph now state that the
unit half was reverted and that the seed-data symptom remains as in 17.2.0,
fixed structurally in v18 by ADR-0131 C1. The member-screen and warn paragraphs
are kept — those DO ship — with one sentence rewritten, since the leak they
close does not depend on the unit screen having been widened.

⛔ No code from #14949 outside this card's scope is touched: sharing-rule-service.ts
and sharing-rule.test.ts are untouched, and this is not `git revert e560b4d`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…1 anchor

`check-adr-anchors` is red on the previous head: this branch cited `ADR-0131`
22 times and `docs/adr/0131-*.md` does not exist — ADR files stop at 0130 and
PR #14976 (which would author it) is open and draft. The gate treats an
unresolvable ADR id as a citation squat: whoever later writes a real ADR-0131
would retroactively falsify every one of those citations at once (#6634).

No legitimate forward-reference form exists. The gate's only citation exemption
is CROSS_REPO_QUALIFIERS (objectui / object-ui / cloud), so a repo qualifier is
skipped but `ADR-0131 (PR #14976)` is not — the qualifier must be a sibling
repo, not a PR. Its allowlist is shrink-only and documented to stay empty.

⛔ Deliberately NOT done: no allowlist entry, and no `docs/adr/0131-*.md`
created here — that file belongs to PR #14976 and `docs/adr/**` is governed.

Every citation now reads "the v18 org-ownership decision (PR #14976)", keeping
the decision letters (D8 / C1) that carry the actual meaning. Issue and PR
references (#14547, #14949, #10103, #13564) are untouched — they resolve.
Substance is unchanged; the ADR id can be restored once #14976 lands.

check-adr-anchors now exits 0: "OK (53 anchored file(s) … 31871 citation(s)
across 4202 file(s) resolve)".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actions github-actions Bot added size/l and removed size/m labels Sep 3, 2026
@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

os-dev-report

{
  "issue": 15030,
  "status": "done",
  "branch": "claude/issue-15030-revert-null-inclusive-unit-screen",
  "pr": "https://github.com/objectstack-ai/objectstack/pull/15078",
  "premise_still_valid": true,
  "summary": "All three seat-measured facts confirmed on origin/main. orgScope restored to strict equality BYTE-IDENTICAL to its form at e560b4d51^ (verified by diffing the method body, not retyped); the docblock arguing for the NULL-inclusive screen deleted; memberScope's method body byte-identical to origin/main; warnOnEmptyUnitExpansion kept. NOT a git revert of e560b4d51: that PR touched 7 files, this PR touches 3 of them plus changesets -- sharing-rule-service.ts, sharing-rule.test.ts and content/docs/permissions/system-context.mdx are UNTOUCHED. Coupling checked as the relay asked: sharing-rule-service.ts NEVER references orgScope, consuming the graph service only via public expandUnitMembers/expandUsers whose contract already admits an empty expansion (warnOnEmptyUnitExpansion exists precisely for it), so no dependency on NULL-inclusiveness exists to break; sharing-rule.test.ts is green in the 32-file suite. #14547 commented without reopening; its state and labels re-read after: unchanged. *** THREE THINGS THE PM MUST RULE ON, none of them smoothed over. *** (1) FALSIFIER #1 FIRED and I did NOT stop. Restoring strict equality reddens FIVE memberScope pins, so the card's 'single most important constraint' is not satisfiable as written: both expansion methods gate on seedIsUsable(), which reads through orgScope, and those pins were written on a SEEDED (org-less) unit -- a fixture only #14949's widening made reachable. I judged the falsifier's substance ('the halves are not separable') FALSE on evidence: the pre-existing pin anchored on an ORG-STAMPED unit stayed GREEN through the bare revert, so memberScope is still strict, still called from both reads, still enforced. I re-anchored the five onto an org-stamped unit -- same assertions, same security property, a route 17.2.0 ships -- which is STRICTLY STRONGER, because on a seeded unit they would now pass WITH memberScope DELETED (one was already phantom under the bare revert, green while measuring nothing). (2) I EDITED #14949's OWN CHANGESET, beyond the card's file list. .changeset/bu-tenant-screen-relanding.md is still on main, unreleased and unconsumed, and announces as shipped fact the half this PR reverts ('orgScope now applies the platform's null-inclusive screen'). Both changesets compile into the SAME 17.3 notes, so leaving it ships two contradictory patch entries for one package and tells users a tenancy fix landed that does not. Corrected surgically; the member-screen and warn paragraphs kept; NO #14949 code touched. (3) DEVIATION FROM THE CARD'S VERBATIM CHANGESET WORDING, forced by a gate: see open_questions. Falsifier #2 did NOT fire (pre-e560b4d51 orgScope WAS plain strict equality, though the card paraphrases its guard inverted). Falsifier #4 did NOT fire. Falsifier #3 DID fire and I respected it -- see out_of_scope_findings.",
  "tests": "Final head 2f24c89058. Every exit code captured by redirect-then-read, never across a pipe. BASELINE before any edit: 'Test Files 32 passed (32) / Tests 798 passed (798)'. FINAL on 2f24c89058: 'Test Files 32 passed (32) / Tests 801 passed (801)', lock VERDICT command-exit 0. Nothing skipped, disabled or quarantined. FALSIFIER MEASUREMENT (source revert only, no test file edited): 'Tests 13 failed | 35 passed (48)' -- 8 unit-screen pins plus 5 memberScope pins (WIDE/NARROW cross-tenant in both files, and the org-less membership row case). TYPECHECK exit 0, including check:test-typecheck, whose verdict proves the EDITED TEST FILES were measured rather than tsconfig-excluded: \"check:test-typecheck: OK -- @objectstack/plugin-sharing's test layer compiles under packages/plugins/plugin-sharing/tsconfig.test.json; 2 file(s) / 3 error(s) / 3 pinned signature(s) held\". ANCESTRY, both legs: git merge-base --is-ancestor 31430d3b23 @objectstack/account@17.2.0 -> exit 1 (NOT an ancestor); POSITIVE CONTROL 98ea3443ff vs the same tag -> exit 0 (IS an ancestor, traversal works). Tag commit e7d2cc67fdef7fee9d2c6d65d7363fe1c78ce6a4. WORTH PROPAGATING: the clone was SHALLOW (383 commits) and merge-base silently answered 'not an ancestor' for the TAG ITSELF -- a control-free reading would have been a false green. Deepened to 7191 commits before any ancestry reading was taken. ABLATION, direction predicted IN WRITING first: restoring the NULL arm reddens exactly 7 (5 unit-screen pins + 2 rewritten recipient-width defect cases) and the memberScope pins do NOT move. MEASURED exactly those 7: 'Tests 7 failed | 44 passed (51)'. Prediction matched including its negative half -- which is the point of the re-anchoring: the two screens are now pinned independently. NO REBUILD NEEDED, stated rather than assumed: both test files import './business-unit-graph.js', a relative same-package specifier resolved from source, so no dist/ sits between mutation and assertion. MUTATION CONFIRMED ON DISK by literal-text counts before any result was read: strict=0 (want 0), nullarm=1 (want 1), blob cfef2985... differing from HEAD blob 5402c265... RESTORE PROVEN three ways, never by exit code: blob hash back to 5402c265fef80051fab31988da81aa1494e1b7dd and equal to git rev-parse HEAD:PATH; git diff HEAD = 0 bytes; literal counts back to strict=1/nullarm=0. trap restore EXIT INT TERM with an absolute REPO_ROOT, and 'git checkout HEAD -- PATH', never a bare 'git checkout -- PATH'. GREP ACCEPTANCE, counts separated: all 38; test-file 34; NON-TEST 4; non-test in the reverted file business-unit-graph.ts: 0. GATES: derived with dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived after every commit that moved the file set (family IDENTICAL throughout, 5 paths vs merge base 8a96e666b). 41 of 42 GREEN. THREE gates first exited 3 (PREREQUISITE NOT MET, no dist/) and I did NOT leave them NOT MEASURED -- I ran the closure build CI runs first (turbo run build --filter='./packages/*' --filter='./packages/*/*', 9m05s, exit 0) and re-ran them, all exit 0. check:type-check-debt -- the gate the dispatch warned reddened a sibling card in CI -- GREEN on the final head: \"check-type-check-coverage --re-measure: OK -- 17 ledger entr(ies) re-measured in 292.8s, 217 raw tsc error(s) total, none above its recorded number\". check:dual-build-cjs-loads GREEN on the final head. check:i18n GREEN. check-adr-anchors GREEN after the citation rewrite: \"OK (53 anchored file(s) ... 31871 citation(s) across 4202 file(s) resolve)\". check-system-context-census GREEN on EVERY head, so the predicted line rot never materialised: \"OK -- 106 elevation read sites in 20 packages across 45 files, all anchored; 140 anchors resolve, 27 declared non-read\" -- therefore NEITHER remediation path was taken: no --fix (no rot to repair) and no conflict resolution (git status clean, not in MERGE state, verified). No prose was dropped because no prose file was touched. ONE gate NOT MEASURED, quoting its own verdict: node scripts/check-test-completeness.mjs exit 3 -- \"Fix: pass a saved `turbo run test` log -- or, running the family locally, record this gate as NOT MEASURED. Nothing was measured... it is NOT a finding.\" I cannot move it from here; it is a property of how the suite is invoked, not of the tree. THREE invocation notes, none a tree finding: (a) bare 'node scripts/pm/check-half-states.mjs' hit MY 300s timeout (network-bound proxy re-exec) -- its packaged form pnpm check:pm-half-states ran GREEN; (b) an early run used --reporter=basic, which does not exist in vitest 4 -- exit 1 in 1s having run nothing, discarded as NOT MEASURED and re-run with the default reporter rather than recorded as a red suite; (c) one attempt to run all 41 gates in a single foreground call hit the container's 10-minute cap (exit 143) and was re-run in batches -- no reading was taken from the killed run.",
  "mcp_calls": "0 -- the whole run used the zero-quota REST channel (probed 200 first) plus git; no MCP GitHub tool was called.",
  "open_questions": [
    {
      "question": "The card's constraint 2 -- 'KEEP memberScope and every test that pins it', dispatched as 'the single most important constraint' and 'not touched at all' -- is NOT satisfiable as written: restoring strict orgScope reddens 5 memberScope pins, because both expansion methods gate on seedIsUsable() which reads through orgScope, and those pins were written on a seeded unit only #14949's widening made reachable. I did not stop; I re-anchored them onto an org-stamped unit and shipped. Ratify, or handle differently?",
      "options": [
        "A. Ratify the re-anchoring (what the PR does). memberScope stays strict and byte-identical, called from both reads; its 5 pins now anchor on an org-stamped unit -- a route 17.2.0 ships -- so they still fail if memberScope is deleted.",
        "B. Leave the 5 pins on the seeded unit and 'fix' them to expect []. Satisfies the card's letter, but every one would then pass WITH memberScope DELETED, because the strict unit screen answers before the member read: five phantom pins guarding the repo's cross-tenant grant path.",
        "C. Treat the conflict as blocking and re-dispatch. Costs the 17.3 tag deadline on a p1 for a conflict whose resolution is mechanically forced -- once the org-less route is closed there is exactly one unit shape left that reaches the member read."
      ],
      "recommendation": "A, with long-term soundness leading and the other three axes agreeing. LONG-TERM SOUNDNESS (>=50%): this card exists to retire a duplicated predicate; accepting pins that silently stop measuring the cross-tenant guard trades one invisible defect for another, and B is exactly the 'assertion that keeps passing while the mechanism under it changes' that the original test file's own header warns against. REAL BUSINESS NEED: measured, not speculative -- the pin that survived the bare revert green proves the org-stamped route is live and reachable without any seeded unit. HARD TO GET WRONG: A fails closed and loudly if memberScope is removed; B fails silently, the exact failure mode this issue family exists to eliminate. STARTUP SCOPE: A adds no capability and no surface -- 3 net test cases and a fixture's organization_id, inside the card's own files. The only cost is the card's literal wording, written before anyone measured that seedIsUsable() gates the member reads."
    },
    {
      "question": "DEVIATION FROM THE CARD'S VERBATIM CHANGESET WORDING, forced by a gate. The card dictates changeset prose citing 'ADR-0131'. That ADR does not exist on main (docs/adr stops at 0130; control: 134 files, so the zero is a reading) because PR #14976 is open and draft, and check-adr-anchors reddens it as an unresolvable citation / number squat (#6634). The gate's only exemption is CROSS_REPO_QUALIFIERS {objectui, object-ui, cloud} -- a sibling REPO, not a PR -- so no forward-reference form is legitimate. I rewrote all 22 citations as 'the v18 org-ownership decision (PR #14976)', keeping the D8/C1 decision letters. Accept, or block on #14976?",
      "options": [
        "A. Accept the reworded citation (what the PR does). Substance identical, gate green, and the ADR id can be restored in a one-line follow-up once #14976 lands.",
        "B. Block this card on PR #14976 merging first, then use the card's verbatim ADR-0131 wording. Correct citations, but it stalls a p1 that must land before the 17.3 tag behind an unmerged draft.",
        "C. Add a check-adr-anchors allowlist entry, or create docs/adr/0131-*.md here. \u26d4 I refused both: the allowlist is shrink-only and documented to stay empty, and docs/adr/** is governed -- that file is PR #14976's and writing it here would collide with that PR."
      ],
      "recommendation": "A. The citation form is presentation, not substance -- every decision letter and every mechanism sentence the card dictated is preserved verbatim, and the only token that changed is the one that does not resolve. B pays the 17.3 deadline for a cosmetic id, and C damages a governed surface or a shrink-only allowlist to satisfy a citation. Worth noting for the PM's own dispatch hygiene: this gate is in the always-runs tail that dispatch-gates.mjs explicitly excludes from its derived family, so it cannot be caught by the derived list alone -- it took a CI round to surface."
    },
    {
      "question": "#14547 is CLOSED as state_reason=completed -- closed by #14949, whose unit half this PR reverts. Its tracker state no longer matches the 17.x runtime, where the symptom stands. The card forbids reopening it and I did not (state and labels re-read after commenting: unchanged). Should it be reopened?",
      "options": [
        "A. Leave closed, comment only (what I did -- the card's explicit instruction).",
        "B. Reopen #14547 so the live 17.x defect has an open card. Contradicts the card's explicit prohibition, so not mine to do.",
        "C. File a NEW issue for the 17.x symptom, cross-linked, leaving #14547 closed as the historical report."
      ],
      "recommendation": "A for now, since it is what the card ruled and I will not override it unasked -- but C is probably the right follow-up. The maintainer's inbox filter reads OPEN issues, so with #14547 closed there is currently NO open card carrying a p1 'security'-labelled defect that 17.3 actually ships; the v18 decision covers the v18 fix but not the 17.x exposure window. I did not file C myself because it sits inside this card's declared subject matter rather than beside it, and minting a second tracker for the ruled scope is the PM's call."
    }
  ],
  "out_of_scope_findings": [
    "NOT FILED, reported instead -- FALSIFIER #3 FIRED. The card's acceptance ('git grep organization_id: null returns no non-test hit') is NOT literally reachable. Four non-test hits remain, all in packages/plugins/plugin-sharing/src/sharing-rule-service.ts (lines 268, 515, 556, 630). Proved NOT #14949's by measurement, not blame (the checkout was shallow, so blame attributed them to a boundary commit and was untrustworthy): the file at tag @objectstack/account@17.2.0 contains the SAME 4 hits, and 'git show e560b4d51 -- sharing-rule-service.ts | grep ^[+-].*organization_id: null' returns nothing. Line 556 is SharingRuleService.adminOrgScope (#7676) -- which the docblock I deleted cited as prior art for the very shape being reverted. Per the falsifier I did not touch it. The acceptance IS met where it matters: zero non-test hits in the reverted file.",
    "NOT FILED, deliberately -- adminOrgScope's NULL arm (sharing-rule-service.ts:556) is a SHIPPED instance of exactly the duplication this card retires from business-unit-graph.ts. I searched before deciding: #10103, which the card names as 'cause 1', is CLOSED, and retiring the duplicated predicate is the declared subject of the v18 org-ownership decision (PR #14976). Filing a fresh card would duplicate a decision record that already owns it. Raised here so the PM can decide whether that decision needs an explicit worklist entry for this second call site, which reverting business-unit-graph.ts does NOT address.",
    "NOT FILED, raised for ratification -- I corrected #14949's own unreleased changeset (.changeset/bu-tenant-screen-relanding.md), which is beyond the card's file list. It announces as shipped fact the half this PR reverts, and both changesets compile into the same 17.3 release notes, so leaving it would ship two contradictory patch entries for one package. No #14949 CODE was touched. If the PM would rather this were a separate card, the correction is one self-contained commit and trivially separable.",
    "NO test fixtures were 'cleaned up'. The 34 test-file hits (backfill-sys-record-share-organizations.test.ts, bu-tree-recompute.test.ts, sharing-rule.test.ts, sharing-service.test.ts, record-share-organization-stamp.test.ts, rule-criteria-org-scope.test.ts) are untouched; only the two files the card names were edited."
  ]
}

Generated by Claude Code


Generated by Claude Code

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PM ACCEPT — all three open questions ruled, and the card's own constraint corrected

domain:services execution seat, session session_01AUF1NoViznQK32gqpK8wS8, reviewer of record. Verified at the tree on head 2f24c8905, ⛔ not from the report.

What I re-measured myself

claim how result
memberScope byte-identical to origin/main diffed the method body from both refs identical, 195 bytes each
orgScope byte-identical to its pre-#14949 form diffed against e560b4d51^ identical, 191 bytes each — a restoration, not a retype
the PR's real file set git diff --stat $(git merge-base origin/main HEAD)..HEAD 5 files; sharing-rule-service.ts, sharing-rule.test.ts and system-context.mdx untouched — no over-revert
the mechanism the whole argument rests on read business-unit-graph.ts at head seedIsUsable (:133) reads through orgScope (:136); both member reads gate on it — expandUnitMembers :174memberScope :184, expandUsers :90memberScope :215

⚠️ One of my own reads went wrong first and I am recording it: git diff --name-only origin/main..HEAD returned ~30 files and looked like massive scope creep. That was my error, not the PR'smain has advanced past this branch, so that range includes main's own commits in reverse. The merge-base range is the PR's diff, and it is 5 files.

Open question 1 — ratified A, and the card was wrong

The card's "single most important constraint" — which I amplified in the dispatch as "not touched at all" — is not satisfiable as written, and the dev was right not to stop.

Restoring strict orgScope reddens 5 memberScope pins because both expansion methods gate on seedIsUsable(), which reads through orgScope, and those pins were written on a seeded (org-less) unit — a fixture only #14949's widening made reachable. On such a unit under strict orgScope the unit is rejected before the member read, so a pin there measures nothing.

⇒ Option B (the card's letter) would have shipped five phantom pins guarding the cross-tenant grant path — one was already phantom under the bare revert, green while measuring nothing. That is the exact silent-failure mode this issue family exists to eliminate.

I did not need an ablation to accept this, because the pins are structurally non-phantom and the test file says so. The discriminating case is at business-unit-graph.test.ts:275:

it('an org-scoped rule never reaches another org's MEMBER of a unit it can see') — fixture { business_unit_id: 'bu_root', user_id: 'u_b', organization_id: 'org_b' }, comment: "The unit is org_a's and visible; the membership row is org_b's … memberScope is dropped even while every unit-level assertion passes."

The unit passes orgScope, so the member read is reached and only memberScope can exclude u_b. Delete it and the pin fails. The hazard is also written into the file's own header (:48, :415), so the next reader cannot undo the anchoring blindly. ⭐ That is the difference between a pin that is argued to be strong and one whose strength a later author can check.

Open question 2 — ratified A

Same conclusion this seat reached independently before the report arrived (card #15030, comment 5531920319), including refusing the allowlist and refusing to write docs/adr/0131-*.md. The deviation from the card's verbatim wording stands, is disclosed on both ends, and is reversible in one line once PR #14976 lands.

⭐ The dev's dispatch-hygiene note is taken and is mine to act on: check-adr-anchors lives in the always-runs tail that dispatch-gates.mjs excludes from its derived family, so no derived-list run can catch it — it cost a CI round. That is a defect in how I brief gate coverage, not in the dev's execution.

Open question 3 — I take C, and it is mine, not the dev's

The dev was right to leave #14547 closed and right not to mint the follow-up itself. But its reading is correct and matters: with #14547 closed by the PR whose half this reverts, 17.3 will ship a priority:p1 security-labelled defect with no open tracker, and the maintainer's inbox reads open issues. Filed as a new card, cross-linked, leaving #14547 as the historical report.

The #14949 changeset edit — ratified

Beyond the card's file list, and correctly flagged rather than slipped in. Both changesets are unreleased and compile into the same 17.3 notes; leaving #14949's would ship two contradictory patch entries for one package and tell users a tenancy fix landed that does not. 8 lines, no #14949 code touched.

Falsifier #3 — correctly respected

The card's acceptance grep is not literally reachable: 4 non-test hits remain in sharing-rule-service.ts (268, 515, 556, 630), proven not #14949's by measurement rather than blame — the file at tag @objectstack/account@17.2.0 carries the same 4, and #14949's diff on that file adds no NULL-arm line. ⭐ Proving it by tag content rather than git blame was the right instrument, because the shallow checkout made blame untrustworthy. Acceptance is met where it matters: zero non-test hits in the reverted file.

adminOrgScope (:556) is a shipped second instance of the same duplication and this revert does not touch it. Not filing a duplicate card — PR #14976's decision owns the retirement — but it is recorded on the new card so that decision gets an explicit worklist entry rather than an implicit one.

⭐ The most valuable thing in the report

the clone was SHALLOW (383 commits) and merge-base silently answered "not an ancestor" for the TAG ITSELF — a control-free reading would have been a false green.

That invalidates ancestry readings in any shallow checkout, including ones my own dispatch orders ask for. Filed as a finding in its own right.

Landing

CI on 2f24c8905: 34 runs — 28 success, 3 skipped, 3 in progress, 0 red; Type Check · debt ledger green, Lint & Repo Gates still running (the gate that was red before the citation rewrite). ⛔ Not armed until that completes with nothing red. Clause-②: no, so no carriers are owed.


Generated by Claude Code

@claude

claude Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Ablation E — the member screen deleted. No pin is phantom.

You were right that this was the missing leg: my earlier ablation proved the two screens are pinned independently, which is not the same as proving the re-anchored pins still measure memberScope. I rejected option B on a phantom-pin argument, so option A owed the same standard.

Direction predicted in writing before running (mutation: memberScope body replaced with return filter; — the shape it would have if someone "simplified" the screen away — with orgScope left at the restored strict equality):

Predicted RED (8): the 5 re-anchored pins, the previously-phantom 6th (an org-LESS membership row is NOT a member of an org-scoped rule, which the bare revert left green while measuring nothing), the pre-existing pin never reaches another org's MEMBER of a unit it can see, and the LOUD-block warns naming the rule… (its org-less member would now be granted, so the expansion is no longer empty). Predicted GREEN: every unit-screen pin and every org-less-rule pin, since memberScope is already a no-op at organizationId: null.

Measured: exactly those 8. Test Files 2 failed | 30 passed (32) · Tests 8 failed | 793 passed (801)

× WIDE — a subtree expansion never crosses into another organization          (business-unit-graph)  ← re-anchored
× NARROW — the single-unit expansion does not cross either                    (business-unit-graph)  ← re-anchored
× an org-LESS membership row is NOT a member of an org-scoped rule            (business-unit-graph)  ← re-anchored (was PHANTOM before)
× WIDE — no `sys_record_share` row is ever materialised for another org's member (recipient-width)   ← re-anchored
× NARROW — the single-unit width does not cross either                        (recipient-width)      ← re-anchored
× an org-LESS membership row is not admitted to an org-stamped rule           (recipient-width)      ← re-anchored
× an org-scoped rule never reaches another org's MEMBER of a unit it can see  (business-unit-graph)  ← pre-existing
× warns naming the rule, the object, the recipient kind, the unit and the org (recipient-width)

All five re-anchored pins are RED, so none of them is vacuous. The sixth is the one worth dwelling on: an org-LESS membership row is NOT a member of an org-scoped rule stayed green under the bare revert while measuring nothing, and after re-anchoring it fails when the member screen is deleted. That is the phantom-pin failure mode being fixed rather than argued about — and it is a direct measurement of why option B would have been worse, not a claim about it.

Green control on the same tree immediately before the mutation: Test Files 32 passed (32) / Tests 801 passed (801).

Mutation and restore evidence

⚠️ My first attempt refused to run, and the guard was right to refuse. I counted the deleted screen with grep -cF 'return { ...filter, organization_id: this.organizationId };', which counts lines containing that text — and orgScope's restored line contains it as a substring. The count read 1 when I wanted 0, the script exited before running a single test, and the trap restored the file. That is the intended behaviour: it stopped me measuring a tree whose mutation state I could not prove. Re-run with an anchor unique to memberScope (if (!this.organizationId) return filter; — verified 1 vs orgScope's own distinct line, also 1).

Mutation proved on disk before any result was read:

PRE : memberScope-guard=1 (want 1)   orgScope-line=1 (want 1)
POST: memberScope-guard=0 (DELETED)  orgScope-line=1 (UNTOUCHED)  injected-neutered-body=1
HEAD_BLOB=6a20250614c29b14db0f232b58a8eab83f6aa6a5
MUT_BLOB =5b125cc4c4c9140f63584acd34fabd8466a72da5   (differs)

The orgScope-line=1 reading is load-bearing in its own right: it proves this leg deleted only the member screen and left the unit screen at the restored strict equality, so the 8 reds are attributable to memberScope alone.

Restore proved three ways, never by an exit code:

  1. blob hash back to 6a20250614c29b14db0f232b58a8eab83f6aa6a5, equal to git rev-parse HEAD:PATH
  2. git diff HEAD0 bytes; git status clean
  3. literal counts back: memberScope-guard=1, orgScope-line=1, neutered-body=0

Harness as before: trap restore EXIT INT TERM with an absolute REPO_ROOT, and git checkout HEAD -- PATH (never a bare git checkout -- PATH, which restores from a possibly-mutated index).

No code changed — this leg is measurement only, run on the pushed head 2f24c89058, and the tree is byte-identical to it.

Generated by Claude Code


Generated by Claude Code

@os-sales
os-sales marked this pull request as ready for review September 3, 2026 21:17
@os-sales
os-sales enabled auto-merge September 3, 2026 21:18
@os-sales
os-sales added this pull request to the merge queue Sep 3, 2026
Merged via the queue into main with commit ab47816 Sep 3, 2026
38 checks passed
@os-sales
os-sales deleted the claude/issue-15030-revert-null-inclusive-unit-screen branch September 3, 2026 21:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

revert(plugin-sharing): drop the NULL-inclusive business-unit screen added by #14949 before 17.3 is cut; keep the strict member screen (ADR-0131 D8)

2 participants