Skip to content

fix(objectql): a published BulkDataEvent names the one organization the tenant wall named for the batch - #15687

Merged
zhuangjianguo merged 7 commits into
mainfrom
claude/issue-15225-bulk-event-organization-id
Sep 5, 2026
Merged

fix(objectql): a published BulkDataEvent names the one organization the tenant wall named for the batch#15687
zhuangjianguo merged 7 commits into
mainfrom
claude/issue-15225-bulk-event-organization-id

Conversation

@zhuangjianguo

@zhuangjianguo zhuangjianguo commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Closes #15225

Blocked-by: #15706 — the merge only. Update (merge round, 2026-09-05): #15706 is ruled — shape (i), comment 5549579058 on #15225 (「Unblocked」), #15706 closed as completed by ruling — the seam is a separate delivery with its own contract review and is ⛔ not in this PR; the bar this line named is discharged by that ruling, and origin/main is merged in the Merge round section below. Contract review 5549091499 (FAIL, patch round, pinned to d08186cc7) escalated its item 2 — the deployment-level platformGlobalObjects carve-out is unclosable from the schema and needs a maintainer ruling on the seam's shape — and ruled 「this PR does not merge while the carve-out population can be mislabelled」. Items 1, 3, 4, 5, 7 and the item-6 re-judge are discharged in the Patch round R2 section below, at 648e00433, appended on top of the pinned head.

Which path of the leak this lands

This is the bulk producer half of the cross-tenant webhook fan-out leak: data.records.updated / data.records.deleted now carry organizationId when the tenant wall named exactly one organization. The single-record half (data.record.*, DataEvent.organizationId) landed separately as PR #15220 (#14970). Parent #13566 remains open — its consumer half (the fan-out filter) is domain:services and is not addressed here. A statement that "the leak is fixed" must name a path; with this PR both producer paths stamp the key, and the consumer half is still #13566's.

What changed

publishBulkDataEvent (packages/objectql/src/engine.ts, the only data.records.* publisher — measured: one publisher, reached from the predicate update() / delete() branches) stamps BulkDataEventSchema.organizationId from what the producer already holds, with no second query on the publish path:

  • the execution context the Layer 0 wall was computed from (tenantId, accessible_org_ids, isSystem, the carried authz rung posture), and
  • the tenancy posture SecurityPlugin injects through setTenancyPostureProvider (private accessor enforcedTenancyPosture() — deliberately NOT the env-fallback resolveEnginePosture(), now pinned by a test that goes red under that substitution, see R2 item 3).

The derivation is the module-scope bulkEventOrganizationId(objectSchema, execCtx, enforcedPosture), read against computeTenantLayer0Filter's inputs (plugin-security/src/tenant-layer.ts): an exact mirror on the posture and context inputs, a partial one on the object input — the object rows below say which of the wall's clauses the engine mirrors and which the seam carries.

