Skip to content

fix(plugin-security,organizations): the seed-ownership claims re-own only the rows the seed landed on (ADR-0138 D1) - #23605

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23354-seed-claim-seed-rows
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-23354-seed-claim-seed-rows

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23354

Clause-②: yes

What this changes

The seed-ownership claim hands seeded business records to the platform admin. It runs on every app:seeded, so on every boot of an app with seed data. Its NULL-owner arm re-owned every unowned row of every eligible object. No stored column tells a seed row from a guest-written row or a public form's intake, and ADR-0138 D1 says those keep no owner ("the owner stays unset"). So the claim handed them to the platform admin at the next boot.

This takes triage's route, option A (6182011415). The seed loader already records the row every record landed on. It now publishes those rows, and both claims re-own only rows that are both unowned and seeded.

@objectstack/spec (minor)

  • SeedLoadResultSchema gains landedIds (z.array(z.string()).default([]), described). It holds the ids of the stored rows a dataset's records landed on: a row a record inserted, a row it updated, and a row its key matched on a replay and left as it was. Each id appears once, in record order. A dry run reports none. gen:schema and gen:docs were regenerated: authorable-surface/data.json, authorable-defaults/data.json and content/docs/references/data/seed-loader.mdx.
  • ISeedSettlementService gains seededRecordIds?(object, organizationId?).
    • Without organizationId, it answers for this process's boot-time seed sources. A source publishes its rows before it settles, so a reader inside app:seeded sees them.
    • With organizationId, it answers for that organization's per-organization replay. The host keeps the most recent replay only (see open question 1).
    • It is optional: a host that publishes no reading leaves the reader with no way to place an unowned row, and the reader then re-owns no NULL-owner row.

@objectstack/metadata-protocol (minor)

SeedLoaderService.loadDataset returns landedIds from internalIdByRecordIndex, the map every write site already fills through noteLanded. That covers the insert flush, the self-reference write, the update path, the replay skip path and a refused update of a matched row. No resolution rule is copied anywhere.

@objectstack/runtime (minor)

  • The tracker (seed-settlement.ts) implements seededRecordIds. SeedSourceHandle gains publishLanded(report), and the new module function recordOrganizationReplay(ctx, organizationId, report) records a per-organization replay. That function is not re-exported from the package entry.
  • AppPlugin publishes the inline seed's result.results before seedSource.settle(). The basic-insert fallback now keeps the ids of the rows it inserts and publishes them too. The per-organization seed-replayer records each replay under its organization.

@objectstack/plugin-security (patch)

  • The NULL arm. claimSeedOwnership builds it from options.seededRecordIds as owner_id IS NULL AND id IN (chunk), one write per chunk of CLAIM_PAGE_ROWS ids, which is half the per-row hook ceiling. A chunk is never refused for its size and stays inside a driver's parameter limit.
  • The usr_system arm is unchanged. No guest door, form door or runtime writer stamps that owner (git grep SystemUserId.SYSTEM), so only seed or legacy rows carry it.
  • No reading. Without seededRecordIds the NULL arm re-owns nothing, and the pass report appends a sentence that says so.
  • Callers. claimSeedOwnershipOnSettle and runBootstrap (through bootstrapPlatformAdmin's new seededRecordIds option) read the reading through the contract, with readSeededRecordIds. The promotion pass runs in the same process as the boot's settle. A seed that settles after promotion (over budget) is claimed by the app:seeded re-run, as before.
  • Logging. The log meta reports an id IN list by its size, never inlined.
  • The carrier note 6158485388 is applied: scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin.ts.json gains ADR-0138 with its D1 invariant.

@objectstack/organizations (patch)

claimOrgSeedOwnership takes the same selector. OrganizationsPlugin binds seededRecordIds(object, organizationId) for the organization the owner bind claims for. That organization's replay ran inside the sys_organization insert of the same call, through Middleware B.

Each write carries at most CLAIM_CHUNK_IDS ids, which is half the ceiling. So no write can be refused for its size, and the page-read fallback from #23054 had nothing left to answer. It is removed rather than kept as unreachable code. The #23054 guarantee holds: every seeded row is claimed, however many there are, because every chunk is written.

The deprecated ensureDefaultOrganization wrapper holds no kernel. So it has no reading and hands over no row, and its docblock says so.

Where the landing went beyond the claim's file surface

Each of these was measured as necessary:

  • packages/runtime/src/app-plugin.ts: the replayer closure and the basic-insert fallback, beside the settle hand-off. The organizations twin needs the replay's rows, which the boot settle does not carry.
  • packages/plugins/organizations/src/organizations-plugin.ts (the claim wiring) and ensure-default-organization.ts (its docblock only).
  • scripts/engine-double-contract.pinned.json: five rows recorded by --write for the two new test files' engine doubles.

Pins

pin where red leg
(a) a seed row is still claimed, on the first boot and on a later boot's replay claim-seed-ownership-seed-settle-rerun.test.ts (two boots, real SecurityPlugin); claim-seed-ownership-warm-boot.test.ts (real ObjectQL over SqlDriver, one SQLite file, two processes) positive control
(b) a guest-written row and a public-form row keep no owner across two boots the same two files, plus claim-seed-ownership.test.ts, the org twin's unit test and both real-engine dispatch pins the only pin with a red leg under the old predicate
(c) a row a hook, flow or assignment rule owned keeps its owner the same files preservation pin: the old predicate never matched such an owner either (the earlier dev's note in 6181198317)

Producer side:

  • seed-loader-landed-ids.test.ts, real loader: the insert path; a replay on the skip path and on the update path; a dataset keyed on a reference reporting the matched row whose stored key is the resolved id; an unseeded row never reported; a refused insert; two records resolving to one row; a dry run.
  • seed-settlement.test.ts: the tracker.
  • app-plugin.seed-landed.test.ts: on the loader path and on the fallback, the claim inside app:seeded reads exactly the seed's rows; a second boot re-reports the first boot's rows and not a guest row; the replayer records under its organization.

Measured: what the id IN write costs (InMemoryDriver)

The claim's docblock records that an id IN predicate is quadratic on InMemoryDriver, where mingo hashes the list again for each row. The NULL arm now pays that shape on purpose, so it was measured.

Rig: a real ObjectQL over InMemoryDriver, one object, one write, two runs each. The scratch rig was deleted after the reading.

rows first claim, old owner_id IS NULL first claim, new owner_id IS NULL AND id IN (all) claimSeedOwnership with the reading warm boot (all owned), old / new
1 000 53 / 17 ms 69 / 62 ms 61 / 66 ms 2 / 1 ms vs 2 / 1 ms
5 000 297 / 435 ms 2 587 / 2 650 ms 2 431 / 3 035 ms 2 / 3 ms vs 5 / 4 ms

So the cost is paid once, on the pass that claims the rows, and only on the in-memory driver. On a warm boot owner_id IS NULL fails first and the IN list is never consulted. SQL drivers were not measured here.

Outside the claim's purpose (named, as triage asked)

The claim's purpose is handing the boot seed to the first admin. These seed writers emit no app:seeded, publish no rows, and are no longer re-owned by a later boot's pass:

  • package-install seed apply (runtime/src/domains/packages.ts);
  • publish-time seed apply (metadata-protocol/src/protocol.ts, untouched per the dispatch);
  • the dev hot-reload seeder (app-plugin.ts);
  • the marketplace install's inline seed (cloud-connection).

A dataset they share with the boot bundle is re-reported by the next boot's replay and claimed then.

Open questions for the contract review

  1. The per-organization reading keeps the most recent replay only. An organization is replayed once, when it is created. A process that keeps every replay's rows would grow with every sign-up, and the one reader, the default organization's owner bind, asks in the same call that created the organization. The cost: if another organization's replay lands between the default organization's replay and its bind, the bind reads nothing. It then hands over no row and logs it. The alternative is an unbounded per-organization map.
  2. The owner bind covers only a replay that ran in this process. An organization that existed before this process has no reading, so a bind retried on a later boot hands over nothing. Before this change it handed over every NULL-owner row of the organization, guests' included.
  3. The fallback replayer records nothing. cloud-connection registers a fallback seed-replayer only when no AppPlugin registered one, and it does not record replays. On that composition the owner bind finds no reading and hands over nothing.

Acceptance notes

  • Reading ignore rows. A seed in ignore mode reports a row its key matched without writing it. So a pre-existing row with the same natural key counts as the seed's. That follows the loader's matching rule, not this change.
  • InMemoryDriver's $in is quadratic. It is a property of that driver, recorded in the claim docblock with the measurement above. Carrier: none.
  • Behaviour change for direct callers. claimSeedOwnership and claimOrgSeedOwnership are exported. Without seededRecordIds they no longer re-own NULL-owner rows. The changeset states it.

Tests and gates

Head 70a9ae600. Every run below went through scripts/pm/os-verify-lock.sh, and its VERDICT command-exit line is quoted.

Merge. origin/main 32e03d2fd was merged in (090a6a0ef, clean). Then the closure was rebuilt (turbo run build of organizations…, plugin-security…, runtime…, cli…, cloud-connection…, platform-objects…: 62 tasks, exit 0), and pnpm --filter @objectstack/spec check:generated exited 0 on the merged tree.

Typecheck: exit 0 for all 8 packages. spec, metadata-protocol, runtime, plugin-security, organizations, cli, cloud-connection, platform-objects. runtime and organizations were run again at 70a9ae600, exit 0.

Whole package suites:

package files tests
metadata-protocol 237 passed 28420 passed
plugin-security 185 passed 3697 passed
organizations (at 70a9ae600) 13 passed 161 passed
platform-objects 70 passed 1091 passed

Targeted, at 70a9ae600. runtime: app-plugin.seed-landed.test.ts, seed-settlement.test.ts and app-plugin.seed.test.ts (3 files, 34 tests passed).

Declared narrowing, read from this PR's CI. These suites were not run locally: runtime's and spec's whole suites, cli (--project unit), cloud-connection, and any test:repo project. The reason is lock pressure on the shared box (PM notes, 03:11Z and 03:40Z). cli and cloud-connection import the changed types only, and both typecheck clean.

Gates. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 125 families, each run with its exit code captured before any pipe. Reconciliation with --ran: 125 accounted for, 121 run, 4 NOT MEASURED, 0 UNRUN.

Two gates were red on the first pass and are green at 70a9ae600:

  • check:test-source-alias: organizations now aliases @objectstack/spec/contracts to source.
  • check:where-matcher: the new runtime test double's matcher sits at module scope and refuses operators it does not implement.

check:engine-double-contract recorded the new doubles (--write, 5 rows).

NOT MEASURED, each named:

  • check-plugin-teardown-shape --self-test: exit 3. A shallow clone cannot reach the fixture commit.
  • spec check:skill-examples: exit 3. It needs a client-react build.
  • check:dual-build-cjs-loads: exit 3. It needs every package built.
  • check:type-check-debt: stopped. Its re-measure builds the whole tree under the shared lock, and it was withdrawn on the PM lock-pressure note.

All four run in CI.

Ablation. Not run as a separate step. Pin (b) is red by construction against the old predicate: the unit double, the real-engine pins and the warm-boot file each assert that an unreported NULL-owner row keeps no owner, and the old owner_id IS NULL write would re-own it.


Generated by Claude Code

…s re-own only the rows the seed landed on

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
… seeded-row readings

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
…s its seed replay reported

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
…double ledger rows

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
…c/contracts to source

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/metadata-protocol, @objectstack/organizations, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec, touching 35 documentable anchor(s). ⚠️ 4 changed file(s) yielded no anchor (packages/plugins/organizations/src/ensure-default-organization.ts, packages/plugins/organizations/vitest.config.ts, packages/spec/authorable-defaults/data.json, …), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/permissions/field-level-security.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/index.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/permissions/tenant-audit-census.mdx (via SYSTEM_CTX (symbol, a top-level const object))
  • content/docs/plugins/packages.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/ui/forms.mdx (via SecurityPlugin (symbol, a top-level class))

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

  • content/docs/releases/implementation-status.mdx (via SecurityPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-0.mdx (via SYSTEM_CTX (symbol, a top-level const object))

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
  • 4 changed file(s) yielded no anchor (packages/plugins/organizations/src/ensure-default-organization.ts, packages/plugins/organizations/vitest.config.ts, packages/spec/authorable-defaults/data.json, …) — pages documenting those are invisible to this run
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 151 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 8d05456cbe03667ea2352c9a99e67d4a862a8294 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8d05456cbe03667ea2352c9a99e67d4a862a8294

⚠️ 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 8d05456cbe03667ea2352c9a99e67d4a862a8294 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT · domain:spec seat 2 (#18549) · session session_01LFqdRsuM5nkqoJdHd5ztVk · 2026-10-15T04:01Z

This is the seat review of record for #23354 (claim 6182191596, dev report 6184063982) on head:

70a9ae6008bb5dd324dd6acb9f20e28cf7ff5529

It is not the contract review. That review is owed at CONTRACT_REVIEW_TIER and runs next.

  1. Form.
  2. Premises measured. The dev confirmed hypotheses 1–3. Hypothesis 4 was wrong, and the dev corrected it: the organizations twin needs the per-organization replay's rows, which the boot settle does not carry. So the replayer in app-plugin.ts records them. The correction stands.
  3. Gates.
    • Check-runs on this head at this read: 38 in all, 17 success, 5 skipped, 16 in progress, none red.
    • Locally the dev ran 121 of the 125 derived families. The other 4 are NOT MEASURED, each with a reason.
    • Ready and enqueue wait for every check to be green.
  4. Accepted deviations.
    • The file surface went beyond the claim. Each step was measured and is named in the PR body:
      • the per-organization replayer and the basic-insert fallback in app-plugin.ts;
      • the binding in organizations-plugin.ts;
      • a docblock in ensure-default-organization.ts;
      • one anchored alias in organizations/vitest.config.ts;
      • rows in engine-double-contract.pinned.json.
    • This record amends the claim's surface to the PR's file list.
    • The twin's paging fallback is removed. The removed symbols (claimUnowned, isPerRowHookBudgetRefusal, idsFrom, MAX_CLAIM_PAGES, the module's CLAIM_PAGE_ROWS) were all module-private. The exported claimOrgSeedOwnership stays.
    • Declared narrowing, per the seat's lock notes: every test:repo project and the whole runtime and spec suites are left to CI.
  5. Out-of-scope findings.
    • (1) cloud-connection's fallback seed-replayer records no replay rows. The carrier is this PR's contract review, as the dev's open question 2.
    • (2) InMemoryDriver's $in cost goes in the Acceptance notes. It is a driver property, recorded in the claim's docblock.
    • (3) An ignore-mode dataset reports a row its key matched. This goes in the Acceptance notes.
  6. Changeset sentences checked against the diff.
    • landedIds defaults to [] (seed-loader.zod.ts:423).
    • seededRecordIds?(object, organizationId?) is optional (contracts/seed-settlement.ts:137).
    • publishLanded is called at app-plugin.ts:1797, :1874 and :1894.
    • CLAIM_PAGE_ROWS = Math.floor(MAX_BULK_PER_ROW_HOOK_ROWS / 2).
    • bootstrapPlatformAdmin gains the seededRecordIds option (:246).
    • The bump levels (spec, metadata-protocol and runtime minor; plugin-security and organizations patch) are the contract review's ② to judge.
  7. Landing terms.
    • First a contract review PASS at CONTRACT_REVIEW_TIER on the head, then every check green, then the seat marks the PR ready and enables auto-merge.
    • needs:contract-review stays on this PR until then.
    • The dev's two open_questions go to the review's ③.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 70a9ae6008bb5dd324dd6acb9f20e28cf7ff5529
Local-runs: none

Inputs: card #23354 (body and all ten comments, rulings and earlier records included); PR #23605 (body, the 31-file list, the net diff against its merge base 32e03d2fd with main, which stood at 16abea88 at this read); the check-runs on this head, read at 2026-10-15T04:15Z. Nothing was built, run or re-run.

① Derived judgments

Face and tier. The file list carries a published schema (packages/spec/src/data/seed-loader.zod.ts) and a published contract (packages/spec/src/contracts/seed-settlement.ts), so the review is owed at CONTRACT_REVIEW_TIER. No governed path: scripts/adr-anchors/**, .changeset/** and scripts/engine-double-contract.pinned.json are not register rows. Head repo equals base repo.

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. SeedLoadResultSchema.landedIds (z.array(z.string()).default([]), described) — a widening: a result without the key still parses, one carrying it now parses. SeedLoadResult is the schema's z.input, so the TypeScript field is optional and no implementer's literal breaks. The generated outputs were regenerated (authorable-surface/data.json, authorable-defaults/data.json, content/docs/references/data/seed-loader.mdx) and Spec property liveness concluded success. RIGHT.
  2. ISeedSettlementService.seededRecordIds?(object, organizationId?) — an optional member: an existing implementer still satisfies the interface, a consumer gains one read. The two readings (boot-time sources without organizationId; the most recent per-organization replay with it) and the "absent means no reading, not an empty seed" rule are stated in the docblock. check:api-surface runs in Type Check · consumer gates (concluded success) and its baselines track export names (ISeedSettlementService (interface), SeedLoadResultSchema (const) both pre-exist), so no baseline regeneration was owed. RIGHT.
  3. @objectstack/runtime SeedSourceHandle — a package-entry type export — gains a REQUIRED publishLanded(report). The handle is produced only by declareSeedSource; consumers call it and nothing outside the runtime implements it, so this is judged right at minor. Named for the record: a hand-rolled value typed SeedSourceHandle outside this repo stops type-checking; an optional member was the strictly additive spelling and was not required. SeedLandingReport and recordOrganizationReplay are not re-exported from the entry, so no further surface is added. RIGHT.
  4. claimSeedOwnership NULL arm — owner_id IS NULL becomes owner_id IS NULL AND id IN (one chunk of the seeded ids), one write per chunk of CLAIM_PAGE_ROWS (half the per-row hook ceiling); the usr_system arm is unchanged, and the diff's premise holds (no guest door, form door or runtime writer stamps SystemUserId.SYSTEM; the dispatch pin claims SEED_IDENTITY rows). Without a reading the NULL arm re-owns nothing and the pass report says so, which is fail-closed toward ADR-0138 D1 and never the old predicate. The function is a package-entry export: its options widen (an optional field) and its default-path behaviour changes, which is the fix and is stated in the changeset under "Direct callers". RIGHT.
  5. bootstrapPlatformAdmin gains the optional seededRecordIds pass-through on a non-exported options interface; both callers (claimSeedOwnershipOnSettle and the kernel:ready promotion) bind the reading through the published contract (readSeededRecordIds), lazily per object, so an over-budget seed is covered by the app:seeded re-run as before. RIGHT.
  6. claimOrgSeedOwnership takes the same selector, bound per organization by OrganizationsPlugin from the same contract; the ql.find precondition is dropped (a widening of the engines it accepts); without a reading it returns nothing and says so once. The organizations: claimOrgSeedOwnership reads at most 10,000 owner-less ids per object and the once-gate never re-runs it, so rows past 10,000 stay unowned for good #23054 paging fallback is removed rather than kept: a chunk predicate matches at most as many rows as it carries ids, at most half the ceiling, so the refusal it answered cannot occur, and the 10 500-row guarantee is re-pinned on a real engine. The removed symbols (claimUnowned, isPerRowHookBudgetRefusal, idsFrom, MAX_CLAIM_PAGES, the module SYSTEM_CTX) were module-private. The deprecated ensureDefaultOrganization wrapper hands over no row; its docblock and the changeset say so. RIGHT.
  7. Producer. landedIds is read off internalIdByRecordIndex, the per-dataset map every write site already fills through noteLanded (insert flush, self-reference write, update, replay skip, refused update of a matched row), deduplicated in record order; a dry run reports none. Pinned on the real loader in seed-loader-landed-ids.test.ts (insert, skip replay, update replay, reference-keyed match reporting the resolved id, unseeded row never reported, refused insert, two records one row, dry run). RIGHT.
  8. Host. The inline seed publishes before settle() on the loader path and on both basic-insert fallbacks; the per-organization replayer records through recordOrganizationReplay. The tracker keeps bootLanded additive and lastReplay single. Pinned in seed-settlement.test.ts and app-plugin.seed-landed.test.ts (loader path, fallback, second-boot re-report excluding a guest row, per-organization record). RIGHT.
  9. ADR anchor. scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin.ts.json gains ADR-0138 and a D1 invariant sentence that matches the code it anchors (carrier note 6158485388). RIGHT.
  10. Logging. An id IN list is reported by its size (whereForLog), never inlined. RIGHT.
  11. Dispatch limits held. packages/metadata-protocol/src/protocol.ts untouched; the app:seeded payload in contracts/plugin-lifecycle-events.ts untouched. RIGHT.
  12. The three pins triage set (6158407363). (a) a seed row is claimed on the first boot and on a later boot's replay: claim-seed-ownership-seed-settle-rerun.test.ts through the real SecurityPlugin, claim-seed-ownership-warm-boot.test.ts over a real ObjectQL on SqlDriver across two processes. (b) a guest-written row and a public-form row keep no owner across two boots: the same two files, claim-seed-ownership.test.ts, the organizations unit test and both real-engine dispatch pins; this is the one pin with a red leg under the old predicate, as the first dev's note said. (c) a hook-, flow- or rule-owned row keeps its owner: a preservation pin in the same files. RIGHT.
  13. Measured cost. The NULL arm now pays InMemoryDriver's quadratic $in once, on the pass that claims the rows (measured 2.6 s at 5 000 rows), and nothing on a warm boot; SQL drivers were not measured and the docblock says so. Accepted as a driver property.

Check-runs on this head, 44 in all at the reading above: 31 success, 8 skipped (path-filtered or opt-in), 0 failure, 5 in progress. Concluded and judged as the gate verdicts: Lint & Repo Gates and its lanes (which carry check:test-source-alias, check:where-matcher, check:engine-double-contract and the anchor gates), Type Check · source gates, Type Check · debt ledger, Type Check · consumer gates, Type Check · workspace and the TypeScript Type Check aggregate, Spec property liveness, Check Changeset, Governed Surface Queue Guard, Build Core, Build Docs, Dogfood Regression Gate, Check Documentation Links, Test Core (5/6), and the card and claim guards. NOT MEASURED by this record: Test Core (1/6), (2/6), (3/6), (4/6) and (6/6), still running. Those five stand between this verdict and the landing: the seat's landing terms (6184105830 §7) and the Post-Task rule require every check green before the PR leaves draft or is queued, and this PASS substitutes for none of them.

② Semver level

  • Clause-②: yes sits at the start of a body line, with no arm — well-formed (at most one arm) and true to the diff: items 1 to 3 widen a published schema, a published contract and a runtime handle; nothing narrows an accept set; no export is removed or renamed, so no breaking changeset, no FROM → TO mapping and no ADR-0087 marker is owed. skip-changeset is not applied, correctly: five released packages publish.
  • .changeset/23354-seed-landed-rows.md — @objectstack/spec minor: RIGHT (items 1 and 2; yes takes at least minor). @objectstack/metadata-protocol minor: RIGHT (a new field on every per-dataset result; additive). @objectstack/runtime minor: RIGHT (item 3, a required member on a handle only the package constructs, plus the tracker's new read; nothing removed).
  • .changeset/23354-seed-claim-seed-rows.md — @objectstack/plugin-security patch: RIGHT (a bug fix in a released package; the option widens; the default-path change is stated). @objectstack/organizations patch: RIGHT (the same; the deprecated wrapper's change is stated; the module-private removals are not surface).
  • Both bodies carry what an upgrading reader greps for: the new names, the direct-caller behaviour, and the seed writers outside the claim's purpose.
  • Base drift: the branch merged main once at 32e03d2fd; CI ran on the merge of this head into 16abea88 (the drift check's tree line), with no conflict reported. The queue leg re-merges.

③ Boundary flags

Dev deviations (6184063982), each:

  1. File surface exceeded — app-plugin.ts beyond the settle hand-off (the per-organization replayer must record or the twin has no reading; the fallback must publish or its seed rows go unclaimed), organizations-plugin.ts (the binding), ensure-default-organization.ts (docblock), organizations/vitest.config.ts (an alias check:test-source-alias demanded), engine-double-contract.pinned.json (--write rows). ANSWERED: each is load-bearing or gate-demanded; the seat amended the surface to the file list (6184105830 §4); accepted.
  2. The twin's paging fallback removed, not adapted. ANSWERED: unreachable by construction (①.6); accepted.
  3. Lock-pressure narrowing — the whole runtime and spec suites, cli unit, cloud-connection and every test:repo project left to CI. ANSWERED: CI's Test Core shards are the gate; one of six has concluded success and five are in progress, so they are NOT MEASURED here; the landing waits on them.
  4. main merged once and moved since. ANSWERED: see ②; the queue re-merges.
  5. Model-free commit trailers. The AGENTS.md form; nothing to judge.

Open questions (the dev's two in 6184063982; the PR body's three):

  • Q1, the per-organization reading keeps the most recent replay only. ANSWERED, not escalated: accept A as built. The bound is stated in the contract docblock, the one reader (the default organization's owner bind) asks in the call that created the organization, and the failure mode when another replay lands in between is D1's empty state plus an info line, never a guest row handed over. Noted for a later card, not owed here: consuming the per-organization entry on read would close that window at the same memory.
  • Q2, an organization that existed before this process, or a bind retried on a later boot, has no reading. ANSWERED: accept A. The behaviour it replaces was the defect; the residual (an organization whose creating boot's bind failed keeps its seed rows unowned) is fail-safe and logged, with no measured demand, so no card is filed now.
  • Q3, cloud-connection's fallback seed-replayer records nothing (also out-of-scope finding (a), whose carrier was this review). ANSWERED: accept A for now. The composition (an OrganizationsPlugin owner bind on a kernel where only that fallback replayed) has no measured instance; under it the bind hands over nothing and logs it. The owning seat files a card on the first measured instance; the carrier moves from this review to that measurement.

Seed writers that emit no app:seeded (package-install seed apply, publish-time seed apply, the dev hot-reload seeder, the marketplace install's inline seed) — triage asked that any one that must be reached is carried, not dropped (6182011415). JUDGED: none must be reached. The claim's declared purpose is handing the boot seed to the first admin; those writers run after a human exists (an admin installs or publishes) or in development, and under D1 their rows' owner stays as the writer left it. The changeset names them, which is the carrier.

Out-of-scope findings: (a) is Q3. (b) InMemoryDriver's quadratic $in — accepted, no carrier; a driver property, measured and recorded in the claim docblock. (c) an ignore-mode dataset reports a key-matched row it did not write — accepted as the loader's matching rule; named residual: a pre-existing unowned row whose natural key collides with an ignore-mode seed record counts as the seed's and is claimed; the loader's key rule owns that, not this claim.

Triage's acceptance added after this head (6184093292): "a runtime-created unowned row keeps owner_id = null across a restart" — PINNED on a real engine: g1 across two SQLite processes in the warm-boot file, guest_row and form_row across two boots through the real SecurityPlugin; HotCRM's crm_case queue is exactly this shape. "…and a criteria share keyed on it still reaches its audience" — NOT PINNED in this diff: it exercises plugin-sharing's unchanged owner_id == null criteria evaluation and follows from the pinned half, and this diff touches no sharing code. Not a contract defect of this head; the consumer measurement on objectstack-ai/hotcrm#2116 against the first prerelease carrying this fix is where that half is read. Named, not blocking.

Carrier note 6158485388 — APPLIED (①.9).

Gap named, not blocking: the OrganizationsPlugin binding (readSeededRecordIds(ctx, organizationId)) has no plugin-level test. The tracker side (seededRecordIds('crm_case', 'org_a')) and the claim side are each pinned, and the read mirrors the landed shape of readSeedSettlementSnapshot; a booted-stack pass over the default organization's owner bind is the measurement that would close it, and the Dogfood Regression Gate concluded success on this head.

Docs drift advisory (6184053472): five hand-written pages are listed through the SecurityPlugin and SYSTEM_CTX symbols; this diff changes no documented behaviour of either (plugin-security's SYSTEM_CTX is unchanged, the organizations file's own was removed). The tenant-audit census row for claim-seed-ownership.ts (UNTYPED_RECEIVERS, the ql: any writer) still describes the file. Nothing owed.

Governed surfaces: none in the file list, so Prime Directive #14's tiers do not bind this PR; this record is owed by the Clause-②: yes schema-and-contract face and the needs:contract-review label.

Implemented-by: claude/issue-23354-seed-claim-seed-rows
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

… results array

SeedLoaderResultSchema requires results (one entry per dataset, empty
when nothing loaded) and the real loader returns it on every path. The
fake answered without it, a shape the producer never produces, so the
replayer's hand-off of result.results to the seed-settlement tracker
threw on it.

Claude-Session: https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 2c1dd397bfc7c161eb1f110638e3ef73e6533c3d
Local-runs: none

Inputs: card #23354 (body and all twelve comments, rulings and earlier records included); PR #23605 (body, the 32-file list, and the net diff against its merge base 32e03d2fd with main, which stood at 8d05456cb at this read); the check-runs on this head, read at 2026-10-15T04:42Z. The earlier record 6184275111 (PASS on 70a9ae600) and the seat's ACCEPT 6184105830 were read as inputs; neither governs this head. What moved since 70a9ae600 is one file, packages/runtime/src/seed-datasets.test.ts (+16/-3), a test fake; no production line moved. Nothing was built, run or re-run.

① Derived judgments

Face and tier. The file list carries a published schema (packages/spec/src/data/seed-loader.zod.ts) and a published contract (packages/spec/src/contracts/seed-settlement.ts), so the review is owed at CONTRACT_REVIEW_TIER. No governed path: the register rows are docs/adr/**, .claude/**, skills/**, AGENTS.md, CLAUDE.md, docs/NORTH-STAR.md and .github/workflows/**; scripts/adr-anchors/**, .changeset/** and scripts/engine-double-contract.pinned.json are not rows. Head repo equals base repo. Governed Surface Queue Guard concluded success.

Every accept-set and public-surface change the diff implies, each named right or wrong:

  1. SeedLoadResultSchema.landedIds (z.array(z.string()).default([]), described) is a widening: a result without the key still parses, one carrying it now parses. SeedLoadResult is z.input of the schema (seed-loader.zod.ts:431), so the TypeScript field is optional; the only producer literal in the repo is SeedLoaderService.loadDataset (a grep for referencesDeferred: outside tests hits only the schema itself), and buildEmptyResult returns results: []. The three artifacts the key touches were regenerated: authorable-surface/data.json, authorable-defaults/data.json, content/docs/references/data/seed-loader.mdx. json-schema.manifest/data.json is a name-only ratchet that already lists data/SeedLoadResult, so no change was owed there. Type Check · source gates, which carries check:authorable-surface, check:docs and the check:generated reconciliation, concluded success; so did Spec property liveness. RIGHT.
  2. ISeedSettlementService.seededRecordIds?(object, organizationId?) is an optional member: every existing implementer still satisfies the interface, a consumer gains one read. The two readings (boot-time sources without organizationId; the most recent per-organization replay with it) and the rule that absence is no reading, not an empty seed, are in the docblock. api-surface/contracts.json records the export name only and ISeedSettlementService (interface) pre-exists, so no baseline regeneration was owed; Type Check · consumer gates, which carries check:api-surface and check:exported-any, concluded success. RIGHT.
  3. @objectstack/runtime SeedSourceHandle (an entry type export, index.ts:112) gains a REQUIRED publishLanded(report). It is produced only by declareSeedSource and the module's INERT_HANDLE; nothing else in the repo implements it (grep over packages/). For a factory-produced handle that consumers call and do not implement this is right at minor; named for the record: a hand-rolled value typed SeedSourceHandle outside this repo stops type-checking, and an optional member was the strictly additive spelling. SeedLandingReport and recordOrganizationReplay are not re-exported from the entry, so no further surface is added. RIGHT.
  4. claimSeedOwnership's NULL arm becomes owner_id IS NULL AND id IN (one chunk of the seeded ids), one predicate per chunk of CLAIM_PAGE_ROWS (half the per-row hook ceiling), followed by the unchanged usr_system arm (unownedPredicates). An empty reading yields zero NULL-arm writes; an absent reading yields zero NULL-arm writes and the pass report appends the sentence saying so, with meta.seededRows left undefined. That is fail-closed toward ADR-0138 D1 and never the old every-unowned-row predicate. The paged fallback in claimPredicate stays, now serving the usr_system arm. The options widen by one optional field; the default-path change for direct callers is stated in the changeset. RIGHT.
  5. bootstrapPlatformAdmin gains the optional seededRecordIds pass-through on the non-exported BootstrapOptions. Both callers, claimSeedOwnershipOnSettle (security-plugin.ts:1663) and the kernel:ready promotion (:4354), bind the reading through the published contract with readSeededRecordIds(ctx), resolved at call time and read per object, so an over-budget seed is covered by the app:seeded re-run as before. RIGHT.
  6. claimOrgSeedOwnership takes the same selector: organization_id = org AND owner_id IS NULL AND id IN (chunk), chunks of CLAIM_CHUNK_IDS; the ql.find precondition is dropped (a widening of the engines it accepts); with no reading it writes nothing and says so once. The organizations: claimOrgSeedOwnership reads at most 10,000 owner-less ids per object and the once-gate never re-runs it, so rows past 10,000 stay unowned for good #23054 paging fallback is removed rather than kept: a chunk predicate matches at most as many rows as it carries ids, half the ceiling, so the refusal it answered cannot occur, and the 10 500-row guarantee is re-pinned on a unit double and a real engine. The removed symbols (claimUnowned, isPerRowHookBudgetRefusal, idsFrom, MAX_CLAIM_PAGES, the module's SYSTEM_CTX, the BULK_PER_ROW_HOOK_LIMIT_ERROR_CODE import) were module-private. OrganizationsPlugin binds readSeededRecordIds(ctx, organizationId) into the claim handed to createEnsureDefaultOrganizationOnce; Middleware B runs the replay on the sys_organization insert that ensureOnce performs before it binds, so the reading is the right organization's in the ordinary call. The deprecated ensureDefaultOrganization wrapper holds no kernel and hands over no row; its docblock and the changeset say so. RIGHT.
  7. Producer. landedIds is read off internalIdByRecordIndex, the per-dataset map (seed-loader.ts:1071) every write site fills through noteLanded (:1179 insert flush; :1734, :1745, :1768, :1780 update, replay match and refused update of a matched row), ordered by record index and deduplicated; every noteLanded site sits under a !config.dryRun guard, so a dry run reports none. Pinned on the real loader in seed-loader-landed-ids.test.ts (insert, skip replay, update replay, reference-keyed match reporting the resolved id, unseeded row never reported, refused insert, two records one row, dry run). RIGHT.
  8. Host. The inline seed publishes before settle() on the loader path and on both basic-insert fallbacks; the per-organization replayer records through recordOrganizationReplay. The tracker keeps bootLanded additive, lastReplay single, and hands back copies. Pinned in seed-settlement.test.ts (six cases) and app-plugin.seed-landed.test.ts (loader path at the app:seeded instant, second boot re-reporting without a guest row, fallback, per-organization record). RIGHT.
  9. The one file moved since 70a9ae600. The fake loader in seed-datasets.test.ts now resolves results: [] beside summary and errors: SeedLoaderResultSchema.results is required (seed-loader.zod.ts:454, no default), and the real SeedLoaderService.load returns it on every path, buildEmptyResult included. The replayer hands result.results to recordOrganizationReplay, so the old fake threw where the producer never would. The fix is at the fake, not a ?? in the consumer, which is what Prime Directive Add comprehensive test suite for Zod schema validation #12 asks; addLanded still iterates the report as the contract types it. RIGHT.
  10. ADR anchor. scripts/adr-anchors/packages__plugins__plugin-security__src__security-plugin.ts.json gains ADR-0138 and a one-sentence D1 invariant that matches the code it anchors (carrier note 6158485388). Lint · repo & tooling gates, which carries the ADR-anchor step, concluded success. RIGHT.
  11. Logging. An id IN list is reported by its size (whereForLog), never inlined. RIGHT.
  12. Dispatch limits held. packages/metadata-protocol/src/protocol.ts and the app:seeded payload in contracts/plugin-lifecycle-events.ts are untouched. RIGHT.
  13. The three pins triage set (6158407363). (a) a seed row is claimed on the first boot and on a later boot's replay: claim-seed-ownership-seed-settle-rerun.test.ts (two boots through the real SecurityPlugin, acc_seed_3 claimed on boot 2), claim-seed-ownership-warm-boot.test.ts (real ObjectQL over SqlDriver, two processes). (b) a guest-written row and a public-form row keep no owner across two boots: guest_row and form_row in the rerun file, g1 in the warm-boot file, guest_1/form_1 in claim-seed-ownership.test.ts and the organizations unit test, guest_row on a real engine in both dispatch pins; this is the one pin with a red leg under the old predicate. (c) a hook-, flow- or rule-owned row keeps its owner: hook_row, seed_hooked, hook_1 in the same files, a preservation pin. RIGHT.
  14. Measured cost. The NULL arm now pays InMemoryDriver's quadratic $in once, on the pass that claims the rows (2.6 s at 5 000 rows), and nothing on a warm boot; SQL drivers were not measured and the docblock says so. Accepted as a driver property.

Check-runs on this head, 39 at the reading above: 27 success, 5 skipped (path-filtered or opt-in: Console Pin Gate, Dogfood Verify CLI, the sharded Dogfood Regression Gate, Temporal Conformance, Packed-tarball smoke), 0 failure, 7 in progress. Concluded and judged as the gate verdicts: Lint & Repo Gates and its eight lanes (which carry check:test-source-alias, check:where-matcher, check:engine-double-contract, the ADR-anchor and governed-prose gates), Type Check · source gates, Type Check · consumer gates, Type Check · debt ledger, Spec property liveness, Check Changeset, Governed Surface Queue Guard, Build Core, Build Docs, Dogfood Regression Gate, Check Documentation Links, Check PR Size, and the card and claim guards. NOT MEASURED by this record: Test Core (1/6) to (6/6) and Type Check · workspace, still running, and the TypeScript Type Check aggregate, not yet reported. Those stand between this verdict and the landing: the seat's landing terms (6184105830 §7) and the Post-Task rule require every check green before the PR leaves draft or is queued, and this PASS substitutes for none of them. The dev's account (6184467870) is that the file moved since 70a9ae600 is the one a Test Core shard failed on at that head; whether this head's shards agree is theirs to say.

② Semver level

  • Clause-②: yes sits at the start of a body line with no arm: well-formed (at most one arm) and true to the diff. Items 1 to 3 widen a published schema, a published contract and a runtime handle; nothing narrows an accept set; no export is removed or renamed, so no breaking changeset, no FROM to TO mapping and no ADR-0087 disposition marker is owed. skip-changeset is not applied, correctly: five released packages publish (all private: false, at 18.0.0-next.1).
  • .changeset/23354-seed-landed-rows.md: @objectstack/spec minor, RIGHT (items 1 and 2; yes takes at least minor). @objectstack/metadata-protocol minor, RIGHT (a new field on every per-dataset result, additive). @objectstack/runtime minor, RIGHT (item 3, a required member on a handle only the package constructs, plus the tracker's new read; nothing removed).
  • .changeset/23354-seed-claim-seed-rows.md: @objectstack/plugin-security patch, RIGHT (a bug fix in a released package; the option widens; the default-path change for direct callers is stated). @objectstack/organizations patch, RIGHT (the same; the deprecated wrapper's change is stated; the module-private removals are not surface).
  • Both bodies carry what an upgrading reader greps for: the new names, the direct-caller behaviour, and the seed writers outside the claim's purpose. Check Changeset concluded success.
  • Base drift: the branch merged main once at 32e03d2fd; CI ran on the merge of this head into 8d05456cb (the drift check's tree line); GitHub reports the PR mergeable with no conflict. The queue leg re-merges.

③ Boundary flags

Dev deviations (6184063982), each:

  1. File surface exceeded: app-plugin.ts beyond the settle hand-off (the per-organization replayer must record or the twin has no reading; the fallback must publish or its seed rows go unclaimed), organizations-plugin.ts (the binding), ensure-default-organization.ts (docblock), organizations/vitest.config.ts (the alias check:test-source-alias demanded), engine-double-contract.pinned.json (--write rows). ANSWERED: each is load-bearing or gate-demanded; the seat amended the surface to the file list (6184105830 §4); accepted.
  2. The twin's paging fallback removed, not adapted. ANSWERED: unreachable by construction (①.6); accepted.
  3. Lock-pressure narrowing: the whole runtime and spec suites, cli unit, cloud-connection and every test:repo project left to CI. ANSWERED: the Test Core shards are the gate and are NOT MEASURED here; the landing waits on them.
  4. main merged once and moved since. ANSWERED: see ②; the queue re-merges.
  5. Model-free commit trailers: the AGENTS.md form; nothing to judge.

Dev deviation (6184467870): the task worktree was recreated from the pushed branch at 70a9ae600 before the patch. Nothing to judge; the diff is what is reviewed.

Open questions (the dev's two in 6184063982; the PR body's three):

  • Q1, the per-organization reading keeps the most recent replay only. ANSWERED, not escalated: accept A as built. The bound is stated in the contract docblock; the one reader, the default organization's owner bind, asks in the same ensureOnce call whose sys_organization insert ran the replay. One correction to the PR body's residual, named not blocking: "it then hands over no row and logs it" holds only when no host publishes a reading at all. When another organization's replay has replaced this one's between the insert and the bind, the tracker answers an empty list for a non-current organization, which the twin cannot tell from a replay that landed nothing, so it hands over nothing and says nothing. The direction is fail-safe (D1's empty state; no row reaches a wrong principal), the window is one ensureOnce call on a sys_user-write arm (at kernel:ready no user exists), and no instance is measured, so it is an acceptance-note residual rather than a card. The shape of a later fix, not owed here: the tracker answers undefined for a non-current organization so the twin can report the replaced reading.
  • Q2, an organization that existed before this process, or a bind retried on a later boot, has no reading. ANSWERED: accept A. The behaviour it replaces was the defect; the ADR-0093 D7 once-gate means a retried bind is the exception; the residual (an organization whose creating boot's bind never recorded keeps its seed rows unowned) is fail-safe and logged, with no measured demand, so no card is filed now.
  • Q3, cloud-connection's fallback seed-replayer records nothing (also out-of-scope finding (a), whose carrier was this review). ANSWERED: accept A for now. The composition (an OrganizationsPlugin owner bind on a kernel where only that fallback replayed) has no measured instance; under it the bind hands over nothing. The owning seat files a card on the first measured instance; the carrier moves from this review to that measurement.

Seed writers that emit no app:seeded (package-install seed apply, publish-time seed apply, the dev hot-reload seeder, the marketplace install's inline seed): triage asked that any one that must be reached be carried, not dropped (6182011415). JUDGED: none must be reached. The claim's declared purpose is handing the boot seed to the first admin; those writers run after a human exists (an admin installs or publishes) or in development, and under D1 their rows' owner stays as the writer left it. The changeset names them, which is the carrier.

Out-of-scope findings: (a) is Q3. (b) InMemoryDriver's quadratic $in: accepted, no carrier; a driver property, measured and recorded in the claim docblock. (c) an ignore-mode dataset reports a key-matched row it did not write: accepted as the loader's matching rule; the named residual is that a pre-existing unowned row whose natural key collides with an ignore-mode seed record counts as the seed's and is claimed, which the loader's key rule owns, not this claim.

Triage's added acceptance (6184093292), answered by the seat in 6184305303: "a runtime-created unowned row keeps owner_id = null across a restart" is PINNED on this head: g1 across two SQLite processes (warm-boot file), guest_row and form_row across two boots through the real SecurityPlugin (rerun file), guest_row beside 10 500 seeded rows on a real engine (plugin-security dispatch pin); HotCRM's crm_case queue is exactly that shape. "A criteria share keyed on it still reaches its audience" is NOT PINNED in this diff: plugin-sharing is untouched, its owner_id == null criteria evaluation follows from the pinned half, and the end-to-end reading belongs to objectstack-ai/hotcrm#2116 against the first prerelease carrying this fix. Named, not blocking.

Carrier note 6158485388: APPLIED (①.10).

Gap named, not blocking: the OrganizationsPlugin binding (readSeededRecordIds(ctx, organizationId) into the ensureOnce claim) has no plugin-level test; the tracker side (seededRecordIds('crm_case', 'org_a')) and the claim side are each pinned, and the read mirrors readSeedSettlementSnapshot. A booted-stack pass over the default organization's owner bind is the measurement that would close it; Dogfood Regression Gate concluded success on this head.

Docs drift advisory (6184053472): five hand-written pages are listed through the SecurityPlugin and SYSTEM_CTX symbols; this diff changes no documented behaviour of either (plugin-security's SYSTEM_CTX is unchanged; the organizations file's own was removed). Nothing owed.

Governed surfaces: none in the file list, so Prime Directive #14's tiers do not bind this PR; this record is owed by the Clause-②: yes schema-and-contract face.

Implemented-by: claude/issue-23354-seed-claim-seed-rows
Reviewed-by: session_01LFqdRsuM5nkqoJdHd5ztVk

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 05:06
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 05:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit b1e9280 Oct 15, 2026
42 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23354-seed-claim-seed-rows branch October 15, 2026 05:39
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 protocol:data size/xl tests tooling

Projects

None yet

2 participants