Repository navigation
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
Conversation
…nant column (ADR-0131 D8) The gate's header and failure text still taught the retired NULL-org platform-row arm as the rule. Restate both, and implement D8's refusal: whereNull/orWhereNull/andWhereNull, a null comparand, the object form and a raw IS NULL fragment on a tenant column reference anywhere in the three SqlDriver-family files is refused. A recognised-predicate floor keeps the rule from passing vacuously. No exemption is needed on main. Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
… organization-less rows ADR-0131 D8 retired the driver's OR organization_id IS NULL arm, but the comments of fifteen runtime files (and one runtime warning) still described it as the live tenant predicate. Restate each: the tenant term is the equality (or IN under group) alone, an organization-less row on a tenant table is reached by no tenant-scoped read, and where code was written against the old arm the text now says so. Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
…e legacy-composite rule text and SharingRuleRow for ADR-0131 - cli.mdx / seed-tenancy-repair.mdx: the live condition is the record of which counter minted the duplicates; since the counter refuses an organization-less row on a tenant table wherever the organization object is registered, it no longer forecasts more. - schema.mdx: a tenant table holds no organization-less row wherever the organization object is registered. - unique/legacy-organization-composite: carry that same hedge. - SharingRuleRow: deploymentWide has no sys_sharing_rule column; the interface is no longer a 1-to-1 stored shape. Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 10 package(s): 8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 153 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 43bd3b8cb97746bb1f66e5984e31b947d6e669de && git checkout 43bd3b8cb97746bb1f66e5984e31b947d6e669de
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b1676441ba59cc32682ffd41e63e74474ad7fa42 e2b88d857238a1dae999df2c271a73cadae160ef && git checkout -B drift-repro b1676441ba59cc32682ffd41e63e74474ad7fa42 && git merge --no-ff e2b88d857238a1dae999df2c271a73cadae160ef
node scripts/docs-audit/affected-docs.mjs --json b1676441ba59cc32682ffd41e63e74474ad7fa42
|
…nger lets a tenant replay an organization-less row MemoryHttpOutbox.redeliver() still carried the v17 NULL arm: a row with no organization passed every tenant's wall. ADR-0131 D8 rules a NULL arm out in every layer, and SqlHttpOutbox (through the driver's applyTenantScope) already answers RESOURCE_NOT_FOUND there. Match it, and flip the pin that held the old arm into one that refuses it. Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ae4eMXWeERGrG6heTbwBX Co-authored-by: Claude <noreply@anthropic.com>
|
Seat note on the PR body · epic PM The dev cannot PATCH the body, so the seat records the correction here. Since commit
Withdrawn from this part: the |
Contract reviewServed-tier: Base: merge-base ① Derived judgments1. The gate —
2. The memory-http-outbox fix is correct and matches 3. Texts are accurate after 7a-1 (#24825), 7a-3 (#24828), 7b-3 (#24845) and 7c (#24836). Each of the 15 runtime files and the 4. Changeset 5. Merge hygiene. Two merges of CI on the head. 41 check-runs, 0 failed (30 success, 6 skipped); ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS |
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-chokepointnow refuses anIS NULLarm on a tenant columnADR-0131 D8 says "
check-tenant-chokepointrefuses anIS NULLarm 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.tenant_column = :tenant, orIN (:tenants)under the group posture.this.resolveTenantField(...),computeTenantFieldorcomputeAndRecordTenantField;tenantField(a local, a parameter or a member);'organization_id'.whereNull/orWhereNull/andWhereNullon such a reference;where/orWhere/andWherewith anullcomparand, two-argument or three-argument;whereRaw,orWhereRaw,andWhereRaw,havingRaw,raw) whose literal text saysIS NULLand that names the column by substitution, by binding or literally.IS NOT NULLis never refused.whereNull(field)on a plain column stays silent.NULL_ARM_EXEMPTis empty, andmainneeds no entry.sys_audit_log.tenant_idattribution partition thatLifecycleServicereads is a D7 field, not a D8 tenant column. It is also outside the scanned files. The header names it.TENANT_PREDICATE_FLOOR = 3(4 onmain). A matcher that stops recognising the tenant column exits 2 instead of reporting clean.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.
events.zod.ts,driver.zod.tsdomains/packages.ts.sys_metadatahas no organization column (D6/D7).organization-scope.ts,delegated-admin-gate.tsbackfill-sys-record-share-organizations.tsauto-enqueuer.tsplatform-object-tenancy.ts. This is a runtimeevidencestring.runtime-identity.ts,builtin/http-nodes.ts. The latter is the runtime[http]warning.http-outbox.ts,memory-http-outbox.ts,sql-http-outbox.tsbackfill-sys-file-organizations.ts,metadata-store.ts3. Docs
deployment/cli.mdx(theos migrate duplicatestable) anddeployment/seed-tenancy-repair.mdx: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: theos explaintext inrule-explanations.tsand the rule docblock indata-model-rules.tsnow carry the "wherever the organization object is registered" hedge.4. Spec TSDoc
SharingRuleRowwas 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, anddeploymentWideis named as the one member with no column.Changeset:
.changeset/24583-7z-closing-d8-texts.md,patchfor the ten packages whose shipped text changed. No existing changeset is touched.Proof
All runs below are on head
e5f47d90dunless marked. That head ismain81120ca merged in with a merge commit.node scripts/check-tenant-chokepoint.mjs --self-testexited 0: "6 reporting shape(s), 6 silent counterpart(s) … D8 null-arm rule: 11 refused spelling(s), 6 silent counterpart(s)."git archive HEADofscripts/plus the three driver files.node scripts/ablation-replace.mjsreplacedreturn builder.where(field, String(tenantId));inapplyTenantScopewith a grouped callback that compileswhere(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 206bca5f0bce to f741fbd69db5.git diff HEADis empty. The scratch copy was green (exit 0) before the mutation.mainplus this diff.pnpm check:tenant-chokepointexited 0: "24 getBuilder() binding(s) across 3 file(s) … no IS NULL arm on a tenant column (ADR-0131 D8)."cd794f6).pnpm --filter @objectstack/spec buildexited 0.check:generatedexited 0: "All 14 generated artifacts are up to date". Nothing was regenerated.@objectstack/formuladist, before its dependency closure was built. It was re-run green afterpnpm --filter '@objectstack/lint...' build.pnpm build --concurrency=2passed 76/76 tasks.pnpm dev -- --fresh -p 38723, merged head):/api/v1/healthreturned 200 andPOST /api/v1/auth/sign-in/emailreturned 200. The boot log had zeroerrorlines. The server was torn down by its PID.node scripts/pm/dispatch-gates.mjs --commandsderived 134 commands, the same list on the pre-merge and merged heads.8c68d506.check-engine-split-ratio(exit 2, shallow clone),check-plugin-teardown-shape --self-test(exit 3, shallow clone), andcheck:skill-examples,check:dual-build-cjs-loads,check:i18nandcheck:lean-entry-closure(exit 3, no build).git fetch --shallow-since=2026-07-15and the full build, all 6 re-ran on the merged head and exited 0.--ranwithcmd :: exit Nrecords: "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-closingexited 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.typecheck(comment and string changes only),test:repo, and the CI-only jobs that--commandsnames.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.tsredeliver()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.applySysRecordShareOrganizationBackfillthreadstenantId, 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.record-share-tenant-wall.test.tsandbackfill-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.tsstill sayssaveMetaItemwritesorganization_id = NULL(~24069, ~29060), butsys_metadatahas no organization column. This is a different drift class from the driver-reads-NULL one, and it is left.Generated by Claude Code