Repository navigation
Commit 0091d84
feat(scripts): check-tenant-chokepoint refuses an IS NULL arm on a tenant column, and shipped texts stop describing the retired arm (18.x step 7, part 7z) (#24880)
Part of #24583
Clause-②: no (the gate refusal narrows what a contributor may write in
three driver files; every other change is text. No accept set, served
body, error code or runtime behaviour moves)
This is step 7's closing part (7z). It leaves #24583 open for the parts
still in flight: 7a-2, 7b-0 and 7b-1+7b-2.
## What changed
### 1. The D8 gate: `check-tenant-chokepoint` now refuses an `IS NULL`
arm on a tenant column
ADR-0131 D8 says "`check-tenant-chokepoint` refuses an `IS NULL` arm on
a tenant column." Before this PR the gate did not do that. Its header
and its failure text also still taught the retired NULL-org platform-row
arm as the rule.
- **Texts restated.** The header and the unscoped-door failure text now
name the one predicate: `tenant_column = :tenant`, or `IN (:tenants)`
under the group posture.
- **Refusal implemented.** It scans every class member and every
module-level function in the three SqlDriver-family files.
- A tenant column reference is one of three things:
- a local bound to `this.resolveTenantField(...)`, `computeTenantField`
or `computeAndRecordTenantField`;
- anything named `tenantField` (a local, a parameter or a member);
- the literal `'organization_id'`.
- Refused spellings:
- `whereNull` / `orWhereNull` / `andWhereNull` on such a reference;
- a `where` / `orWhere` / `andWhere` with a `null` comparand,
two-argument or three-argument;
- the object form with a null value;
- a raw fragment (`whereRaw`, `orWhereRaw`, `andWhereRaw`, `havingRaw`,
`raw`) whose literal text says `IS NULL` and that names the column by
substitution, by binding or literally.
- `IS NOT NULL` is never refused.
- Tenant bindings are scoped to their outermost container. So the filter
compiler's own `whereNull(field)` on a plain column stays silent.
- **Exemptions: none.** `NULL_ARM_EXEMPT` is empty, and `main` needs no
entry.
- The `sys_audit_log.tenant_id` attribution partition that
`LifecycleService` reads is a D7 field, not a D8 tenant column. It is
also outside the scanned files. The header names it.
- **Floor.** `TENANT_PREDICATE_FLOOR = 3` (4 on `main`). A matcher that
stops recognising the tenant column exits 2 instead of reporting clean.
- **Self-test.** A second battery, `D8 null-arm rule`, is pinned at 22
cases: 11 refused spellings, 6 silent counterparts, 2 floor-counter
cases and 3 exemption-channel cases. The roster floor goes from 1 to 2.
### 2. Text drift
Comments in 15 runtime files, plus one runtime warning, said in the
present tense that the driver or engine still reads organization-less
rows. Each is now correct. Where code was written against the old arm,
the text says so.
- spec `events.zod.ts`, `driver.zod.ts`
- runtime `domains/packages.ts`. `sys_metadata` has no organization
column (D6/D7).
- plugin-security `organization-scope.ts`, `delegated-admin-gate.ts`
- plugin-sharing `backfill-sys-record-share-organizations.ts`
- plugin-webhooks `auto-enqueuer.ts`
- objectql `platform-object-tenancy.ts`. This is a runtime `evidence`
string.
- service-automation `runtime-identity.ts`, `builtin/http-nodes.ts`. The
latter is the runtime `[http]` warning.
- service-messaging `http-outbox.ts`, `memory-http-outbox.ts`,
`sql-http-outbox.ts`
- service-storage `backfill-sys-file-organizations.ts`,
`metadata-store.ts`
### 3. Docs
- `deployment/cli.mdx` (the `os migrate duplicates` table) and
`deployment/seed-tenancy-repair.mdx`:
- The live condition is now described as the record of which counter
minted the duplicates. It is no longer "about to mint more". Since 7b-3,
the counter refuses an organization-less row on a tenant table wherever
the organization object is registered.
- The colliding rows "stay findable by this report". The old text said
"visible".
- `protocol/objectql/schema.mdx`: wherever the organization object is
registered, a tenant table holds no organization-less row. On a
single-organization deployment, rows are stamped with the Default
Organization.
- `unique/legacy-organization-composite`: the `os explain` text in
`rule-explanations.ts` and the rule docblock in `data-model-rules.ts`
now carry the "wherever the organization object is registered" hedge.
### 4. Spec TSDoc
`SharingRuleRow` was described as a "Stored shape … 1-to-1 to
sys_sharing_rule". It is now described as a stored row or a declared
rule's projection, and `deploymentWide` is named as the one member with
no column.
Changeset: `.changeset/24583-7z-closing-d8-texts.md`, `patch` for the
ten packages whose shipped text changed. No existing changeset is
touched.
## Proof
All runs below are on head `e5f47d90d` unless marked. That head is
`main` 81120ca merged in with a merge commit.
- **Gate self-test.**
- `node scripts/check-tenant-chokepoint.mjs --self-test` exited 0: "6
reporting shape(s), 6 silent counterpart(s) … D8 null-arm rule: 11
refused spelling(s), 6 silent counterpart(s)."
- **Ablation, on a scratch copy.** The copy is a git-initialised `git
archive HEAD` of `scripts/` plus the three driver files.
- `node scripts/ablation-replace.mjs` replaced `return
builder.where(field, String(tenantId));` in `applyTenantScope` with a
grouped callback that compiles `where(field,
String(tenantId)).orWhereNull(field)`, the v17 arm. The anchor count
went 1 to 0, the injection count was 1, and the blob changed from
206bca5 to f741fbd69db5.
- Under that mutation the gate exited 1 with: "1 IS NULL arm(s) on a
tenant column … sql-driver.ts:15664 applyTenantScope() —
orWhereNull(tenant column)".
- Restored: the blob equals HEAD (206bca5) and `git diff HEAD` is
empty. The scratch copy was green (exit 0) before the mutation.
- **Gate green on `main` plus this diff.**
- `pnpm check:tenant-chokepoint` exited 0: "24 getBuilder() binding(s)
across 3 file(s) … no IS NULL arm on a tenant column (ADR-0131 D8)."
- Exemptions: none.
- **Spec build and generated artifacts** (pre-merge head `cd794f6`).
- `pnpm --filter @objectstack/spec build` exited 0.
- `check:generated` exited 0: "All 14 generated artifacts are up to
date". Nothing was regenerated.
- **Spec suite** (pre-merge head): 703 files passed, 21360 tests passed.
- **Lint suite** (pre-merge head): 150 files passed, 6988 tests passed.
The first attempt failed on a missing `@objectstack/formula` dist,
before its dependency closure was built. It was re-run green after `pnpm
--filter '@objectstack/lint...' build`.
- **Full build** on the merged head: `pnpm build --concurrency=2` passed
76/76 tasks.
- **R1, a fresh boot** (`pnpm dev -- --fresh -p 38723`, merged head):
`/api/v1/health` returned 200 and `POST /api/v1/auth/sign-in/email`
returned 200. The boot log had zero `error` lines. The server was torn
down by its PID.
- **`node scripts/pm/dispatch-gates.mjs --commands`** derived 134
commands, the same list on the pre-merge and merged heads.
- Each command was run with its exit code captured before any pipe.
- 128 exited 0 on the first pass, on the pre-merge head `8c68d506`.
- 6 were prerequisite refusals: `check-engine-split-ratio` (exit 2,
shallow clone), `check-plugin-teardown-shape --self-test` (exit 3,
shallow clone), and `check:skill-examples`,
`check:dual-build-cjs-loads`, `check:i18n` and
`check:lean-entry-closure` (exit 3, no build).
- After `git fetch --shallow-since=2026-07-15` and the full build, all 6
re-ran on the merged head and exited 0.
- `--ran` with `cmd :: exit N` records: "134 derived famil(ies)
accounted for — 134 run, 0 NOT-MEASURED (a DERIVED zero …)".
- **`node scripts/pm/check-governed-merges.mjs --branch
claude/issue-24583-7z-closing`** exited 0: "0 of 23 path(s) hit the
register … NOT governed".
- `scripts/` is not a governed surface. The size is 741 changed lines,
under 3000.
- **Not measured locally** (left to CI): per-package `typecheck`
(comment and string changes only), `test:repo`, and the CI-only jobs
that `--commands` names.
## Acceptance notes
Found along the way and left as they are. None is fixed here, because
the claim's file surface is texts plus the gate.
- `memory-http-outbox.ts` `redeliver()` still admits an
organization-less row for every tenant: its predicate was written to
mirror the v17 SQL term. The SQL driver no longer does this. The comment
now says that the two implementations disagree; the code is unchanged.
Reported to the seat.
- `organizationOutOfWriteReach`
(`service-storage/src/metadata-store.ts`) still answers "in reach" for
an organization-less row, which D8 put out of reach. Under ADR-0131 a
walled deployment holds no such row once certified. The comment says the
branch predates D8.
- `applySysRecordShareOrganizationBackfill` threads `tenantId`, so under
D8 its update no longer reaches an organization-less row. No runtime
path calls it; the 18.x ceremony's attribution fate stamps these rows
now. The comment says so.
- Test-file comments still mention the driver's "NULL-tolerant arm":
`record-share-tenant-wall.test.ts` and
`backfill-sys-record-share-organizations.test.ts`. They are not runtime
files, so they were left.
- `core/src/security/resolve-authz-context.ts` ~795 ("a null-org row is
global") is an authorization reader. That is 7a-2's surface, so it is
left to 7a-2.
- `metadata-protocol/src/protocol.ts` still says `saveMetaItem` writes
`organization_id = NULL` (~24069, ~29060), but `sys_metadata` has no
organization column. This is a different drift class from the
driver-reads-NULL one, and it is left.
---
_Generated by [Claude
Code](https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent a8b44d4 commit 0091d84
24 files changed
Lines changed: 635 additions & 143 deletions
File tree
- .changeset
- content/docs
- deployment
- protocol/objectql
- packages
- lint/src
- objectql/src/tenancy
- plugins
- plugin-security/src
- plugin-sharing/src
- plugin-webhooks/src
- runtime/src/domains
- services
- service-automation/src
- builtin
- service-messaging/src
- service-storage/src
- spec/src
- api
- contracts
- data
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1797 | 1797 | | |
1798 | 1798 | | |
1799 | 1799 | | |
1800 | | - | |
1801 | | - | |
| 1800 | + | |
| 1801 | + | |
1802 | 1802 | | |
1803 | 1803 | | |
1804 | | - | |
1805 | | - | |
| 1804 | + | |
| 1805 | + | |
1806 | 1806 | | |
1807 | 1807 | | |
1808 | 1808 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
302 | 302 | | |
303 | 303 | | |
304 | 304 | | |
305 | | - | |
| 305 | + | |
306 | 306 | | |
307 | | - | |
308 | | - | |
| 307 | + | |
309 | 308 | | |
310 | 309 | | |
311 | 310 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
606 | 606 | | |
607 | 607 | | |
608 | 608 | | |
609 | | - | |
610 | | - | |
611 | | - | |
| 609 | + | |
| 610 | + | |
| 611 | + | |
| 612 | + | |
| 613 | + | |
| 614 | + | |
| 615 | + | |
612 | 616 | | |
613 | 617 | | |
614 | 618 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
594 | 594 | | |
595 | 595 | | |
596 | 596 | | |
597 | | - | |
| 597 | + | |
| 598 | + | |
| 599 | + | |
598 | 600 | | |
599 | 601 | | |
600 | 602 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
2695 | 2695 | | |
2696 | 2696 | | |
2697 | 2697 | | |
2698 | | - | |
2699 | | - | |
| 2698 | + | |
| 2699 | + | |
| 2700 | + | |
2700 | 2701 | | |
2701 | 2702 | | |
2702 | 2703 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
239 | 239 | | |
240 | 240 | | |
241 | 241 | | |
242 | | - | |
243 | | - | |
244 | | - | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
245 | 245 | | |
246 | 246 | | |
247 | 247 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1296 | 1296 | | |
1297 | 1297 | | |
1298 | 1298 | | |
1299 | | - | |
1300 | | - | |
1301 | | - | |
| 1299 | + | |
| 1300 | + | |
| 1301 | + | |
1302 | 1302 | | |
1303 | 1303 | | |
1304 | 1304 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | | - | |
22 | | - | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
37 | | - | |
38 | | - | |
39 | | - | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
40 | 43 | | |
41 | 44 | | |
42 | 45 | | |
| |||
69 | 72 | | |
70 | 73 | | |
71 | 74 | | |
72 | | - | |
73 | | - | |
74 | | - | |
75 | | - | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
76 | 80 | | |
77 | 81 | | |
78 | 82 | | |
| |||
111 | 115 | | |
112 | 116 | | |
113 | 117 | | |
114 | | - | |
115 | | - | |
116 | | - | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
117 | 123 | | |
118 | 124 | | |
119 | 125 | | |
120 | | - | |
| 126 | + | |
121 | 127 | | |
122 | 128 | | |
123 | 129 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
16 | | - | |
| 16 | + | |
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
23 | 27 | | |
24 | 28 | | |
25 | 29 | | |
| |||
591 | 595 | | |
592 | 596 | | |
593 | 597 | | |
594 | | - | |
595 | | - | |
596 | | - | |
| 598 | + | |
| 599 | + | |
| 600 | + | |
| 601 | + | |
| 602 | + | |
| 603 | + | |
597 | 604 | | |
598 | 605 | | |
599 | 606 | | |
| |||
0 commit comments