You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Seed replay: a dataset of an object walled by a declared tenantField throws INVALID_FILTER (existing rows filtered on organization_id) #33411
Found while fixing #33393 (PR #33409), measured on its branch at 99f60e1df with a temporary test case (real AppPlugin + SqlDriver over better-sqlite3), then removed.
Line
A seed dataset OF an object walled by a declared tenancy.tenantField with systemFields.tenant: false (e.g. { object: 'crm_workspace_tag', mode: 'upsert', externalId: 'name', records: [{ name: 'Silver' }] }) · AppPlugin's per-organization seed replayer on a walled boot · rare (custom tenant field) · measured.
replayer(ORG_NORTH) throws the driver's INVALID_FILTER ("A filter on object crm_workspace_tag names a column the database could not resolve"). Cause: SeedLoaderService.loadExistingRecords (packages/metadata-protocol/src/seed-loader.ts, the per-tenant replay branch) filters the SOURCE object's existing rows by where.organization_id, the hard-coded column #33393 fixed on the reference-target side.
Not a mechanical fix
The loader also has to decide what it stamps on such rows: seedRowNeedsOrganization answers only for organization_id. So the fix needs (a) the existing-rows filter on the resolved tenant column, and (b) a rule for stamping the declared tenant column on seeded rows (or refusing such a dataset with a named error instead of a driver refusal).
Seed replay is Area A4 (seed) under the area-lease pilot (#33059, table on #28677). The card was filed without a lease: label; server#5 may move it to another area.
Claim: area-lease pilot #33059, area thread A4 (data layer) · p3 (A4's p1 #33454 waits on PR #33451; #33409 is merged, so this card's serial blocker is cleared) · dispatched to an os-dev subagent of this area thread
Session: session_016WV4cw787kMeaX7oyz7DDC (area thread; subagent of it)
Account: os-elon-musk
Branch: claude/area-A4-33411
Worktree: objectstack-a4-33411
Domain: domain:server
Seat: domain:server#5
File surface: packages/metadata-protocol/src/seed-loader.ts (loadExistingRecords, seedRowNeedsOrganization and the stamp it drives), packages/runtime/src/seed-org-scoped-references.integration.test.ts (or a sibling integration test), one changeset. Stop on breach.
Container & model: M, mode:subagent, model: default, effort: high
Clause-②: no (expected; re-read after the diff)
Metadata-protocol: none
Responsibility: a seed dataset of an object walled by a declared tenancy.tenantField replays per organization on that column, matching and stamping the rows of the seeding organization only
Thread-read: none
Premise: re-measured at origin/main before any edit; premise_still_valid: false is a valid result
Serial constraints cleared: #33409 merged; A4's in-flight work (#33201 driver-sql widening; PRs #33451 mcp/spec, #33518 filter door) does not touch seed-loader.ts
{
"issue": 33411,
"status": "done",
"branch": "claude/area-A4-33411",
"pr": "https://github.com/objectstack-ai/objectstack/pull/33550",
"session": "session_016WV4cw787kMeaX7oyz7DDC",
"premise_still_valid": true,
"summary": "Premise re-measured at origin/main e40cd1e28d with the new integration cases (fix not yet applied): replayer(ORG_NORTH) on a crm_workspace_tag dataset (tenancy.tenantField workspace_id, systemFields.tenant false) threw the driver's INVALID_FILTER on 'organization_id'. (a) SeedLoaderService.loadExistingRecords now matches existing rows on organizationColumnOf(objectName) = resolveTenantFieldName(definition) ?? DEFAULT_TENANT_FIELD, the reading #33409 introduced for the reference probe. (b) seedRowNeedsOrganization delegates to a new seedRowTenantColumn (resolveTenantFieldName, then the injected organization_id via resolveInjectedSystemColumns). The dataset loop stamps, refuses (field and message), skips the stamped reference, owns seed files and picks the per-row write organization on that column, and writeRecord takes the row organization as a parameter. The wall reads the column the same way (SqlDriver applyTenantScope and injectTenantOnInsert both use resolveTenantField), so no refusal path was needed. Behaviour change to note: an object that declares a tenantField and also carries organization_id is now stamped only on the declared column, the one the engine and driver wall by. No object in the repo declares a non-default tenantField.",
"tests": "New top-level describe in packages/runtime/src/seed-org-scoped-references.integration.test.ts (real AppPlugin + SeedLoaderService + ObjectQL + SqlDriver over better-sqlite3): (1) two organizations replay the tag dataset, South pre-holds its own Silver; North inserts 1 stamped workspace_id=North, South inserts 0 and keeps tag_silver_s, a second replay of both inserts 0 with rows unchanged; (2) an unpinned load with two organizations refuses the row with the loader's named refusal on field workspace_id, no row lands; (3) CONTROL crm_account (organization_id) replays stamped and idempotent. BEFORE the fix (origin/main source): Tests 2 failed | 19 passed (21), case 1 'A filter on object crm_workspace_tag names a column the database could not resolve' plus the INVALID_FILTER log line on organization_id, case 2 field '(write)' (engine refusal) instead of 'workspace_id'. AFTER: Tests 21 passed (21). pnpm --filter @objectstack/metadata-protocol vitest run: Test Files 340 passed | 9 skipped (349), Tests 22866 passed | 43 skipped, exit 0. Ablations at fe338b4650 (fix committed first; scripts/ablation-replace.mjs with restore proof; metadata-protocol rebuilt each leg; runtime resolves metadata-protocol through dist, not aliased): LEG A (filter back to organization_id): mutation landed blob 5ff5e8c6e2a6 -> b40cbc9a0619, ablation-dist-preflight --absent on '[await this.organizationColumnOf(objectName)]' passed, Tests 1 failed | 20 passed with case 1 red on INVALID_FILTER crm_workspace_tag. The leg's build exits 1 at the DTS step (TS6133: organizationColumnOf unused under the mutation), after the JS was emitted. The first run of this leg discarded build output and was re-run with it captured; the reading is the same. LEG B (stamp column back to organization_id only): blob 5ff5e8c6e2a6 -> ba19869c5932, preflight --absent on 'const resolved = resolveTenantFieldName(definition);' passed, Tests 1 failed | 20 passed with case 2 red only. Case 1 stays green because the driver's injectTenantOnInsert stamps workspace_id from the write tenantId, so (b)'s observable gain is the loader's named refusal, plus row-organization reads for records that author their own tenant column. RESTORE: blob == HEAD, git diff HEAD empty, rebuild exit 0, both markers present in dist, tree clean, Tests 21 passed (21).",
"gates": "Local: metadata-protocol typecheck exit 0; runtime typecheck (tsc + check:test-typecheck) exit 0; eslint --no-inline-config on both changed files exit 0. dispatch-gates --commands (derived at fe338b4650 vs merge base e40cd1e28, 3 paths): 64 families, each run with its exit code captured before any pipe; 63 exit 0 (check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:durability-log-level, check:engine-double-contract, check:issue-citations, check:doc-authoring, check:changeset-gate-self-tests, check-adr-0087-registration, check-empty-changeset among them). NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET, because only the runtime dependency closure was built; declared narrowing, CI Lint & Repo Gates runs it. --ran reconciliation with exit codes: 64 derived, 63 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN, exit 0. test:repo was not run locally (the diff touches no repo-project test); CI Test Core runs it. PR check-runs were not yet concluded when this report was written (in_progress); CI convergence is PM's.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm",
"mcp_calls": "0",
"api_writes": "3 — git push (not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + assignee POST, run 43657381412, read-back identical 7757 bytes, assignee os-elon-musk); post-stamped.mjs os-dev-report comment on #33411",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"class: a · reach: AppPlugin's seed-replayer on a walled boot (the sys_organization-insert replay), measured: replayer(ORG_NORTH) on a dataset { object: 'crm_country', mode: 'upsert', externalId: 'name', records: [{ name: 'Japan' }] } of an object declared tenancy: { enabled: false } throws the driver's INVALID_FILTER ('A filter on object crm_country names a column the database could not resolve'), and the row does not land · evidence: temporary case in the #33411 harness at fe338b4650 (removed). Cause: loadExistingRecords' per-organization filter. The object resolves no tenant column, so the filter falls back to organization_id, a column the object does not have. Same line this PR edited, same behaviour before and after. Not fixed here: the remedy is a semantic choice (match a global object's rows unscoped on a per-org replay, so every new organization's replay upserts the shared row, the probeScopeOf 'global' precedent; or refuse the dataset by name), not a mechanical one. Recommendation: unscoped match keyed on the same predicate probeScopeOf uses for 'global' · dedupe words: seed replay tenancy disabled INVALID_FILTER; global catalog seed per-organization replay; loadExistingRecords organization_id filter tenancy enabled false"
]
}
Activity
objectstack-fleet commentedon Nov 14, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage:
+lease:A4· 2026-11-14T19:44Z⛔ Not a claim; claim, grade and state unchanged.
Seed replay is Area A4 (seed) under the area-lease pilot (#33059, table on #28677). The card was filed without a
lease:label; server#5 may move it to another area.objectstack-fleet commentedon Nov 14, 2026
domain:server#5 · os-elon-musk · session_016WV4cw787kMeaX7oyz7DDC
Claim: area-lease pilot #33059, area thread A4 (data layer) · p3 (A4's p1 #33454 waits on PR #33451; #33409 is merged, so this card's serial blocker is cleared) · dispatched to an os-dev subagent of this area thread
Session:
session_016WV4cw787kMeaX7oyz7DDC(area thread; subagent of it)Account:
os-elon-muskBranch:
claude/area-A4-33411Worktree:
objectstack-a4-33411Domain:
domain:serverSeat:
domain:server#5File surface:
packages/metadata-protocol/src/seed-loader.ts(loadExistingRecords,seedRowNeedsOrganizationand the stamp it drives),packages/runtime/src/seed-org-scoped-references.integration.test.ts(or a sibling integration test), one changeset. Stop on breach.Container & model:
M,mode:subagent,model: default,effort: highClause-②: no (expected; re-read after the diff)
Metadata-protocol: none
Responsibility: a seed dataset of an object walled by a declared
tenancy.tenantFieldreplays per organization on that column, matching and stamping the rows of the seeding organization onlyThread-read: none
Premise: re-measured at
origin/mainbefore any edit;premise_still_valid: falseis a valid resultSerial constraints cleared: #33409 merged; A4's in-flight work (#33201 driver-sql widening; PRs #33451 mcp/spec, #33518 filter door) does not touch seed-loader.ts
objectstack-fleet commentedon Nov 15, 2026
domain:server#5 · os-elon-musk · session_016WV4cw787kMeaX7oyz7DDC
os-dev-report
{ "issue": 33411, "status": "done", "branch": "claude/area-A4-33411", "pr": "https://github.com/objectstack-ai/objectstack/pull/33550", "session": "session_016WV4cw787kMeaX7oyz7DDC", "premise_still_valid": true, "summary": "Premise re-measured at origin/main e40cd1e28d with the new integration cases (fix not yet applied): replayer(ORG_NORTH) on a crm_workspace_tag dataset (tenancy.tenantField workspace_id, systemFields.tenant false) threw the driver's INVALID_FILTER on 'organization_id'. (a) SeedLoaderService.loadExistingRecords now matches existing rows on organizationColumnOf(objectName) = resolveTenantFieldName(definition) ?? DEFAULT_TENANT_FIELD, the reading #33409 introduced for the reference probe. (b) seedRowNeedsOrganization delegates to a new seedRowTenantColumn (resolveTenantFieldName, then the injected organization_id via resolveInjectedSystemColumns). The dataset loop stamps, refuses (field and message), skips the stamped reference, owns seed files and picks the per-row write organization on that column, and writeRecord takes the row organization as a parameter. The wall reads the column the same way (SqlDriver applyTenantScope and injectTenantOnInsert both use resolveTenantField), so no refusal path was needed. Behaviour change to note: an object that declares a tenantField and also carries organization_id is now stamped only on the declared column, the one the engine and driver wall by. No object in the repo declares a non-default tenantField.", "tests": "New top-level describe in packages/runtime/src/seed-org-scoped-references.integration.test.ts (real AppPlugin + SeedLoaderService + ObjectQL + SqlDriver over better-sqlite3): (1) two organizations replay the tag dataset, South pre-holds its own Silver; North inserts 1 stamped workspace_id=North, South inserts 0 and keeps tag_silver_s, a second replay of both inserts 0 with rows unchanged; (2) an unpinned load with two organizations refuses the row with the loader's named refusal on field workspace_id, no row lands; (3) CONTROL crm_account (organization_id) replays stamped and idempotent. BEFORE the fix (origin/main source): Tests 2 failed | 19 passed (21), case 1 'A filter on object crm_workspace_tag names a column the database could not resolve' plus the INVALID_FILTER log line on organization_id, case 2 field '(write)' (engine refusal) instead of 'workspace_id'. AFTER: Tests 21 passed (21). pnpm --filter @objectstack/metadata-protocol vitest run: Test Files 340 passed | 9 skipped (349), Tests 22866 passed | 43 skipped, exit 0. Ablations at fe338b4650 (fix committed first; scripts/ablation-replace.mjs with restore proof; metadata-protocol rebuilt each leg; runtime resolves metadata-protocol through dist, not aliased): LEG A (filter back to organization_id): mutation landed blob 5ff5e8c6e2a6 -> b40cbc9a0619, ablation-dist-preflight --absent on '[await this.organizationColumnOf(objectName)]' passed, Tests 1 failed | 20 passed with case 1 red on INVALID_FILTER crm_workspace_tag. The leg's build exits 1 at the DTS step (TS6133: organizationColumnOf unused under the mutation), after the JS was emitted. The first run of this leg discarded build output and was re-run with it captured; the reading is the same. LEG B (stamp column back to organization_id only): blob 5ff5e8c6e2a6 -> ba19869c5932, preflight --absent on 'const resolved = resolveTenantFieldName(definition);' passed, Tests 1 failed | 20 passed with case 2 red only. Case 1 stays green because the driver's injectTenantOnInsert stamps workspace_id from the write tenantId, so (b)'s observable gain is the loader's named refusal, plus row-organization reads for records that author their own tenant column. RESTORE: blob == HEAD, git diff HEAD empty, rebuild exit 0, both markers present in dist, tree clean, Tests 21 passed (21).", "gates": "Local: metadata-protocol typecheck exit 0; runtime typecheck (tsc + check:test-typecheck) exit 0; eslint --no-inline-config on both changed files exit 0. dispatch-gates --commands (derived at fe338b4650 vs merge base e40cd1e28, 3 paths): 64 families, each run with its exit code captured before any pipe; 63 exit 0 (check:nul-bytes, check:cross-package-test-inputs, check:test-source-alias, check:durability-log-level, check:engine-double-contract, check:issue-citations, check:doc-authoring, check:changeset-gate-self-tests, check-adr-0087-registration, check-empty-changeset among them). NOT MEASURED: pnpm check:dual-build-cjs-loads exit 3, PREREQUISITE NOT MET, because only the runtime dependency closure was built; declared narrowing, CI Lint & Repo Gates runs it. --ran reconciliation with exit codes: 64 derived, 63 run, 1 NOT-MEASURED (derived from the recorded exit 3), 0 UNRUN, exit 0. test:repo was not run locally (the diff touches no repo-project test); CI Test Core runs it. PR check-runs were not yet concluded when this report was written (in_progress); CI convergence is PM's.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm", "mcp_calls": "0", "api_writes": "3 — git push (not REST); fleet-write relay pr_create (POST /repos/objectstack-ai/objectstack/pulls + assignee POST, run 43657381412, read-back identical 7757 bytes, assignee os-elon-musk); post-stamped.mjs os-dev-report comment on #33411", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "class: a · reach: AppPlugin's seed-replayer on a walled boot (the sys_organization-insert replay), measured: replayer(ORG_NORTH) on a dataset { object: 'crm_country', mode: 'upsert', externalId: 'name', records: [{ name: 'Japan' }] } of an object declared tenancy: { enabled: false } throws the driver's INVALID_FILTER ('A filter on object crm_country names a column the database could not resolve'), and the row does not land · evidence: temporary case in the #33411 harness at fe338b4650 (removed). Cause: loadExistingRecords' per-organization filter. The object resolves no tenant column, so the filter falls back to organization_id, a column the object does not have. Same line this PR edited, same behaviour before and after. Not fixed here: the remedy is a semantic choice (match a global object's rows unscoped on a per-org replay, so every new organization's replay upserts the shared row, the probeScopeOf 'global' precedent; or refuse the dataset by name), not a mechanical one. Recommendation: unscoped match keyed on the same predicate probeScopeOf uses for 'global' · dedupe words: seed replay tenancy disabled INVALID_FILTER; global catalog seed per-organization replay; loadExistingRecords organization_id filter tenancy enabled false" ] }