Skip to content

fix(organizations): the organization seed ownership claim writes with skipAutomations — no app hooks or record-change flows for a bookkeeping write - #23053

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23008-org-seed-claim-skip-automations
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-23008-org-seed-claim-skip-automations

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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 twin claimSeedOwnership already uses. The card carries no maintainer ruling; the binding direction is triage's grade on the card (comment 6123012416), verbatim:

claimOrgSeedOwnership writes with { isSystem: true, skipAutomations: true }, with #22069's pin: a metadata-bound hook does not run, and a code-registered hook (audit, sharing projection) still does.

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.

file change
packages/plugins/organizations/src/claim-org-seed-ownership.ts CLAIM_WRITE_CTX beside SYSTEM_CTX, used by the update only; the header says why, and which hooks still run
packages/plugins/organizations/src/claim-org-seed-ownership.test.ts new case: every update call carries exactly { isSystem: true, skipAutomations: true }, every find exactly { isSystem: true }
packages/plugins/organizations/src/claim-org-seed-ownership-dispatch.pin.test.ts (new) real ObjectQL over SqliteWasmDriver (the driver objectstack dev uses): what the flag reaches
.changeset/23008-org-seed-claim-skip-automations.md patch for @objectstack/organizations

No package.json or lockfile change: @objectstack/objectql and @objectstack/driver-sqlite-wasm were already devDependencies of the package.

What the pin asserts, on the real engine

  1. The metadata-hook filter. A hook bound through the real binder (bindHooksToEngine, which stamps entry.meta) on beforeUpdate/afterUpdate sees 0 write dispatches during the claim.
  2. The per-row hook path. A code-registered hook (registerHook, no metadata) fires once per claimed row and phase. Ids are compared, not only counted: 12 rows, 2 phases.
  3. The record-change trigger's reading point. Every one of those contexts carries isSystem: true, skipAutomations: true and session.skipTriggers: true, the field RecordChangeTrigger reads 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 one beforeFind and one afterFind), 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 red

The change was committed first (04d20c038), and the mutation was made from that state with node scripts/ablation-replace.mjs in wrap mode under the verify lock, with an outer trap restore on the absolute path. Both test files import their subject as ./claim-org-seed-ownership.js, which is src/, so no dist/ sits in the resolution path.

  • Mutation landed: anchor const CLAIM_WRITE_CTX = { isSystem: true, skipAutomations: true }; went from 1 to 0, the replacement const CLAIM_WRITE_CTX = { isSystem: true }; from 0 to 1, and the blob from 8474e3b66319 to 814e30575673.
  • Mutated run: Tests 2 failed | 6 passed (8). The pin failed on half 1:
AssertionError: a metadata-bound hook fired for the claim write: expected [ …(24) ] to deeply equal []
+   { "event": "beforeUpdate", "id": "d0", "isSystem": true, "skipAutomations": undefined, "skipTriggers": undefined },

The unit test's context assertion failed as well.

  • Restore proven: the blob after restore is 8474e3b66319, equal to HEAD, and git diff HEAD is empty. The restored run: Test Files 2 passed (2), Tests 8 passed (8).

The read: measured, then chosen

  • Measured. find dispatches beforeFind/afterFind through triggerHooks, so a metadata-bound find hook does fire on an isSystem-only read. The pin observes it on the real engine: one of each per claimed object.
  • Chosen. Keep the read plain, as the twin does. A read starts no record-change flow; the approvals and emails this card is about come from write hooks and flows. A metadata-bound beforeFind hook 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/ and packages/qa at 9f4b652e3 declare 0 metadata-bound find hooks (grep for beforeFind and afterFind: 0 hits).

Verification

origin/main was merged once (a1a4f4025). The merge brought 4 paths: two ADRs and two scripts/adr-anchors/*.json, with no package source.

  • Tests: pnpm --filter @objectstack/organizations test gave Test Files 13 passed (13), Tests 155 passed (155). pnpm --filter @objectstack/organizations typecheck passed: tsc --noEmit, then check:test-typecheck: OK, 0 file(s), 0 error(s). Both ran at 04d20c038; the merge touched no package file.
  • Gates at a1a4f4025: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 64 commands over the 4 changed paths. That is the dispatch's 51 plus the changeset families: check-adr-0087-registration, check-changeset-no-major and check-empty-changeset (each with its self-test), check:changeset-gate-self-tests, check:objectui-changeset, check:pm-changeset-deadline-census, and the release-rehearsal-clone and release-pending-publish self-tests.
    • All 64 ran and every one exited 0.
    • check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (exit 3: the tree had no full build). check:type-check-debt --re-measure then built the closure, the gate was re-run, and it exited 0.
    • check:dts-closure and check:sourcemap-no-sources-content were re-run after that build so they cover this package: 72 and 69 built packages swept, against 48 the first time.
    • --ran with exit codes: Run reconciliation — 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN, and the zero is derived from the recorded codes.
  • Lint, a declared narrowing: eslint --no-inline-config --format json over the 3 changed TypeScript files at a1a4f4025 gave 3 files, 0 errors, 0 warnings.
    • Population: all three match eslint.config.mjs's packages/**/*.{ts,tsx,mts,cts} object, and none was ignored (an ignored file passed explicitly reports a warning).
    • Invariance: the config enables no type-aware linting (no parserOptions.project, no projectService), so this diff cannot move an untouched file's verdict.
    • The repo-wide pnpm lint is CI's.

Acceptance notes

  • The 10,000-row read limit: measured, reproduces, out of this PR (triage's direction). An untracked scratch test, since deleted, ran the real engine with 10,500 owner-less rows of one object in one organization. The claim returned count: 10000: 10,000 owned and 500 unowned, in one 5.0 s pass. The production caller reaches the claim through createEnsureDefaultOrganizationOnce, 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).
  • The changeset does not say that no example app is affected. examples/app-showcase declares record-after-update flows on showcase_task and showcase_project; whether any of them matched the claim's owner change was not measured.
  • No booted-app (dogfood) counterpart is added. The twin has packages/qa/dogfood/test/seed-ownership-claim-dispatch.dogfood.test.ts for the single-tenant claim. This PR's dispatch evidence is the real-engine pin, within the file surface of the dispatch.
  • Cloud's adapter in objectstack-ai/cloud#2794, which adds the flag before calling this function, can retire with cloud's next framework pin.

Generated by Claude Code

… 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>
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

2 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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 7 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 df7a137923be503bf6fea15a6fc944d3ba06df6f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from b1c9b1252362c184cb597414edd369b7f1bc613d — the merge of head a1a4f4025bd6f2c46fee37bcd3e86c739196b1b7 into base df7a137923be503bf6fea15a6fc944d3ba06df6f, 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 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

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

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/m tests tooling

Projects

None yet

2 participants