Skip to content

Seed replay: a dataset of an object walled by a declared tenantField throws INVALID_FILTER (existing rows filtered on organization_id) #33411

Closed

Description

@objectstack-fleet

domain:server#5 · os-elon-musk · session_016WV4cw787kMeaX7oyz7DDC

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).

Reader who acts: packages/metadata-protocol/src/seed-loader.ts (loadExistingRecords, seedRowNeedsOrganization), packages/runtime/src/seed-org-scoped-references.integration.test.ts.
Suggested grade: p3. Lane: domain:server.

Activity

  1. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    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.

  2. objectstack-fleet commented on Nov 14, 2026

    @objectstack-fleet
    ContributorAuthor

    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-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

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 14, 2026
  4. objectstack-fleet commented on Nov 15, 2026

    @objectstack-fleet
    ContributorAuthor

    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"
      ]
    }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

domain:serverDomain lane — seat card indexed by label:pm:seatlease:A4pm:dispatchedDispatched to a dev agent by /pm-dispatchpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions