Uh oh!
There was an error while loading. Please reload this page.
fix(sharing): stamp organization_id on every sys_record_share write, backfill the stranded rows, admit the object to the tenancy ledger (#14484) - #14726
Conversation
…r row; backfill module Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
… backfill legacy rows, admit the object to the tenancy ledger Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…cord-share-org-stamp
… on the merged tree (#14484) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…ation (#14484) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…he backfill test double the precedent's shape (#14484 gates) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…es (#14484) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
📓 Docs Drift CheckThis PR changes 2 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 20 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 8276750644960e28c1bad2cf743d543af3cff69a && git checkout 8276750644960e28c1bad2cf743d543af3cff69a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dee4dd4ba33f8b98ff90e240327b7567adecc017 8344d31b5c7de3b802f85b0a99b144d83544bf87 && git checkout -B drift-repro dee4dd4ba33f8b98ff90e240327b7567adecc017 && git merge --no-ff 8344d31b5c7de3b802f85b0a99b144d83544bf87
node scripts/docs-audit/affected-docs.mjs --json dee4dd4ba33f8b98ff90e240327b7567adecc017
|
…cord-share-org-stamp
…14484) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
os-sales
commented
Sep 2, 2026
In-seat Clause-② contract review — VERDICT: FAIL. One blocking finding, and it is a real one.Reviewer: isolated subagent, fed only the card, the ruling and this PR. Model served, verified from the transcript rather than from self-report — The reviewer was told mid-run that the head had moved and re-anchored its readings itself; the head-comparison work in ① is its own. VERDICT: FAIL ① Head anchoring: readings were first taken on Scope, held. Both halves are in one diff: the writer ( ② Derived on the new head: ③ Rule-materialised grant: carries the rule's org via TESTS: BLOCKING:
§5 NOTES:
The seat's dispositionsBLOCKING 1 is the patch round, and it is the finding the whole review was for. A transient read failure on a permission-boundary column does not fail closed here — it fails sideways, into the caller's organization. Everything that would normally contain such a mistake is absent by construction: the row is not NULL so the backfill cannot repair it, a tenant-scoped read makes the grant invisible to the org that owns the record and visible to a sibling, and the only trace is a §5, disposed one by one. Notes 1, 2 and 3 ride patch round 1: note 1 is this PR's own changeset sentence, and an inaccurate release note about a walled-install behaviour change is exactly the sentence an operator reads before upgrading; note 2 is a one-token level change and the precedent (#13491 shipped its admission rows as ⛔ Not reopened: ① scope, ② Clause-② and the changeset shape for Both carriers stay hung on this PR and on #14484. This PR stays draft. Neither is cleared until a delta review — the same reviewer, resumed, on the patch head — returns PASS. ⏳ Scheduling, stated so the wait is visible: this seat is at the maintainer's dispatch cap of 3 running dev agents. The patch round is queued behind PR #14528's merge round, which was already declared first in line at 20:56Z. Nothing is being dispatched ahead of either. Generated by Claude Code |
…cord-share-org-stamp
…organization on a direct grant (#14484) Patch round 1 of the 2026-09-02 contract review (BLOCKING 1): `recordOrganization` returned the same `null` for "the record carries no organization" and for "the read threw", so `resolveDirectGrantOrganization` fell through to the acting session's organization on a transient read failure — a wrong stamp on a permission-boundary column that the backfill (`WHERE organization_id IS NULL`) could never repair. The reading is now a three-way `RecordOrganizationReading` (`organization` / `none` / `read-failed`). The direct path substitutes the session's organization for `none` only; `read-failed` yields `null` to the engine's ruled derive-or-refuse (#8844). The warn text now states what the failed read does NOT do. Pinned on the double (insert half, update half, and the `none` control) and on a real SqlDriver + ObjectQL: `single` with no organization -> NULL, `single` holding two organizations -> refused as ambiguous, `isolated`/`group` -> refused, plus the read-working control on both. Review §5: note 3 pinned (the scoped update half's silent no-op on a row stamped with another organization — `grant`'s return value, the two-rules `grantsCreated` path and the re-homed-record `grantsUpdated` path); note 1 — the changeset now states the two shipped paths that meet the walled-install refusal (platform-global rule x organization-less record, with the mid-loop reconcile abort; and a failed direct-grant read); note 2 — objectql graded `minor`. Census page re-anchored with `--fix` (8 line-rot anchors, 0 refused). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…RECORD_NOT_FOUND abort (#14484) CI on the previous head measured two things the first spelling of these pins assumed wrongly. 1. A plain `{ userId, tenantId: ORG_B }` session never reaches an ORG_A record through `grant`: `assertCanManageShares` -> `isRecordVisible` reads under the CALLER's context and the driver scopes it to `organization_id = :active OR IS NULL` on every posture. The reachable wrong-stamp shape is the `group` one — a multi-member owner active in the sibling organization, whose membership set the engine threads as `tenantIds` — plus the `single` install holding several organizations under a session carrying no active organization (nothing to stamp wrongly, but the failed read must end in the ambiguity refusal, never a NULL row). Both pinned with their read-working controls; `isolated` is unreachable (one organization per session). 2. Review §5 note 3 read the scoped update half's unreachable-row case as a SILENT no-op. Measured on the real engine it is loud: `ObjectQL.update` reports the unreachable row as RECORD_NOT_FOUND (404), `grant` throws it, and `evaluateRule`'s pass aborts there — grants before it stay, the ones after it and the stale-row revocation never run. Pinned as measured (the direct grant, the two-organizations path with a stale-row witness, the re-homed-record path); the implementation is unchanged, and the `grant` comment that called it silent now says what was measured. Census page re-anchored with `--fix` (4 line-rot anchors, 0 refused). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
…cord-share-org-stamp # Conflicts: # content/docs/permissions/tenant-audit-census.mdx # docs/audits/2026-08-tenant-audit-write-call-sites.counts.md
…14484) The merge of origin/main conflicted in two generated census artefacts and silently dropped one side of a third. All three are resolved by regeneration, never by hand: node scripts/tenant-audit-census.mjs --write pnpm gen:system-context-census The tenant-audit population is the joint one: main's suspended-run-store row and this branch's backfill row both land, moving the census 218 -> 219 write call sites. The nine hand-written prose figures the gate holds to that census (check B) follow it; `scripts/engine-double-contract.pinned.json` needed no regeneration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
The |
Fixes#14484
What this lands
Maintainer ruling 2026-09-02 (decision batch #11 item 3, A adopted — verbatim 「#13564 转维护者处理;其他同意」, recorded at 14484#issuecomment-5511797642):
sys_record_shareis tenant-scoped, writer-repaired, and the rows written before the repair are backfilled from the record they grant access to. Both halves in one diff, in the ruling's order — writer first, backfill second, ledger row alongside.packages/plugins/plugin-sharing/src/sharing-service.ts).SharingService.grantstampsorganization_idon both halves of its upsert. A rule-materialised grant carries the granting rule's organization:SharingRuleService.reconcile/reconcileForRecordnow handgrantthe rule's owncriteriaContext(rule)—{ isSystem, tenantId: rule.organization_id }, the same context the rule's criteria sweep already ran under (finding: an org-stamped sharing rule's criteria sweep runs unscoped — recipients are org-aware, matched records are not, and per-org materialization (10103) multiplies it #10119) — so the grant lands in the organization whose records the rule was allowed to sweep. A direct grant carries the shared record's organization, read off the column the record's object is actually walled by (resolveTenantFieldNamefrom@objectstack/objectql: ADR-0066 opt-out → declaredtenancy.tenantField→ injectedorganization_id), with the acting session's organization as the fallback for a record that carries none (thesys_approval_requestwriter's ruled shape: subject first, acting context second). The organization rides the write context astenantIdand the row literal — the{ isSystem, tenantId }shape the System-context writes land untenanted at RUNTIME, so a single-tenant install keeps re-forking the autonumber scope and minting duplicate business identifiers — the producer #8686's backfill cannot reach (17.0.0 GA) #8844 refusal message prescribes — so the driver's tenant audit is satisfied and the update half lands through the driver's own scope (organization_id = ? OR IS NULL, which keeps a pre-repair NULL row in reach). Nothing resolvable ⇒ an explicitnullon the row, for the engine's ruled derive-or-refuse to decide. ⛔ The service's eleven bare-context reads are untouched (measure: census the dependents of the SQL driver's orWhereNull tenant-wall carve-out before deciding its future (NULL org_id rows are globally visible on shared-DB walled deployments) #13564's question, with the maintainer).packages/plugins/plugin-sharing/src/backfill-sys-record-share-organizations.ts, new). Thebackfill-sys-file-organizations.tsshape, per-table and not a sweep: scansWHERE organization_id IS NULL, re-reads each row's record (object_name+record_id) at repair time, stamps the record's organization off its wall column. Dry run first and by default;runSysRecordShareOrganizationBackfill(engine, { dryRun: false })writes; idempotent by construction (pinned by running it twice). Trigger copied from both precedents: an operator-invoked module, not exported from the index and not bundled intodist(tsupbuildssrc/index.ts) — nothing runs at boot and nothing is scheduled.packages/objectql/src/tenancy/platform-object-tenancy.ts).sys_record_shareleavesunclassifiedastenant-scopedwith the ruling as the cited fact; the admitted-list pin intenancy-by-object-classification.test.tsgains the entry (the necessary companion of the row — it pins the list, not a count). This is the whole of the cross-domain touch.Orphans — the implementer's choice, stated
Grant rows whose record no longer exists are left NULL, counted (
totals.orphans/residue.recordNotFound) and logged (onewarnline per plan, naming the count) — never deleted here. Reason: the invariant "record gone ⇒ the row cannot describe any access" is already owned, byrecord-orphan-cleanup.tsand thekernel:bootstrappedsweepSharingService.sweepOrphanedRecordShares(#5103), which runs on every boot ahead of the rule-grant passes and deletes exactly this population. A second deleter would be the fork that module exists to prevent (two chunk sizes, two "a failed probe deletes NOTHING" rules). Pinned: the orphan row is present after the run, counted, logged, and the second run re-reports it without writing. Every other underivable row stays NULL and is reported by reason (object has no organization column · object has no readable schema · record read failed · record exists but carries none · unaddressable), for a dry run too.Premise check (P1–P4), measured on
origin/mainatc616c2cc2(the card read at9e286e248)git grep -nE "(insert|update|upsert)\(\s*['\"]sys_record_share"over non-test sources: exactlysharing-service.ts:1242(update) and:1261(insert). The pre-existingbackfillRetiredAccessLevels(sharing-plugin.ts, fix(sharing): 「完全访问」声明了删除/转移/共享,实现上与「编辑」等价 #3865) doesengine.update(object, { id, access_level: 'edit' })onsys_record_share— a one-column normaliser that predates the card's reading and creates no rows; it is not a second writer of the organization and is untouched.organization_id: holds. The only non-commentorganization_idhits in the file were theHierarchyScopeContext.organizationIdplumbing (~:1450–1522), not a write.unclassified: holds.sys_record_sharewas absent fromPLATFORM_OBJECT_TENANCY, i.e. unclassified by absence.SqlDriver.injectTenantOnInsertstill fires only onDriverOptions.tenantId;buildDriverOptionsstill sets it only whenexecCtx.tenantId !== undefined.Engine.resolveSystemInsertOrganizationreturns early for anunclassifiedobject; once admitted, an organization-less system insert onsys_record_shareis derived only on asingleposture with exactly one organization and refused (ERR_SYSTEM_WRITE_ORGANIZATION_REQUIRED, 500) ongroup/isolated— the engine never derives the rule's or the record's organization, it derives the install's. So the writer half is required, and it is also what makes the walled case pass. Pinned inrecord-share-organization-stamp.test.ts: onisolatedandgroupa bare system insert is refused with that envelope, and the repaired writer's grant sails through carrying the organization.Pins the ruling names
deal_p1is the discriminating row: ORG_A under an ORG_A rule, NULL under a platform-global rule)record-share-organization-stamp.test.ts(realSqlDriver+ObjectQL),sharing-service.test.tsbackfill-sys-record-share-organizations.test.ts(fake),record-share-organization-stamp.test.ts(real driver)computeTenantLayer0Filterreturns the same grants the bare-context read returns for that organization (isolated and group), and the two organizations' reads are disjoint; the control case shows the legacy NULL row on the bare read and on no tenant readpackages/plugins/plugin-security/src/record-share-tenant-wall.test.ts(new; plugin-security owns the wall and already devDepends on plugin-sharing) — the backfilled-rows half composes Layer 0'sisolatedpredicate intowhereexactly asandComposeLayersdoes, in the plugin-sharing backfill testVerification (union run on
238211787; re-run unchanged on the post-sync head1ad15772f— see the last section)pnpm --filter @objectstack/plugin-sharing exec vitest run --maxWorkers=2→Test Files 32 passed (32) · Tests 759 passed (759);pnpm --filter @objectstack/plugin-sharing typecheck→ exit 0 (check:test-typecheck: OK … 2 file(s) / 3 error(s) / 3 pinned signature(s)— the debt ledger is unchanged).pnpm --filter @objectstack/plugin-security exec vitest run --maxWorkers=2(whole package, 96 files: ten of its suites consume@objectstack/plugin-sharingfromdist/) →Test Files 96 passed (96) · Tests 1790 passed (1790);pnpm --filter @objectstack/plugin-security typecheck→ exit 0.sys_record_share;git grep -l sys_record_shareoverpackages/objectql/**/*.test.tsnames two files, plus the eight*tenan*.test.tsfiles that exercise the tenancy machinery →vitest runon those 9 →Test Files 9 passed (9) · Tests 103 passed (103);tsc --noEmit -p packages/objectql/tsconfig.json→ exit 0 (after rebuilding@objectstack/types, which the mergedmainmoved).cmd > log 2>&1; EXIT=$?); the verdict lines quoted are the tools' own.Ablation (two legs, each: mutate → prove on disk → build → prove in
dist/→ run → restore → prove restore)organizationId = null; (globalThis as any).ABLATION_14484_WRITER = true;right after resolution; a//marker was tried first and is not a valid measurement — esbuild strips comments,ablation-dist-preflightreported it absent, so the leg was redone with a marker that survives the bundle). Mutation proven bygrep -c= 1 andgit hash-object≠ HEAD blob;pnpm --filter @objectstack/plugin-sharing buildthennode scripts/ablation-dist-preflight.mjs @objectstack/plugin-sharing ABLATION_14484_WRITER→ exit 0. Result: plugin-sharingTests 15 failed | 125 passed (140)(every organization pin insharing-service.test.tsandrecord-share-organization-stamp.test.ts, including the walled-posture "NOT refused" cases, which flip to refused), plugin-securityTests 4 failed | 1 passed (5)(every equality pin; only the control survives). Restore bygit checkout HEAD -- ABS_PATH:git diff HEAD --statempty, blob hash = HEAD blob, marker count 0, rebuild, preflight--absent→ exit 0.const organization = null as string | null; // ABLATION_14484_BACKFILL, replacingnonEmpty(record[column])). Proven by marker count 1 / original text count 0 / blob ≠ HEAD. Nodist/consumer exists for this module (unexported, unbundled; both suites resolve it fromsrc) — stated, not skipped. Result:Tests 10 failed | 14 passed (24)— every planning / apply / idempotency / report pin, the real-driver backfill case, and the cliff's AFTER case. Restore proven as above.Gates — derived from the actual change set
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(15 paths vs merge base; 70 commands: 64 by path, 8 by change kind). All 70 were run with exit captured before any pipe:check:system-context-census(page re-anchored withpnpm gen:system-context-census;--fixrefused zero files, every change a pure line shift),check:tenant-audit-census(node scripts/tenant-audit-census.mjs --writere-derived the ledger — one row arrives: the backfill'supdateonsys_record_share, classified elevated; the page's nine hand-written prose figures restated at the new population, as the gate's own contract requires),check:engine-double-contract(two new doubles pinned with--write),check:objectql-double-limit,check:doc-authoring,check:cross-package-test-inputs,check:test-source-alias,check:durability-log-level,check:adr-0087-registration,check:changeset-no-major,check:empty-changeset,check:skill-examples(after building the@objectstack/client-reactclosure it reads),check:type-check-coverage, and the docs/spec families.check-engine-split-ratio(exit 2: shallow clone, the 90-day window is not covered; "refusing rather than printing a caveat"),check-test-completeness(exit 3: needs a savedturbo run testlog — "not a red, nothing to fix"),check:dual-build-cjs-loads(exit 3: "PREREQUISITE NOT MET — 52 packages have no dist/"),check:type-check-debt(exit 3: "build the closure first … nothing was measured"),check:i18n(exit 1 with "Nothing was checked" — its configs resolve through unbuiltdist/). All five run in CI'sLint & Repo Gateson a full build.check:doc-authoring(tracker ids inside operator-facing strings — moved to adjacent//comments) andcheck:objectql-double-limit(my backfill test'sfinddouble was UNJUDGED under the probe — reshaped to thesys_fileprecedent's double, with the tenant read composing Layer 0's predicate intowhere).Fixture triage
rule-criteria-org-scope.test.tshand-builds thesys_record_sharetable from a field list that omitted theorganization_idcolumn the registry provisions on every platform object, so the stamped insert hitno column named organization_id. Disposition: 补声明 — the fixture was never spec-faithful, the omission just had no reader. Consumption-radius sweep: exactly two hand-built real-driversys_record_sharefixtures exist in the repo (git grepover**/*.test.ts∩initObjects|new SqlDriver); the other,read-scope-provenance-mark.test.ts, already declares the column.Clause ②:
yes— declared by the ruling (tenant-boundary write change), re-derived from the diffAll 13 are in the unexported, unbundled backfill module — zero additions to any package's published entry point, and no
packages/speccontract moved (GrantShareInput/RecordShareare unchanged; the organization reachesgrantthroughExecutionContext.tenantId, the field every transport already fills). The mechanical derivation therefore under-states the tier: what makes this Clause ②yesis the behavioural contract — everysys_record_sharewrite now carries a tenant, the engine refuses an organization-less system write on it under a wall, andSharingRuleServicethreads the rule's organization into the grant. The declaration stands;needs:contract-reviewis on this PR and on the card, and ⛔ neither is cleared here — both are the PM seat's after the isolated contract review.Changeset
.changeset/record-share-organization-stamp.md:@objectstack/plugin-sharingminor (a behaviour change on a published writer plus a new operational module — graded, not skipped, because the release notes are where an operator learns the backfill exists and that the dry run comes first),@objectstack/objectqlpatch (one ledger row; the engine's ruled derive-or-refuse now coverssys_record_share). Not declared-breaking: no authorable key, export or config field is removed or renamed, so no ADR-0087 disposition is owed (check:adr-0087-registrationgreen).Scope fences honoured
sys_record_shareand no other table — the scope pin is a named constant with the ruling in its doc. ⛔ The 11 bare-context reads in the service are unchanged. ⛔content/docs/permissions/system-context.mdxwas never hand-edited:origin/mainmerged and committed first, thenpnpm gen:system-context-censuson the merged tree, with the gate as the proof. ⛔ fix(plugin-sharing): a seeded business unit is a usable rule recipient, and its members are tenant-screened (#14547) #14572's files were not edited exceptsharing-rule-service.tsat the fourgrantcall sites (+ one doc paragraph), measured disjoint from its hunks (:137,:1085,:1118,:1123vsreconcileat ~:1230+) and declared as a claim amendment on the card (14484#issuecomment-5515460939) before the edit; its residualsys_business_unit_memberis unadjudicated inPLATFORM_OBJECT_TENANCY, so seed-replayed and system-written membership rows land organization-less #14570 is untouched. ⛔ Nocontent/docs/releases/**, noskills/**, nodocs/adr/**.RecordShare(spec contract) should surfaceorganization_idto cross-package callers — the column is on the row and readable through the engine; the contract projection is a separate decision. Also#11611 remains open— the general "platform tables and organization_id" question this card is one instance of.Post-open sync (coordinator readings, 2026-09-02)
origin/mainmerged again (13 commits,809d41704) throughscripts/pm/os-regen-merge.sh. Its step 3 refused the ordinary commit right after the merge — the designed collection point — becausecontent/docs/permissions/system-context.mdxwas deferred by theos-regendriver; the whole chain was then re-derived on the merged tree:pnpm gen:system-context-census(8 re-anchors,--fixrefused zero files),node scripts/tenant-audit-census.mjs --write, andnode scripts/check-engine-double-contract.mjs --writefor the three-way-contendedscripts/engine-double-contract.pinned.json(0 added or grown, 0 lost — never hand-edited). All three gates green on the result:check-system-context-census: OK — 109 elevation read sites … all anchored,check-tenant-audit-census: OK -- 218 write call sites certified,check-engine-double-contractexit 0. Head is now1ad15772f.Fence readings honoured.
sharing-rule-service.tsis co-held by PR #14572: my hunks sit at :1014 / :1267 / :1284 / :1340 / :1357 (origin/main numbering), theirs at :136 / :1085 / :1118 / :1123 (+49); nearest pair 71 lines apart, regions disjoint, and nothing here needs the :1085–:1172 band — if it ever did, that would be a stop-and-report, not a merge.Docs drift — re-derived, triaged, zero edits
node scripts/docs-audit/affected-docs.mjs --json origin/mainon the merged head lists 30 pages = 25 hand-written + 5 release-owned (60 anchors: 68 symbol, 2 route, 3 sdk, 8 literal; 6 test files excluded). Criterion applied: does this change make a sentence on the page false? — this card changes the tenant attribution ofsys_record_sharerows (who sees a grant under a wall, whether a grant row carries an organization, what legacy rows look like after the backfill). Only pages that state those facts needed reading.client-sdk,data-api,error-catalog,wire-format,webhooks,architecture,fields,import-mappings,metadata-service,storage-service,audit-service,authentication,record-view-auditing,http-protocol,i18n-standard,realtime-protocol,knowledge,state-machine,ui/pages— anchored ondryRun/pageSize/maxRows(fields of the new backfill options that happen to share names with the import route's parameters),object_name/record_id(the pointer-pair columns), orgetSchema. Agrepfor record-share statements returns nothing on any of them; a secondary sweep for "sharing × organization/tenant" on one line hit onlywebhooks.mdx:324(payload scrubbing, unrelated).sys_record_share(6):data-modeling/objects.mdx:280(engine-owned list — "written only by their owning service under a system context": still true, the writer still writes under a system context, now carryingtenantId);permissions/permissions-matrix.mdx:159,342(manual sharing =source: 'manual'rows: still true);permissions/sharing-rules.mdx:137,183,207(rule materialisessource: 'rule'rows keyed bysource_id: still true; its organization prose is about rule reads, OWD and the Layer 0 wall, none of it about grant rows);plugins/packages.mdx:102,379(ownership lists);protocol/objectql/security.mdx:420,442(enforcement status, share-manager);permissions/system-context.mdx:137(row 36: the read surface is not self-scoped — the reads this card leaves to measure: census the dependents of the SQL driver's orWhereNull tenant-wall carve-out before deciding its future (NULL org_id rows are globally visible on shared-DB walled deployments) #13564; the page is generated-anchored and never hand-edited). No sentence on any of them is falsified: none states whether a grant row carries an organization or what a tenant-scoped read of the table returns.isSystem写入是否在租户审计控制范围内?——#13178 类级装置(A/B/C)的共同前置,从未被裁过 #13491 admitted set (the one sentence class the ledger row would falsify) —grepovercontent/docsminusreferences/andreleases/forPLATFORM_OBJECT_TENANCY/platform-object-tenancy/sys_notification_delivery/unclassifiedfinds no enumeration (the twounclassifiedhits are an "unclassified surface" CI note inauthorization.mdxand unrelated prose inrealtime-protocol.mdx).v12(no record-share mention),v14:52(therecipient_typerename),v15:1003(seed-loader history),v16:252(a list),v17:54,1478,2029,3609,4291(read-surface self-scoping, positions). None states grant-row tenancy, so this change introduces no factual error on them — nothing for a docs-only card.Merge round (2026-09-03)
Dequeued at 01:16:56Z with
MERGE_CONFLICT. No behaviour changed in this round: the branch's own source delta against its merge base is byte-identical before and after it (git diff BASE HEAD -- packages/hashes toa07d7a95260dd4de9d23c681a7c2042f00d2477bon both sides), and the branch's twoengine-double-contractpin rows survive verbatim.What conflicted, and why none of it is resolvable by hand.
origin/mainhad moved todee4dd4ba— PR #14718 landedplugin-security/src/claim-seed-ownership.ts, PR #14712 added asuspended-run-storedelete site. Two generated census artefacts collided:content/docs/permissions/tenant-audit-census.mdxdocs/audits/2026-08-tenant-audit-write-call-sites.counts.mdBoth conflicts fell entirely inside the deliberately-unenforced corpus-scale block and its dated marker. The load-bearing part merged cleanly and wrong, exactly as that ledger's own header warns it would: both sides' rows landed (main's
suspended-run-store.tsdeleteat count 3, this branch'sbackfill-sys-record-share-organizations.tsupdaterow) while the totals block still read218. A third artefact,content/docs/permissions/system-context.mdx, is routed tomerge=os-regen; the driver merged it with exit 0 while silently keeping one side.Resolved by regeneration, never by hand, in the order the runbook fixes:
git merge origin/main. The two conflicts were resolved by taking main's side wholesale — the content was about to be overwritten, so the side is irrelevant — andgit restore --source=origin/main(worktree only) took main's side of the one os-regen path both sides had moved.98b1cf0b7. Theos-regenpre-commit hook recorded the deferral rather than passing it:os-regen: this is a MERGE commit — the 1 stale artifact(s) above are DEFERRED to the next commit, not passed.node scripts/tenant-audit-census.mjs --write(it owns both tenant-audit artefacts) andpnpm gen:system-context-census.scripts/engine-double-contract.pinned.jsonwas re-checked and needed no regeneration.8344d31b5, which discharged the deferral:os-regen: deferred regeneration discharged — all artifacts current, marker cleared.What the generator did to each file. The census moved to the joint population —
218 → 219write call sites (147statically decidable,147against a tenancy-enabled object,135threading a tenant context,101decidably elevated) — keeping both sides' rows rather than either side's totals. The nine hand-written prose figures that check B holds to the census followed it; that check has no repair arm by design, and the page says so.system-context.mdxhad 8 anchors re-derived onto the merged tree's real line numbers inplugin-sharing/src/sharing-service.ts::654→:677,:920→:943,:1007→:1030,:1208→:1238,:1286→:1476,:1311→:1501,:1338→:1528,:1597→:1787. That is the driver's silent drop being repaired, and it is visible only because the regeneration ran on a committed merge.Gate verdicts on the merged tree (every exit code captured before any pipe):
check-tenant-audit-census: OK -- 219 write call sites certified (147 decidable; 9 tenancy-enabled sites PROVABLY carry no tenant context, 32 more unreadable), 23 prose figures held to the census.check-tenant-audit-census self-test: 19 cases passandtenant-audit-census self-test: 18 cases passcheck-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.(self-test:all cases passed)check-engine-double-contract: OK — 761 pinned, 133 in the DEBT ledger, 3 exempt.pnpm --filter @objectstack/plugin-sharing test→Test Files 32 passed (32)·Tests 769 passed (769)8344d31b5:dispatch-gates --commands: 70 command(s)→ 64 green, 6 NOT MEASURED, zero findings. All six are prerequisite refusals by their own verdict text:check-engine-split-ratio(exit 2, shallow clone inside the 90-day window),check-test-completeness(exit 3, needs a saved CI test log),check:skill-examples(exit 1,packages/client-react/distunbuilt),check:dual-build-cjs-loads(exit 3,PREREQUISITE NOT MET),check:i18n(exit 1,Nothing was checked),check:type-check-debt(exit 3, closure unbuilt). All six run green in CI on a full build.Mergeability confirmed against a freshly fetched
origin/main(89a156af8, two commits newer than the one merged):git merge-tree --write-tree origin/main HEADexits 0 with zeroCONFLICTlines. Those two newer commits leave both census artefacts untouched onmain, so the population does not move again.Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
🤖 Generated with Claude Code
https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
Generated by Claude Code