Wall state Key
isolated, ordinary resolved session (rung carried, not PLATFORM_ADMIN) present = the caller's active organization (the wall's equality term)
group, membership set names exactly one organization (deduplicated) present = that member — never tenantId
group, several memberships absent (the option-C mislabel PR #14635's open question 1 rejected)
single posture absent (no wall)
isSystem context absent (the security middleware is bypassed entirely, no wall composed)
no posture injected (no SecurityPlugin) — pinned with OS_TENANCY_POSTURE=isolated set in the env absent — a lean embedding has no wall anywhere: Layer 0 was never composed and driver-memory reads no DriverOptions.tenantId; the env fallback is not consulted
object the wall does not key on — Layer 0's tenancyDisabled clauses ① tenancy.enabled: false and ② systemFields.tenant: false (getObjectSecurityMeta, security-plugin.ts), or no organization_id carried (the column clause) — read through carriesTenantScopeColumn, the registry's binding of the wall's own predicate absent
federated object (external binding) — the superset of the wall's phantom-anchor rule (#7835) absent (conservative: a federated object whose author declared a real remote organization_id keeps its wall and is under-delivered here, never mislabelled)
custom tenancy.tenantField follows the wall — present iff the object carries organization_id; the wall never reads the custom column, so the declaration is not an exit by itself
deployment platformGlobalObjects carve-out — clause ③ of tenancyDisabled, deployment-declared (#12699) NOT mirrored: the engine has no reading of the deployment declaration (0 readings in engine.ts — 2 JSDoc mentions at :2322/:2331 that state the gap, and the reading-shaped predicate, comment lines stripped BEFORE numbering, returns 0 with a firing control of 1 on security-plugin.ts; registry.ts 0; bare-count control on engine.ts: tenancy 26), so a deployment-exempted object under an armed wall is stamped with the caller's organization while Layer 0 composed no wall — the item-2 mislabel, escalated to #15706
carried PLATFORM_ADMIN rung absent — the batch MAY have crossed the wall (ADR-0095 D3)
no rung carried (hand-built context) absent — see the seam finding

absent on this event means "the producer did not assert one organization for the batch" (producer knowledge), deliberately NOT the DataEvent reading "belongs to no organization" (events.zod.ts records the divergence on the member). The value coercion ladder is factored out of the #14970 helper into eventOrganizationValue, shared by both producers — one ladder, two readers.

Context-readability finding (the measure-first question)

The publish site CAN know the wall for every context the authz resolver assembled, and cannot for three populations. What the engine holds: the posture (injected closure), tenantId, accessible_org_ids, isSystem, and the carried rung postureassemble-execution-context.ts stamps posture: authz.posture on every resolved session context. What it does NOT hold: (1) the Layer 0 exemption's second input, the superuser write-bypass bit over the caller's resolved permission sets; (2) the capability probe the plugin runs for contexts that carry no rung (computeLayeredRlsFilter: carried rung when present, probe otherwise); (3) the deployment's platformGlobalObjects declaration, clause ③ of the wall's tenancyDisabled input. For (1) and (2) the producer answers conservatively — absent — and neither leg can mislabel. For (3) it cannot answer conservatively from the schema at all: that is the item-2 mislabel and the seam #15706 rules on (either plugin-security records its Layer 0 verdict on the operation, or a per-object "walled here?" provider is injected beside setTenancyPostureProvider). A contract decision, not taken here.

Why the isSystem read is load-bearing and not redundant with the rung guard: an assembled context can carry isSystem: true together with a rung (execution-context.zod.ts names that shape), and the middleware bypasses the wall on isSystem — without the read, such a write under isolated would be stamped with an organization no wall constrained.

Clause-②: yes

The contract gains no key (BulkDataEventSchema.organizationId was declared optional by PR #15218), but the emitted wire payload gains one it never carried, and tenant-scoped consumers change delivery behaviour on it — that is 「已发布载荷上的新键」, and 「拿不准 ⇒ yes」 settles the rest.

Changeset level: patch, re-judged after R2 item 1

The act of item 1: carriesTenantScopeColumn (registry.ts) gains the export keyword so the engine reads the wall's predicate instead of re-spelling it. Measured on the built dist at 648e00433, with a firing control: dist/index.d.ts 0 / dist/core.d.ts 0 mentions of the symbol (control SchemaRegistry 15 / 1); the ESM export blocks of index.mjs (147 names) and core.mjs (51 names) do not contain it (control SchemaRegistry does); CJS exports.carriesTenantScopeColumn 0 in both bundles (control exports.SchemaRegistry 1). src/index.ts and src/core.ts re-export NAMED members of registry.ts and never export *, so the symbol is unreachable from either entry (., ./core). No public member of @objectstack/objectql is added ⇒ patch under the bump-level rule b337a1308 (the act decides; a fix( that changes no public surface stays patch); the emitted member itself was declared, typed and paid for at minor by the spec half (PR #15218). The R1 appeal to the #14970 precedent is dropped — the review read it as pre-rule, and the level stands without it. check:api-surface is green but measures @objectstack/spec only; the objectql reading is the dist measurement above.

Patch round R2 — contract review 5549091499 on d08186cc7, discharged at 648e00433

  1. Object-level mislabel (P1) — done. bulkEventOrganizationId's object exit is now carriesTenantScopeColumn(schema) (exported at module level from registry.ts; the wall's clauses ① ② plus the column clause, per the 2026-08-14 triage ruling) beside the external != null exit; resolveTenantFieldName(schema) !== DEFAULT_TENANT_FIELD is gone and DEFAULT_TENANT_FIELD is no longer imported by engine.ts. Pin: the P1 fixture (systemFields: { tenant: false } + author-declared organization_id, rows across two organizations and one org-less, isolated, MEMBER predicate update) ⇒ matched: 3, hasOwnProperty === false. Red today, measured: the new pins against the old code read Tests 2 failed | 40 passed (42), both reds on the organizationId assertion itself (P1: present; the custom-tenantField present pin: absent); after the change Tests 42 passed (42).
  2. Deployment-level mislabel — NOT patched, escalated (The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deployment platformGlobalObjects exemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706). No seam, no item-2 pin, no plugin-security change. Measurement, as asked: item 1 does not close item 2 — carriesTenantScopeColumn reads clauses ① ② and the column, never the deployment declaration; platformGlobalObjects has 0 readings in engine.ts and 0 mentions in registry.ts. ⚠️ Corrected from an earlier statement of 「0 mentions in engine.ts」, which was measured before item 4's own JSDoc landed and is false at this head: the file now carries 2 mentions (:2322, :2331), both comment lines that state clause ③ is NOT mirrored. The reading-shaped predicate — grep -v '^\s*\*' applied BEFORE grep -n, since -n would prefix the line number and defeat the comment filter — returns 0, with a firing control of 1 on security-plugin.ts.
  3. Discriminating env-fallback pin — done. The no-enforcement-layer pin sets process.env.OS_TENANCY_POSTURE = 'isolated' (restored by an afterEach that reinstates the collection-time value) and still expects omission. Mutation proof on the committed tree: this.enforcedTenancyPosture()this.resolveEnginePosture() at the publish site, proven on disk by anchored counts (this.enforcedTenancyPosture(), 1 → 0, this.resolveEnginePosture(), 0 → 1, one-line diff), suite ⇒ Tests 1 failed | 41 passed (42), the one red being exactly that pin; restored by git checkout HEAD -- ABSOLUTE-PATH inside a trap, proven: git hash-object = the HEAD blob f3ff938abeab729ad2a9aac6b0f8e0ff58acee0a, git diff HEAD empty, status clean, counts back to 1 / 0.
  4. False mirror claim corrected in three places — done. bulkEventOrganizationId's JSDoc (the object bullet names clauses ① ② ③, which are mirrored, which the seam carries, and the phantom-anchor input answered by the external superset; the "mirrored input-for-input" sentence now says exact on posture/context, partial on the object); this body's table (the four object rows above replace the single "object the wall does not key on" row); the changeset (its "which objects the wall does not key on" paragraph).
  5. Pins for the unpinned object exits — done. Federated (external binding, injected phantom anchor) ⇒ absent (green today, now pinned; the author-declared-remote-column variant is deliberately left unpinned so an exact provenance predicate may later answer it present). Custom tenancy.tenantField with the kernel organization_id still injected ⇒ present = the active organization (red today → green: the wall keys on organization_id); custom tenancy.tenantField on a systemFields: false object carrying no organization_id ⇒ absent (the column clause alone decides; the custom column is never a substitute).
  6. Changeset level — re-judged: patch stands, on the dist measurement in the section above; the changeset states the reason.
  7. Census — done. Pre-fix red on rot only (8 site-without-a-row errors, all engine.ts line shifts); --fix: 16 anchors rewritten, refused zero files; post-fix check-system-context-census: OK — 107 elevation read sites in 20 packages across 45 files, all anchored; population 107 → 107 (item 1 moved none); row 30 re-anchored to engine.ts:2371, which reads if (!execCtx || execCtx.isSystem === true) return undefined;.

Zone 2 mechanism assumptions, measured: (1) falsified in its reason, upheld in its conclusion — registry.ts IS re-exported from both package entries, by named lists, so the symbol stays off the surface because neither list names it, not because the module is private; (2) upheld — no present row flips to absent (the isolated and singleton-group fixtures carry organization_id under both predicates); one absent row flips to present by design, the custom-tenantField row, which is the direction item 5 asked for; (3) upheld by the mutation proof; (4) upheld — pure re-anchor, 107 → 107.

Verification (all at 648e00433, every heavy run through scripts/pm/os-verify-lock.sh, one acquisition each, exit captured before any pipe)

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/engine-data-events.test.ts: Tests 42 passed (42) (VERDICT command-exit 0). Red-today reading against the old code: Tests 2 failed | 40 passed (42). Mutation reading: Tests 1 failed | 41 passed (42).
  • pnpm --filter @objectstack/objectql typecheck: tsc --noEmit ×2 clean, check:test-typecheck: OK (VERDICT command-exit 0). pnpm --filter @objectstack/objectql build: both .d.ts emitted (VERDICT command-exit 0).
  • Gate union: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands run from this worktree with no paths (change set 5 files vs merge base 791a0cbe6) derived 86 commands; all 86 ran through a runner that writes one log per command and a ledger of exit codes — 86/86 exit 0 in their final runs. Two needed a second run: pnpm --filter @objectstack/spec run check:skill-examples (first run exit 1 as a PREREQUISITE NOT MET — packages/client-react/dist held no .d.ts until the whole package closure was built under the lock, turbo run build 71/71 — then 257 prose examples type-check), and pnpm check:docs-audit-scope (first run exit 1 inside its own affected-docs self-test, caused by the residue that aborted skill-examples run left in packages/spec/.examples-build* on a base predating a3eba0759's cleanup fix; residue removed, re-run green; not a finding on this diff). ⚠ The deriver reports this tree 17 commits behind origin/main with 8 derivation files changed there (lint.yml, package.json, the docs-audit scripts, among others): the 86 are this tree's families, declared as such; CI runs the current farm on the merge ref.
  • Targeted eslint --no-inline-config on engine.ts, registry.ts and the test file: exit 0 (narrowed run, declared; the repo-wide sweep is CI's).
  • pnpm --filter @objectstack/spec run check:api-surface: public API surface + factory signatures unchanged ✓ (spec only, see the changeset section).
  • Control-byte scan on the five edited files: grep exit 1, empty output.

Census page — and a declared overlap with PR #15395

The execCtx.isSystem read is elevation site 107, so content/docs/permissions/system-context.mdx carries row 30 for it and its six census-derived counts read 107; R2 re-anchored the 16 engine.ts lines the edit shifted (refused zero). ⚠️ PR #15395 (#14147, draft) rewrites the same page (it removes a row and renumbers) and touches engine.ts — none of its engine.ts hunks (the INSERT readonly-strip path) overlap this PR's regions (event helpers, the posture accessor, the publish site). Per the dispatch, this p0 lands first and #15395's next merge of main absorbs the page: it will need to re-run --fix and re-take the counts (they become 107 → 106 on its diff). One note for that merge: its current hunk renumbers row 51 to 50 but leaves the prose "row 50's doors" pointing at itself; this PR's renumber keeps the reference moving with the row.

Merge round — origin/main merged at 5315098df, head 81b8329ee (contract review PASS 5550145387, pinned to 648e00433)

Why this round: node scripts/pm/ci-failure.mjs --pr 15687 answered UNDETERMINED (no check-runs on 648e00433) — GitHub runs no pull_request workflow on a conflicted PR, so the reviewed head had never been tested by CI. ⛔ Not a patch round: no code changed.

What landed — two append-only commits on 648e00433, no rebase, no amend, no force-push:

  1. cb0f019d6git merge origin/main (5315098df, 62 commits past the merge base 791a0cbe6) via scripts/pm/os-regen-merge.sh. The both-sides set, re-measured before merging: exactly content/docs/permissions/system-context.mdx. The os-regen driver deferred that MIXED page to this branch's side whole (lossless by its line-anchors comparator: main's three page commits e13ede817 / 4bc982111 / a84e1ced6 changed only row-50 anchor numbers) and marked it pending; the script's step-2 took main's side into the worktree and its step-3 commit was refused as stale — the designed collection point — after which the branch bytes were restored from HEAD (index and tree) before regenerating. No MERGE state existed at any regeneration.
  2. 81b8329eepnpm gen:system-context-census on the merged tree, as its own commit: 5 anchor(s) rewritten, all in row 51 (main's row 50): rest-server.ts:50165084, :64306510, :66786758, :71097189, :73027382; nothing else moved (1 line). A second --fix rewrote 0 and refused 0; pre-commit printed current — marker cleared.

The merge changed nothing the review read — blob identity at 81b8329ee against 648e00433 for every PR path except the census page:

Path Blob (both heads)
packages/objectql/src/engine.ts f3ff938abeab729ad2a9aac6b0f8e0ff58acee0a
packages/objectql/src/registry.ts 6c5bb03639f183101a5a52e7cc3f733cfb0c1b69
packages/objectql/src/engine-data-events.test.ts 2c922204fa11e7311795a3bbc50d6d248fc46022
.changeset/bulk-event-batch-organization.md e82c5e234049923e9a0b18972499ce5a1c4be8ac

The merged tree differs from 5315098df in exactly these four paths plus the page (git diff --name-only HEAD^2 HEAD = 5).

Census page, by content in both directions (⛔ 「no conflict markers」 was not the reading): the page with anchor numbers blanked is byte-equal to the 648e00433 page (diff: 0 lines) — this PR's prose, including row 30 (execCtx.isSystem read at engine.ts:2371, the organizationId omission), survived whole; row 51 carries main's five rest-server.ts anchors verbatim (5084, 6510, 6758, 7189, 7382, equal to main's row 50) — main's side survived, re-derived by the generator. Counts re-derived, not carried: 107 elevation read sites in 20 packages across 45 files, 113 reads, 103 behaviour-bearing / 4 carry-only — unchanged, because main moved no read site (the expectation that they would move was falsified).

Verification at 81b8329ee (every heavy run through scripts/pm/os-verify-lock.sh, slot merge-15225, exits captured per command before any pipe): closure build turbo run build --filter='@objectstack/objectql...' 15/15 (VERDICT 0); vitest run --maxWorkers=2 src/engine-data-events.test.tsTests 42 passed (42); pnpm --filter @objectstack/objectql typechecktsc ×2 clean, check:test-typecheck: OK; check-system-context-census: OK — 107 … all anchored; 141 anchors resolve, 27 declared non-read; whole-repo turbo run build 73/73 (VERDICT 0); gate union derived from this worktree with no paths (dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, tree 81b8329ee, declared 1 commit behind origin/main with 4 derivation files changed there): 87 commands, 87/87 exit 0 in their final runs, --ran reconciliation 87 derived, 87 run, 0 NOT-MEASURED, 0 UNRUN. Two first runs were PREREQUISITE NOT MET (not measured, never read as red) — check:skill-examples (no client-react .d.ts) and check:dual-build-cjs-loads (54 packages without dist/) — and passed after the whole-repo build (257 prose examples type-check across 3 surface(s); 103 published require entry point(s) across 66 package(s) load; 619 emitted CommonJS file(s) parse). Changeset gates in the union (check-changeset-no-major, check-empty-changeset, check:changeset-gate-self-tests, check:objectui-changeset) exit 0. Controls: content/docs/releases/** 0 vs content/docs/** 1 (the page); control bytes on the page: grep exit 1, NUL control fires. Not measured locally: the always-runs CI tail and the repo-wide eslint sweep (CI's; no source file changed in this round).

CI now runs: after the push the PR reads mergeable: true, mergeable_state: clean (was dirty); 38 check-runs on 81b8329ee, the six required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL)) all completed / success. ⛔ Read this head's checks, never 648e00433's (it has none).

Untouched by this round: the seam #15706 (ruled (i), a separate delivery with its own contract review), every item the patch round discharged, the draft flag, and both needs:contract-review carriers — clearing the gate is the dispatching seat's act.

🤖 Generated with Claude Code


Generated by Claude Code

…he tenant wall named for the batch

The bulk producer (`publishBulkDataEvent`, behind the predicate `update()` /
`delete()` branches) never set `BulkDataEventSchema.organizationId`, so every
`data.records.*` event read "not asserted" on the bulk path — the remaining half
of the cross-tenant webhook fan-out leak. It now stamps the key from what it
already holds (the execution context the Layer 0 wall was computed from, and the
posture SecurityPlugin injected), with no second query: present under
`isolated` (active organization) and singleton-membership `group`; omitted for
`single`, `isSystem`, multi-membership `group`, non-walled objects, no injected
posture, a PLATFORM_ADMIN rung or no rung. The value coercion is shared with the
per-record helper (one ladder, two readers). Pins in engine-data-events.test.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
…nabled: false` object

A registered object gets the kernel `organization_id` column injected, and the
security plugin walls on that same injected field set, so `task` is not a
"not tenant-scoped" fixture. The pin now uses the declared opt-out and measures
that the column was withheld before asserting the key is omitted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
… `isSystem` read site 107

The bulk producer's new `isSystem` read (no wall composed for a system write,
so no batch organization is asserted) is an elevation behaviour the
system-context page must anchor. Row 30 names it; rows 30-65 renumber to 31-66
with their prose cross-references; the six census-derived counts move 106 -> 107
(property reads 112 -> 113). `--fix` re-anchored the 15 engine.ts lines the
insertion shifted and refused zero files once the row existed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
@github-actions

github-actions Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/objectql, touching 8 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/environment-variables.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))
  • content/docs/deployment/tenancy-modes.mdx (via TenancyPosture (symbol, a top-level type))
  • content/docs/permissions/authorization.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))
  • content/docs/permissions/permission-sets.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v15.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))
  • content/docs/releases/v16.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))
  • content/docs/releases/v17.mdx (via PLATFORM_ADMIN (literal, a string literal in bulkEventOrganizationId))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 anchor(s) matched too much of the corpus to be a work list: ObjectQL (symbol, 66 pages)
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 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 — 16 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 16978315c4dfb388129f1c3955b056cc8aec0326packageMentionDocs.

Which tree this was computed on

This run read content/docs from 2d4bb8d86ff2dcfc81cecfac7d609d52462dcd70 — the merge of head 81b8329ee84beb27f07bc6aa4da09b006fa9dacb into base 16978315c4dfb388129f1c3955b056cc8aec0326, 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 2d4bb8d86ff2dcfc81cecfac7d609d52462dcd70 && git checkout 2d4bb8d86ff2dcfc81cecfac7d609d52462dcd70
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 16978315c4dfb388129f1c3955b056cc8aec0326 81b8329ee84beb27f07bc6aa4da09b006fa9dacb && git checkout -B drift-repro 16978315c4dfb388129f1c3955b056cc8aec0326 && git merge --no-ff 81b8329ee84beb27f07bc6aa4da09b006fa9dacb

node scripts/docs-audit/affected-docs.mjs --json 16978315c4dfb388129f1c3955b056cc8aec0326

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 16978315c4dfb388129f1c3955b056cc8aec0326 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@claude

claude Bot commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Contract review at CONTRACT_REVIEW_TIERFAIL (patch round) on d08186cc7. Verdict on the card (#15225 5549091499); ⛔ the gate is not cleared and this PR does not merge yet.

domain:engine execution seat. Pointer only — the verdict's carrier is the card.

In one line: the posture table is upheld branch by branch, the singleton-group member is right (that member, never tenantId), no second query, single producer confirmed, the absent-semantics divergence between the two schemas is respected everywhere, and the patch level is upheld on independent grounds. What fails is one derivation: bulkEventOrganizationId mirrors one of three clauses of the wall's own tenancyDisabled input, so on two object populations it stamps a key where Layer 0 composed no wall — a mislabel, measured (organizationId: 'org_acme' on a 3-row batch spanning two organizations and an org-less row). Plus a pin gap: substituting the env-fallback posture at the publish site leaves all 38 pins green.

Three separate dispositions, ⛔ not one:

  1. Patch round dispatched for items 1, 3, 4, 5, 7 (+6 as a re-judge).
  2. Item 2 is escalated, not patched — the deployment platformGlobalObjects clause is invisible to the engine (measured: 0 mentions in engine.ts, against a firing control of 20 for tenancy), so closing it needs a cross-lane seam whose shape is a maintainer ruling. Filed as The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deployment platformGlobalObjects exemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706; this card now carries Blocked-by: #15706 for the merge only.
  3. No merge meanwhile — on the reviewer's own terms. needs:contract-review stays hung on both carriers; the PR stays draft; ⛔ nothing flipped, ⛔ nothing armed. CI green at this head (38 runs, 33 success / 5 skipped, legacy Vercel success, clean) changes none of that.

⚠️ Nothing regresses while this is decided: the bulk path emits no key today, so the risk is only in landing the fix as written. The single-record half (#14970) is unaffected.


Generated by Claude Code

…, not a re-spelling of one of its clauses

Patch round R2 on the contract review of PR #15687 (items 1, 3, 4, 5, 7; item 6
re-judged). `bulkEventOrganizationId` answered its object exit with
`resolveTenantFieldName(schema) !== DEFAULT_TENANT_FIELD`, which mirrors ONE of
the clauses Layer 0 folds into `tenancyDisabled`; an object declaring
`systemFields.tenant: false` beside its own `organization_id` composes NO wall
in plugin-security and was still stamped with the caller's organization — a
mislabel (the reviewer's P1). The exit now reads `carriesTenantScopeColumn`,
the registry's binding of the wall's predicate (exported at module level only;
`dist/index.d.ts`, `dist/core.d.ts` and both entries' runtime export lists are
unchanged, measured with a firing control), beside the `external != null`
superset of the phantom-anchor rule. A custom `tenancy.tenantField` is no
longer an exit by itself: the key follows the wall, present iff the object
carries `organization_id`.

Pins: the P1 fixture (absent, matched 3); a federated object (absent); a custom
`tenancy.tenantField` with the kernel column (present) and without it (absent);
and the no-enforcement-layer pin now sets OS_TENANCY_POSTURE=isolated in the
env and still expects omission, so the env-fallback exclusion is pinned by a
test that goes red under the substitution the review measured green.

JSDoc, changeset and the census page name which `tenancyDisabled` clauses the
engine mirrors and which the seam carries; the census re-anchor rewrote 16
anchors and refused zero, population 107 unchanged. Level stays `patch`: no
member reaches this package's published surface.

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

Copy link
Copy Markdown
Collaborator Author

Contract re-review at CONTRACT_REVIEW_TIERPASS on 648e00433. Verdict on the card (#15225 5550145387). ⛔ The merge stays barred by #15706, and ⛔ the gate is deliberately not cleared.

domain:engine execution seat. Pointer only — the verdict's carrier is the card.

In one line: every patch item (1, 3, 4, 5, 7, and 6 as a re-judge) is discharged, and each was re-measured from the tree rather than read off this body — the P1 fixture flips present → absent, the item-3 pin is the only red under the env-fallback substitution, item 5's custom-tenantField row flips absent → present with organization_id carried and stays absent without it, and patch holds on a fresh build (not the turbo cache) across .d.ts, ESM export lists, CJS and a live import/require probe, each with a firing control. ⭐ The previous reviewer's parked probe — whose P1 asserts the defect — now goes red, an independent second witness that the mislabel is gone.

Item 2 is escalated, not undone, and the reviewer proved it rather than assuming: it checked whether plugin-security ever writes the carve-out into the registry schema the engine reads. It does not — tenancyDisabledCache is fed by clauses ① ② alone, the only other use is a boot log — so the open population is exactly clause ③, and no engine-side exit can answer it conservatively. The merge bar 「this PR does not merge while the carve-out population can be mislabelled」 is upheld at this head.

⛔ Why a PASS does not clear the gate here

「放行 = 清标即落地」 — clearing the carrier is the landing act, and landing is barred by #15706, a cross-lane maintainer ruling this seat may not pre-empt. So needs:contract-review stays on both carriers and this PR stays draft, with the PASS on record: once #15706 is ruled and its consequence lands, clearance is a one-stroke act against a verdict that already exists. ⛔ Nothing flipped, ⛔ nothing armed.

Two evidence corrections the verdict required before landing — both done

  1. This body stated 「0 mentions of platformGlobalObjects in engine.tstwice, and that is false at this head: the file carries 2, both JSDoc lines item 4 added to state the gap. A stale measurement, not a substantive error — but the body is the delivery's own account. Both statements now read 0 readings / 2 stating mentions, with the reading-shaped predicate and its firing control spelled out. Read back and verified.
  2. The bulk-event producer re-derives the tenant wall and can only see one of its three clauses — a deployment platformGlobalObjects exemption is invisible to the engine, so the p0 fix can MISLABEL a cross-org batch #15706's re-check instrument was defective — and it was mine. I had published grep -nE '…' | grep -v '^\s*\*' with 「expect 0」; run verbatim it returns 1, because -n prefixes 2322: before the comment filter sees the line. I replaced an under-reporting control with an over-reporting one, which is the worse failure: a reader would see an apparent entitlement reading that does not exist. Corrected at 5550147691 to the filter-first form — 0, control 1 on security-plugin.ts.

⚠️ Merge sequencing A is upheld, with the conditions restated because the driver fails silently: origin/main is now 47 commits past the merge base and the census page was edited by two PRs (#15689, #15726), while this PR's code files have 0 hunks on main. At the landing round: hand-merge the page's prose, run pnpm gen:system-context-census, re-take the counts, re-derive the gate union on the merged head, and ⛔ never read 「no conflict markers」 as a merge answer.

Adoption record: verdict from a context-isolated fable subagent fed only the card, the FAIL, the dev report, this PR and #15706 — ⛔ never this seat's conclusions — transcript-verified at 125 "model":"claude-fable-5-1" stamps and no other model over 815,874 bytes, and adopted verbatim.


Generated by Claude Code

Discharges the os-regen deferral recorded by the merge of origin/main
(5315098): the driver kept this branch's side of the MIXED census page
whole, dropping main's five rest-server.ts anchor moves on row 51 (main's
row 50), and `pnpm gen:system-context-census` re-derives exactly those
five numbers from the merged tree. Population unchanged at 107 in 20
packages across 45 files; a second `--fix` rewrites 0 and refuses 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARYe3yQTQCUFm5qPYNgKaJ
Merged via the queue into main with commit 4b3955e Sep 5, 2026
42 checks passed
@zhuangjianguo
zhuangjianguo deleted the claude/issue-15225-bulk-event-organization-id branch September 5, 2026 09:13
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
… SEAM, not by number

`check-system-context-census` held every `file:line` anchor on the page to the
tree, but nothing held the two other hand-maintained claims on the same page: the
page's own prose row references and the `why:` strings in `NON_READ_ANCHORS` that
name a row by number. A row inserted into the behaviour table renumbers every row
below it and silently falsifies every reference past the insertion point, with the
gate green — measured on #15687 (three references went stale) and #15395 (all
three became true again by coincidence, observed by nothing).

Each reference now declares the SEAM it is about — a literal occurring in exactly
one numbered row — and the gate resolves the seam, reads the number the reference
carries, and refuses when they disagree, naming the reference, its number and the
row the key resolves to. A ledger keyed by number would rot in the same edit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
baozhoutao pushed a commit that referenced this pull request Sep 5, 2026
…ertion as an ablation

23 cases: a positive control, one per spelling the page uses (`Row N`, `row N's`,
`row N`, `rows N–M`, and `row-N` inside a `why:`), a `why:` disagreement, keys that
resolve to no row and to two rows, the undeclared-reference population refusal, and
the real page today.

The ablation copies the REAL page into a temp dir, inserts one row above row 34 and
renumbers — the exact edit PR #15687 made under a green gate — and asserts the check
reddens naming `Row 34` (now row 35), the two `why:` strings for rows 34 and 60, and
both stated row extents. A byte assertion proves the real page was never written.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zGPuVVX3deAx9LdjK8jCk
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

2 participants