Repository navigation
fix(organizations): the organization seed ownership claim writes with skipAutomations — no app hooks or record-change flows for a bookkeeping write - #23053
Conversation
… skipAutomations
claimOrgSeedOwnership re-owns an organization's owner-less seeded rows to
its owner. Its per-row write now runs under
{ isSystem: true, skipAutomations: true } (CLAIM_WRITE_CTX), the context
its single-tenant twin claimSeedOwnership already uses: no metadata-bound
hook fires and no record-change flow is dispatched for the bookkeeping
write, while code-registered hooks (audit, capability gates, sharing
projection) still run on every claimed row. The per-object read stays a
plain system read.
Adds the real-engine dispatch pin (ObjectQL over SqliteWasmDriver) and a
context assertion in the unit test.
Claude-Session: https://claude.ai/code/session_01CeRCvvvjxbxu2iYJAc5Cce
Co-authored-by: Claude <noreply@anthropic.com>
…g-seed-claim-skip-automations
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 7 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 b1c9b1252362c184cb597414edd369b7f1bc613d && git checkout b1c9b1252362c184cb597414edd369b7f1bc613d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin df7a137923be503bf6fea15a6fc944d3ba06df6f a1a4f4025bd6f2c46fee37bcd3e86c739196b1b7 && git checkout -B drift-repro df7a137923be503bf6fea15a6fc944d3ba06df6f && git merge --no-ff a1a4f4025bd6f2c46fee37bcd3e86c739196b1b7
node scripts/docs-audit/affected-docs.mjs --json df7a137923be503bf6fea15a6fc944d3ba06df6f |
Fixes #23008
Clause-②: no
What changed
claimOrgSeedOwnership(@objectstack/organizations) hands an organization's owner-less seeded rows to its owner. Its per-row write now runs under{ isSystem: true, skipAutomations: true }(CLAIM_WRITE_CTX) instead of a bare{ isSystem: true }, the context its single-tenant twinclaimSeedOwnershipalready uses. The card carries no maintainer ruling; the binding direction is triage's grade on the card (comment 6123012416), verbatim:No metadata-bound hook runs on the claim's owner change and no record-change flow is dispatched for it, so no approval opens and no notification goes out for the bookkeeping write. Code-registered hooks (the audit writer, capability gates, the sharing projection) still run on every claimed row. The per-object read stays a plain
{ isSystem: true }read; see "The read" below.packages/plugins/organizations/src/claim-org-seed-ownership.tsCLAIM_WRITE_CTXbesideSYSTEM_CTX, used by the update only; the header says why, and which hooks still runpackages/plugins/organizations/src/claim-org-seed-ownership.test.tsupdatecall carries exactly{ isSystem: true, skipAutomations: true }, everyfindexactly{ isSystem: true }packages/plugins/organizations/src/claim-org-seed-ownership-dispatch.pin.test.ts(new)ObjectQLoverSqliteWasmDriver(the driverobjectstack devuses): what the flag reaches.changeset/23008-org-seed-claim-skip-automations.mdpatchfor@objectstack/organizationsNo
package.jsonor lockfile change:@objectstack/objectqland@objectstack/driver-sqlite-wasmwere already devDependencies of the package.What the pin asserts, on the real engine
bindHooksToEngine, which stampsentry.meta) onbeforeUpdate/afterUpdatesees 0 write dispatches during the claim.registerHook, no metadata) fires once per claimed row and phase. Ids are compared, not only counted: 12 rows, 2 phases.isSystem: true,skipAutomations: trueandsession.skipTriggers: true, the fieldRecordChangeTriggerreads before it dispatches a flow.There are two positive controls on the same engine. During the claim, the same metadata hook, also bound on
beforeFind/afterFind, sees the claim's read (exactly onebeforeFindand oneafterFind), so it is live while the write events stay silent. After the claim, the claim's own by-id write shape under a bare{ isSystem: true }fires the metadata hook 2N times, and the code hook sees no skip flag. An owned row and another organization's owner-less row stay untouched. A second case pins that a re-run claims nothing and dispatches no write hook of either kind.Ablation: drop
skipAutomations, the pin goes redThe change was committed first (
04d20c038), and the mutation was made from that state withnode scripts/ablation-replace.mjsin wrap mode under the verify lock, with an outertraprestore on the absolute path. Both test files import their subject as./claim-org-seed-ownership.js, which issrc/, so nodist/sits in the resolution path.const CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true };went from 1 to 0, the replacementconst CLAIM_WRITE_CTX = { isSystem: true };from 0 to 1, and the blob from8474e3b66319to814e30575673.Tests 2 failed | 6 passed (8). The pin failed on half 1:The unit test's context assertion failed as well.
8474e3b66319, equal to HEAD, andgit diff HEADis empty. The restored run:Test Files 2 passed (2),Tests 8 passed (8).The read: measured, then chosen
finddispatchesbeforeFind/afterFindthroughtriggerHooks, so a metadata-bound find hook does fire on anisSystem-only read. The pin observes it on the real engine: one of each per claimed object.beforeFindhook is an app's read-filter seam, and switching it off would change which rows the claim selects relative to every other system read of that object. No consumer was measured either way:examples/andpackages/qaat9f4b652e3declare 0 metadata-bound find hooks (grep forbeforeFindandafterFind: 0 hits).Verification
origin/mainwas merged once (a1a4f4025). The merge brought 4 paths: two ADRs and twoscripts/adr-anchors/*.json, with no package source.pnpm --filter @objectstack/organizations testgaveTest Files 13 passed (13),Tests 155 passed (155).pnpm --filter @objectstack/organizations typecheckpassed:tsc --noEmit, thencheck:test-typecheck: OK, 0 file(s), 0 error(s). Both ran at04d20c038; the merge touched no package file.a1a4f4025:node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 64 commands over the 4 changed paths. That is the dispatch's 51 plus the changeset families:check-adr-0087-registration,check-changeset-no-majorandcheck-empty-changeset(each with its self-test),check:changeset-gate-self-tests,check:objectui-changeset,check:pm-changeset-deadline-census, and therelease-rehearsal-cloneandrelease-pending-publishself-tests.check:dual-build-cjs-loadsfirst answeredPREREQUISITE NOT MET(exit 3: the tree had no full build).check:type-check-debt --re-measurethen built the closure, the gate was re-run, and it exited 0.check:dts-closureandcheck:sourcemap-no-sources-contentwere re-run after that build so they cover this package: 72 and 69 built packages swept, against 48 the first time.--ranwith exit codes:Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, and the zero is derived from the recorded codes.eslint --no-inline-config --format jsonover the 3 changed TypeScript files ata1a4f4025gave 3 files, 0 errors, 0 warnings.eslint.config.mjs'spackages/**/*.{ts,tsx,mts,cts}object, and none was ignored (an ignored file passed explicitly reports a warning).parserOptions.project, noprojectService), so this diff cannot move an untouched file's verdict.pnpm lintis CI's.Acceptance notes
count: 10000: 10,000 owned and 500 unowned, in one 5.0 s pass. The production caller reaches the claim throughcreateEnsureDefaultOrganizationOnce, the ADR-0093 D7 once-gate. Once the owner bind is decided, the bootstrap "binds nobody and hands over no seed ownership", so the remainder stays unowned through that caller. It goes to the seat as its own card, naming the paged predicate write the twin adopted (claimSeedOwnership writes up to 20k single-id system updates in a loop, so per-record sharing materialisation cannot batch them #14530).examples/app-showcasedeclaresrecord-after-updateflows onshowcase_taskandshowcase_project; whether any of them matched the claim's owner change was not measured.packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.tsfor the single-tenant claim. This PR's dispatch evidence is the real-engine pin, within the file surface of the dispatch.Generated by Claude Code