Skip to content

17.5.0: every boot of a database created on 17.4 prints "Paged read of 'sys_migration' is NOT deterministic" for the platform's own primary-key lookup #20648

Closed

Description

@hotlong

Found while upgrading HotCRM from @objectstack/* 17.4.0 to 17.5.0 in place (objectstack-ai/hotcrm#1970): 17.5.0 was booted on a SQLite database that 17.4.0 had created.

Symptom

Every objectstack dev boot, and every os migrate plan, prints this on the upgraded database:

Paged read of 'sys_migration' is NOT deterministic: this driver did not create the table, so it cannot name a unique column to order by, and the query asked for no sort of its own. Walking the pages may serve one row twice and never serve another (objectstack#4363). Give the query an `orderBy` on a unique column, or declare the object so this driver manages its table.

Control: 17.4.0 on the same database file prints nothing about sys_migration in three boots.

Why it is a false positive

The reads are the platform's own point lookups by primary key. Neither can return more than one row:

  • packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts, persistSeedTenancyReceiptRow: ledger.find(DATA_MIGRATION_FLAG_OBJECT, { where: { id: flag.id }, limit: 1, … })
  • packages/platform-objects/src/system/migration-flag.ts (~line 68): engine.find(DATA_MIGRATION_FLAG_OBJECT, { where: { id: migrationId }, limit: 1, … })

The limit: 1 makes the driver classify these as paged reads. On a table this driver instance did not create, the driver has no unique column to order by, so it warns. The warning then tells an operator to add an orderBy to a query that the operator did not write and cannot edit.

Impact

It is cosmetic, but it hits every in-place upgrade, on every boot. It is the first warning an upgrading operator sees, and its wording ("may serve one row twice and never serve another") reads as possible data loss in the migration ledger.

Suggested direction (not a ruling)

Either option would work:

  • give the two lookups an explicit orderBy: [{ field: 'id' }];
  • have the determinism check skip a read whose where pins a unique key with equality, since such a read has no pages to walk.

The second covers any other point lookup with the same shape.

Related: in the same upgraded database, os migrate plan also lists + sys_migration [add_columns: columns_moved_at] as pending. That is additive and expected. It is noted here only because it is the same table.

Activity

  1. objectstack-fleet commented on Sep 29, 2026

    @objectstack-fleet
    Contributor

    Path: the road's upgrade step — an upgraded install boots without false alarms | 缺项 (the platform's own sys_migration point lookups go through find with limit: 1, so the driver's paging-determinism check reads them as unsorted pages on a table it didn't create) | P3

    Triage: first grade — bug · priority:p3 · domain:engine · area:devpath · pm:queue. Direction: the two point lookups take the driver's single-row route (findOne); the check is not taught a uniqueness guess

    Triage: lands in packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts (:1101) and packages/platform-objects/src/system/migration-flag.ts (:68) ⇒ domain:engine.

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-29T13:57Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p3. Nothing is read wrongly: both are primary-key lookups that can return at most one row. But every in-place upgrade prints it on every boot, and on os migrate plan, as the operator's first warning. Its words suggest a ledger can lose rows, and it asks for an orderBy on a query the operator didn't write. That makes it a tax on the upgrade step, like #20620 and #20622.

    The mechanism, read on main.

    • sql-driver.ts's check (:17610–17639) treats limit / offset as a page, unless opts.singleRowLookup is set. That is how findOne marks itself (:7507, :17594).
    • On a table this driver instance didn't create, it has no tie-breaker column, so an unsorted page warns.
    • The two platform lookups call find(DATA_MIGRATION_FLAG_OBJECT, { where: { id }, limit: 1 }), not findOne.

    Direction.

    • Both lookups use the engine's findOne, the route the driver already exempts. ⛔ Don't add an orderBy to each call; that only hides the shape.
    • ⛔ Not option 2. Teaching the check to skip a where that "pins a unique key" contradicts its own premise: on a table it didn't create, the driver can't know which column is unique.
    • Sweep the family. git grep the platform packages for find( with limit: 1 and an id equality, and move each hit the same way, in the same PR.
    • Pins:
      • A SQLite database created by the 17.4 code path, booted on the fix, prints no sys_migration paging warning, on boot or on os migrate plan.
      • The control: an unsorted limit read on a table the driver didn't create still warns.
  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Claim: PM loop round 24
    Session: session_01DEvba2nBuD4tWzfq8r8NFY
    Account: os-support-ai (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20648-migration-flag-point-lookup
    Worktree: objectstack-issue-20648
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (triage 5891790566):

    • packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts (persistSeedTenancyReceiptRow) and packages/platform-objects/src/system/migration-flag.ts: both sys_migration point lookups take the engine's single-row route (findOne), the route the driver's paging-determinism check already exempts. ⛔ No per-call orderBy.
    • the family sweep triage orders: platform-package find( calls with limit: 1 and an id equality, moved the same way in the same PR. The census comes first, and the count is reported. If it passes about 15 sites or reaches another lane's package in a way that is not a one-line move, stop and report.
    • pins: a SQLite database created by the 17.4 code path, booted on the fix, prints no sys_migration paging warning on boot or on os migrate plan. Control: an unsorted limit read on a table the driver did not create still warns.
    • .changeset/20648-*.md (patch for each package touched).

    Stop on breach and explain in the report. ⛔ Not the driver's determinism check (triage: "⛔ Not option 2").
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no
    Thread-read: 5891790566
    Serial constraints cleared: read at 2026-09-30T01:39Z against origin/main.

    • None of the 8 open PRs' file lists touches packages/metadata-protocol/src/migrations/, packages/platform-objects/src/system/migration-flag.ts or sql-driver.ts.
  3. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    {
    "issue": 20648,
    "status": "done",
    "branch": "claude/issue-20648-migration-flag-point-lookup",
    "pr": "#20766",
    "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
    "premise_still_valid": true,
    "summary": "The symptom reproduced, but the card's attribution did not. On the 17.4 code path (a probe app installed from npm with all 261 @objectstack/* packages pinned to 17.4.0, three 17.4.0 boots as the control, all printing 0 warnings), base 01e78dc prints the sys_migration paging warning once on boot and once on os migrate plan. It also prints it on every main boot of examples/app-crm, first and second. A gated stack-trace probe in driver-sql's built dist (restored afterwards, byte-identical by cmp) shows the only unsorted paged read of an unregistered sys_migration, on boot and on plan, is ObjectQL.readMigrationFlagVerified via haveFileColumnsMoved (packages/objectql/src/engine.ts). It is neither of the two named sites. Under the dispatch's producer clause, all five sys_migration primary-key reads now use findOne: the engine's readMigrationFlagVerified, recordObservedDeviation and retractCreationAttestation, platform-objects' readDataMigrationFlag, and the metadata-protocol seed-tenancy receipt. The duck-typed MigrationFlagEngine and SeedTenancyLedger declare findOne in place of find, and resolveSeedTenancyLedger requires findOne. sql-driver.ts is untouched. After the change: 0 warnings on boot and on os migrate plan against the 17.4 file, from builds of 4ad574e and of the final head 2c59fca, and 0 on both app-crm boots. The family sweep found 55 sites at base, 50 of them outside sys_migration, across 13 packages. That is past the ~15 stop line, so the 50 are not moved and the census goes to the seat (full file:line list in the PR body). The PR opens with 'Fixes #20648'. If the seat wants the card kept open for the sweep, the one-line change is to make that first line 'Part of #20648' (seat edit; I do not PATCH bodies).",
    "tests": "All runs are at or after the fix commit; results are read from the vitest summary lines. (1) objectql vitest run --project local: 339 files / 6708 tests passed, after merging origin/main (338/6691 before the merge). (2) platform-objects pnpm test: 59 files / 948 passed. (3) metadata-protocol pnpm test: 190 files passed, 3 skipped; 2792 tests passed, 19 skipped. After the last double edit, seed-tenancy-backfill.test.ts plus null-seam: 74 passed. (4) service-storage pnpm test: 40 files / 627 passed. (5) runtime, the new sys-migration-point-lookup.integration.test.ts plus seed-tenancy-autonumber-split.integration.test.ts: 15 passed. (6) The new objectql sys-migration-single-row-route.test.ts: 4 passed. (7) typecheck green for objectql, platform-objects, metadata-protocol, service-storage, runtime and cli. The check:test-typecheck debt is unchanged, and --listFiles counts 1 for each new test file in its test program. Ablation, from the committed fix: scripts/ablation-replace.mjs reverted readMigrationFlagVerified to find+limit:1 (anchor x1 to x0, blob 607f1216 to b828359f). objectql was rebuilt, and ablation-dist-preflight found the marker in 2 dist files (exit 0). The runtime pin went RED ('expected [ Array(1) ] to deeply equal []') and the objectql pin went RED on 3 of 4 cases. The restore was proven (blob == HEAD 607f1216, git diff HEAD empty). objectql was rebuilt, preflight --absent exited 0 with the tree clean, and both pins were green again. The observed direction is 'turns red', as expected. Manual before/after readings are in the PR body.",
    "mcp_calls": "0 — no MCP GitHub tools used (reads were single-card REST GETs with curl)",
    "api_writes": "3 — each one relay repository_dispatch (POST /repos/objectstack-ai/objectstack/dispatches) executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #20766); (2) label-write --assign os-support-ai → POST /repos//issues/20766/assignees; (3) this os-dev-report comment → POST /repos//issues/20648/comments. git push is not a REST write. Zero label writes: the dispatch named no labels, and a changeset is present.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door — objectstack dev --database file:NEW.sqlite (examples/app-crm), FIRST boot of a new database, prints once: "[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... select * from sys_migration where id = 'adr-0104-file-references' limit 1 - no such table: sys_migration". Measured on base 01e78dc and on a build of 4ad574e (unchanged by this PR). · evidence: the engine's flag read runs before the table is created. The engine's own missing-table line is demoted to debug; the driver's is on its warn channel. It is the same false-alarm family as this card, one frame lower. · dedupe words: 'DATABASE_ERROR sys_migration no such table', 'backend refused a read fresh database first boot', 'readMigrationFlagVerified before schema sync', 'first boot sql-driver warn missing table'",
    "carrier: seat (stop-line census for the triage-ordered sweep) · noted, not filed — 50 non-test find calls with limit: 1 and an id equality remain in 13 packages at 2c59fca. By package: plugin-approvals 11, plugin-security 9, plugin-sharing 8, service-automation 7, runtime 4, plugin-email 3, mcp 2, and one each in plugin-auth, plugin-hono-server, service-datasource, service-queue, service-settings and service-storage. The file:line list is in the PR #20766 body. None warned in the measured boots (their tables are registered before they run), so each is dormant until read before registration. Not one-line moves include runtime/src/action-execution.ts:196/217/261 (unwraps .value and scans the array) and plugin-hono-server current-user-endpoints.ts:570 (find.call).",
    "carrier: 承接者:无 · noted only in the PR Acceptance notes — the header table of packages/objectql/src/engine-find-missing-table-log-level.test.ts still lists readMigrationFlagVerified among the find callers it measured. That is a historical reading; the reader now goes through findOne."
    ],
    "hypotheses": {
    "H1": "Partly falsified. Both named sites exist with the stated shape (seed-tenancy-backfill.ts:1101, migration-flag.ts:68 at base), but neither produces the observed warning; the producer is engine.ts readMigrationFlagVerified (:9599 at base). Held: engine findOne reaches driver.findOne, whose findRows gets singleRowLookup=true. The duck-typed handles (MigrationFlagEngine, SeedTenancyLedger) are narrower interfaces, so each now declares findOne. The options survive: where {id} passes requireFindOnePredicate, and context {isSystem:true} goes through mergeReadContext as on find. Both are pinned by recorded-call assertions.",
    "H2": "Measured with an AST census (object-literal arg with limit:1 and a where/filter object literal carrying an id key). Blind to a where or limit held in a variable, and to a top:1 alias. Lit control: both named sites hit. 55 at base in 16 packages; 5 on sys_migration are moved; 50 remain. Stop line breached, so the census is reported.",
    "H3": "Falsified as stated. os migrate plan does not read through the two named sites. Its single unregistered-table sys_migration read is the engine's haveFileColumnsMoved → readMigrationFlagVerified. Pinned by objectql sys-migration-single-row-route.test.ts ('the gate reads (boot, os migrate plan) are findOne by primary key') and the runtime e2e."
    },
    "gates": {
    "head": "2c59fcaf18",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), 77 commands",
    "ran": 77,
    "exit_nonzero_final": 0,
    "reconcile": "dispatch-gates --ran: 77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED (a DERIVED zero — all 77 recorded an exit code and none of them is 3)",
    "fixed_on_the_way": [
    "check:engine-double-contract — 2 doubles routed through assertEngineFindOnePredicate / assertEngineUpdateDispatch; the new pinned findOne rows recorded with --write (the ledger only grew, 0 seams lost)",
    "check:objectql-double-limit — baseline ratcheted down: 3 entries deleted, lax-deviation-reclamation-gate blind 2 to 1",
    "check:query-options-erasure — the new tests type their options (test surface back to 236, at the ceiling)",
    "check:dual-build-cjs-loads — first run PREREQUISITE NOT MET (unbuilt packages, NOT MEASURED); green after a full turbo build"
    ],
    "ci": "in_progress at report time on 2c59fca: 31 check runs, 10 success, 3 skipped, 18 in_progress. No wait per contract."
    },
    "line_budget": {
    "diff_vs_merge_base": "a6866da0c7..2c59fca: 17 files, +582 / -75",
    "source": "engine.ts +11/-9, migration-flag.ts +11/-4, seed-tenancy-backfill.ts +12/-6",
    "tests": "two new files, +241 (objectql) and +121 (runtime); the rest are double moves",
    "ledgers": "engine-double-contract.pinned.json +27/-2, objectql-double-limit.baseline.json +1/-10",
    "changeset": "+42",
    "human_merge_threshold": "under 5000"
    },
    "deviations": [
    "Landing site extended beyond the two named files to packages/objectql/src/engine.ts (3 sites), under the producer clause: the measured producer of the boot and plan warning is ObjectQL.readMigrationFlagVerified. The two sibling engine reads are in the same file and the same ledger family, and seed-tenancy-backfill.ts documents all five as one enumerated reader set, so moving only some of them would falsify that comment.",
    "Stop line hit: the sweep census found 55 sites (50 outside sys_migration, in 13 packages, most in other lanes). They are not moved; the census is reported.",
    "Files outside the claimed surface, all test-only or gate ledgers: service-storage test doubles (forced by the MigrationFlagEngine type change); the runtime e2e pin; objectql engine.test.ts, value-shape-scan-advisory.test.ts and adr0104-attestation-evidence.test.ts (their doubles served the flag read through find); scripts/engine-double-contract.pinned.json and scripts/objectql-double-limit.baseline.json (gate-prescribed). No source outside the three packages.",
    "Merged origin/main into the branch (53650ad) because main had moved packages/objectql/src/engine.ts. objectql full suite, typecheck and runtime pins reran green after the merge.",
    "Clause-② copied as dispatched ('no'). The exported duck types MigrationFlagEngine and SeedTenancyLedger change find to findOne. The runtime ObjectQL engine has both, so no spec or authorable key moves. The changeset states the FROM/TO for hand-written stand-ins. The seat should re-grade if it reads this as a narrowing.",
    "The first local commit carried the harness trailer with a model name. It was amended to the model-free pair before the first push, so it never reached the remote.",
    "Temporary gated instrumentation was added to packages/drivers/driver-sql/dist (gitignored build output) to trace the producer. It was restored byte-identical (cmp against a saved copy), and the tree was clean throughout."
    ],
    "files_changed": [
    ".changeset/20648-sys-migration-flag-point-lookup.md",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/sys-migration-single-row-route.test.ts",
    "packages/objectql/src/engine.test.ts",
    "packages/objectql/src/value-shape-scan-advisory.test.ts",
    "packages/objectql/src/adr0104-attestation-evidence.test.ts",
    "packages/platform-objects/src/system/migration-flag.ts",
    "packages/platform-objects/src/system/migration-flag.test.ts",
    "packages/platform-objects/src/system/migration-flag.column-move.test.ts",
    "packages/platform-objects/src/plugin.test.ts",
    "packages/runtime/src/sys-migration-point-lookup.integration.test.ts",
    "packages/services/service-storage/src/files-to-references-migration.test.ts",
    "packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts",
    "scripts/engine-double-contract.pinned.json",
    "scripts/objectql-double-limit.baseline.json"
    ]
    }

  4. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    Addendum, patch round 1 (seat regrade), head 225de70625 on PR #20766. Changeset-only; no code changed.

    • Changeset: platform-objects and metadata-protocol go to minor, objectql stays patch. The Clause line reads Clause-②: no (narrowing). There is a BREAKING paragraph, and the FROM/TO table is kept.
    • ADR-0087 disposition: not-required (runtime-interface-only ...), naming packages/platform-objects/src/system/migration-flag.ts#MigrationFlagEngine and packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts#SeedTenancyLedger. check-adr-0087-registration verified both as interfaces. The reason for the choice, and one detector miss handed up, are in adr_0087_category below.
    • PR body, for the seat to apply: line 3 Clause-②: no becomes Clause-②: no (narrowing). Line 1 Fixes #20648 and blank line 2 are unchanged.
    • Gates at 225de70625: 77 derived, 77 ran, all exit 0, and --ran reconciles to 0 NOT-MEASURED. CI is in progress.

    {
    "issue": 20648,
    "status": "done",
    "branch": "claude/issue-20648-migration-flag-point-lookup",
    "pr": "#20766",
    "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
    "premise_still_valid": true,
    "summary": "Patch round 1, changeset-only; no code changed. .changeset/20648-sys-migration-flag-point-lookup.md now bumps @objectstack/platform-objects and @objectstack/metadata-protocol minor and @objectstack/objectql patch. Its title carries !, its Clause-②: line reads Clause-②: no (narrowing), a BREAKING: paragraph names what a hand-written MigrationFlagEngine / SeedTenancySeam ledger stand-in must now provide (findOne answering the where.id row or null) and says it ships minor under the launch-window convention for accept-set narrowings, and the FROM/TO table is kept. It carries exactly one ADR-0087 marker: not-required (runtime-interface-only packages/platform-objects/src/system/migration-flag.ts#MigrationFlagEngine, packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts#SeedTenancyLedger), with a reason that closes the other categories on facts. check-adr-0087-registration verified both symbols (interface, interface). PR body change for the seat to apply through the relay: body line 3 (line 1 is Fixes #20648, line 2 is blank) goes from Clause-②: no to Clause-②: no (narrowing); nothing else changes. Driven offline with a pull_request event carrying that body, check-changeset-no-major exits 0 and reads the arm as narrowing, a BREAKING change that ships minor in the launch window. It also exits 0 on the current body, which declares no with no arm, so CI does not red either way; the patch only makes the PR declaration match the changeset. Round-0 content is unchanged: all five sys_migration reads use findOne, measured 1 warning before and 0 after on boot and on os migrate plan of a 17.4.0-created database, and the 50 other-lane dormant census sites stay listed in the PR body.",
    "tests": "This round is changeset-only and no code changed; round-0 test evidence stands (objectql 339/6708, platform-objects 59/948, metadata-protocol 2792 passed, service-storage 40/627, runtime pins 15, ablation red then green). Gates this round, on head 225de70: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (1 declared-breaking changeset [BREAKING+bang+clause-②-narrowing], runtime-interface-only verified: MigrationFlagEngine (interface), SeedTenancyLedger (interface)); node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (no major; level axis not applicable without a PR payload), and with --event on the patched PR body exit 0 (arm narrowing, platform-objects minor, metadata-protocol minor); node scripts/check-empty-changeset.mjs --base origin/main exit 0; pnpm check:doc-authoring exit 0. The worktree had been removed after round 0, so it was re-created from the pushed head 2c59fca. A full turbo build was 72/72 cache hits before the dist-reading gates ran.",
    "mcp_calls": "0 — no MCP GitHub tools used in either round",
    "api_writes": "Round 1: 1 — this os-dev-report addendum comment on #20648, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, which executes POST /repos//issues/20648/comments). git push (2c59fca..225de70, no force) is not a REST write. Round 0 (for the record): 3 relay writes (pr_create #20766, PR assignee, report comment 5903382265).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door — objectstack dev --database file:NEW.sqlite (examples/app-crm), FIRST boot of a new database, prints once: "[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... select * from sys_migration where id = 'adr-0104-file-references' limit 1 - no such table: sys_migration". Measured on base 01e78dc and on a build of 4ad574e (unchanged by this PR). · evidence: the engine's flag read runs before the table is created. The engine's own missing-table line is demoted to debug; the driver's is on its warn channel. It is the same false-alarm family as this card, one frame lower. · dedupe words: 'DATABASE_ERROR sys_migration no such table', 'backend refused a read fresh database first boot', 'readMigrationFlagVerified before schema sync', 'first boot sql-driver warn missing table'",
    "carrier: seat (stop-line census for the triage-ordered sweep) · noted, not filed — 50 non-test find calls with limit: 1 and an id equality remain in 13 packages at 2c59fca. By package: plugin-approvals 11, plugin-security 9, plugin-sharing 8, service-automation 7, runtime 4, plugin-email 3, mcp 2, and one each in plugin-auth, plugin-hono-server, service-datasource, service-queue, service-settings and service-storage. The file:line list is in the PR #20766 body. None warned in the measured boots (their tables are registered before they run), so each is dormant until read before registration. Not one-line moves include runtime/src/action-execution.ts:196/217/261 (unwraps .value and scans the array) and plugin-hono-server current-user-endpoints.ts:570 (find.call).",
    "carrier: 承接者:无 · noted only in the PR Acceptance notes — the header table of packages/objectql/src/engine-find-missing-table-log-level.test.ts still lists readMigrationFlagVerified among the find callers it measured. That is a historical reading; the reader now goes through findOne."
    ],
    "hypotheses": {
    "H1": "Partly falsified. Both named sites exist with the stated shape (seed-tenancy-backfill.ts:1101, migration-flag.ts:68 at base), but neither produces the observed warning; the producer is engine.ts readMigrationFlagVerified (:9599 at base). Held: engine findOne reaches driver.findOne, whose findRows gets singleRowLookup=true. The duck-typed handles (MigrationFlagEngine, SeedTenancyLedger) are narrower interfaces, so each now declares findOne. The options survive: where {id} passes requireFindOnePredicate, and context {isSystem:true} goes through mergeReadContext as on find. Both are pinned by recorded-call assertions.",
    "H2": "Measured with an AST census (object-literal arg with limit:1 and a where/filter object literal carrying an id key). Blind to a where or limit held in a variable, and to a top:1 alias. Lit control: both named sites hit. 55 at base in 16 packages; 5 on sys_migration are moved; 50 remain. Stop line breached, so the census is reported.",
    "H3": "Falsified as stated. os migrate plan does not read through the two named sites. Its single unregistered-table sys_migration read is the engine's haveFileColumnsMoved → readMigrationFlagVerified. Pinned by objectql sys-migration-single-row-route.test.ts ('the gate reads (boot, os migrate plan) are findOne by primary key') and the runtime e2e."
    },
    "gates": {
    "head": "225de70625",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths), 77 commands, the same list as at 2c59fca",
    "ran": 77,
    "exit_nonzero_final": 0,
    "reconcile": "dispatch-gates --ran: 77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED (a DERIVED zero — all 77 recorded an exit code and none of them is 3)",
    "ci": "in_progress at report time on 225de70: 33 check runs, 26 success, 3 skipped, 4 in_progress (Test Core 1/6, 3/6, 6/6, Lint & Repo Gates). No wait per contract. The previous head 2c59fca shows Test Core failure: that is the aggregate job reporting at 03:28:21Z after shards 1/6, 3/6 and 5/6 and Lint & Repo Gates were cancelled at 03:27:52-55Z by the round-1 push; shards 2/6, 4/6 and 6/6 had completed success. No shard completed red."
    },
    "line_budget": {
    "diff_vs_merge_base": "a6866da0c7..225de70: 17 files; this round changes only the changeset (+1 file already counted)",
    "changeset": "now 50 lines (was 42)",
    "human_merge_threshold": "under 5000"
    },
    "deviations": [
    "Landing site extended beyond the two named files to packages/objectql/src/engine.ts (3 sites), under the producer clause: the measured producer of the boot and plan warning is ObjectQL.readMigrationFlagVerified. The two sibling engine reads are in the same file and the same ledger family, and seed-tenancy-backfill.ts documents all five as one enumerated reader set, so moving only some of them would falsify that comment.",
    "Stop line hit: the sweep census found 55 sites (50 outside sys_migration, in 13 packages, most in other lanes). They are not moved; the census is reported.",
    "Files outside the claimed surface, all test-only or gate ledgers: service-storage test doubles (forced by the MigrationFlagEngine type change); the runtime e2e pin; objectql engine.test.ts, value-shape-scan-advisory.test.ts and adr0104-attestation-evidence.test.ts (their doubles served the flag read through find); scripts/engine-double-contract.pinned.json and scripts/objectql-double-limit.baseline.json (gate-prescribed). No source outside the three packages.",
    "Merged origin/main into the branch (53650ad) because main had moved packages/objectql/src/engine.ts. objectql full suite, typecheck and runtime pins reran green after the merge.",
    "The first local commit carried the harness trailer with a model name. It was amended to the model-free pair before the first push, so it never reached the remote.",
    "Temporary gated instrumentation was added to packages/drivers/driver-sql/dist (gitignored build output) to trace the producer. It was restored byte-identical (cmp against a saved copy), and the tree was clean throughout.",
    "Round 1: Clause-② regraded by the seat to no (narrowing); platform-objects and metadata-protocol bumped minor, objectql stays patch.",
    "Round 1: the seat said "line 2" of the PR body; line 2 is blank, and the Clause line is line 3. The exact replacement is given in summary.",
    "Round 1: the runtime-interface-only claim is not refused by the prescription detector because the detector misses the FROM/TO table. This is handed up in adr_0087_category.why rather than relied on.",
    "Round 1: the worktree was re-created from pushed head 2c59fca (it had been removed after round 0) and pnpm install was re-run. No force-push."
    ],
    "files_changed": [
    ".changeset/20648-sys-migration-flag-point-lookup.md",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/sys-migration-single-row-route.test.ts",
    "packages/objectql/src/engine.test.ts",
    "packages/objectql/src/value-shape-scan-advisory.test.ts",
    "packages/objectql/src/adr0104-attestation-evidence.test.ts",
    "packages/platform-objects/src/system/migration-flag.ts",
    "packages/platform-objects/src/system/migration-flag.test.ts",
    "packages/platform-objects/src/system/migration-flag.column-move.test.ts",
    "packages/platform-objects/src/plugin.test.ts",
    "packages/runtime/src/sys-migration-point-lookup.integration.test.ts",
    "packages/services/service-storage/src/files-to-references-migration.test.ts",
    "packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts",
    "scripts/engine-double-contract.pinned.json",
    "scripts/objectql-double-limit.baseline.json"
    ],
    "round": "patch round 1 (seat regrade of the changeset level); head 225de70",
    "adr_0087_category": {
    "chosen": "runtime-interface-only",
    "why": "The five categories in CATEGORIES, read against this change. unpublished: false, both packages publish. registered / already-registered: false, no ADR-0087 id covers an engine interface's method set; writing one would put a source-code prescription in the ledger that objectstack migrate meta cannot project. no-migration-prescription: closed, because the body does prescribe a rewrite (the FROM/TO table). type-surface-only: refused by its predicate 4, since both interfaces were concretely typed at the merge base, not erased. runtime-interface-only: fits the class it was built for (#8277: a published runtime TypeScript interface, no Zod schema, no packages/spec declaration, no object definition, no metadata surface referencing it; the compiler at the stand-in's call site is the channel). The gate verified both path#Symbol refs at HEAD. Handed up, as the #18745 precedent asks: this category inherits the prescription refusal, and the prescription detector does NOT flag this body's table (findMigrationPrescription returns null; the header | Surface | FROM | TO | matches neither FROM_TO_LABEL_RE's leading-FROM cell nor the OLD/NEW column words). The claim rests on the four symbol predicates the gate verifies positively, not on that miss, and the marker says so. If the seat reads the inherited refusal as covering a source-code prescription too, no category the gate offers fits this change."
    }
    }

  5. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    os-dev-report
    Addendum, patch round 2 (review 5903787430, FAIL item ②), head 67d5021948 on PR #20766. Changeset-only; no code changed.

    • The category stays runtime-interface-only with both path#Symbol refs. The | Surface | FROM | TO | table is replaced by one prose line: Your fix: a stand-in that implemented find for these helpers implements findOne(object, options) instead, answering the row whose where.id matches, or null.
    • The marker justification now states that the body carries no migration prescription and that the compiler at the stand-in author's call site carries the fix. The claim that the category was built for a rewrite prescription is removed.
    • The public door is named resolveSeedTenancySeam.
    • Gate lines at 67d5021948:
      • check-adr-0087-registration --base origin/main exit 0: runtime-interface-only verified, MigrationFlagEngine (interface), SeedTenancyLedger (interface).
      • check-changeset-no-major --base origin/main exit 0. The same check with --event on the current PR body exit 0: LEVEL AXIS reads no (narrowing), platform-objects and metadata-protocol are minor, objectql is patch.
      • check-empty-changeset exit 0; pnpm check:doc-authoring exit 0.
      • dispatch-gates --commands: 77 derived, 77 ran, all exit 0. --ran: 0 NOT-MEASURED (derived zero).
      • CI Check Changeset: success.

    {
    "issue": 20648,
    "status": "done",
    "branch": "claude/issue-20648-migration-flag-point-lookup",
    "pr": "#20766",
    "session": "session_01DEvba2nBuD4tWzfq8r8NFY",
    "premise_still_valid": true,
    "summary": "Patch round 2 is changeset-only; no code changed. It answers the one FAIL in review 5903787430 (②). The changeset keeps category runtime-interface-only and both path#Symbol refs (packages/platform-objects/src/system/migration-flag.ts#MigrationFlagEngine, packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts#SeedTenancyLedger). The | Surface | FROM | TO | table is gone. The fix is one prose line, in the #18905 form: 'Your fix: a stand-in that implemented find for these helpers implements findOne(object, options) instead, answering the row whose where.id matches, or null.' It has no FROM/TO columns, no arrow and no migration label. The BREAKING paragraph keeps its meaning: two published engine interfaces narrow, each now requires findOne where it required find, and it ships minor under the launch-window convention. The marker justification now says the body carries no migration prescription, that the only affected party is the TypeScript author of a hand-written stand-in, and that the compiler carries the fix at their call site. It closes unpublished, registered/already-registered and type-surface-only on facts. It explains why this category and not the broader no-migration-prescription: the positive no-metadata-surface reading is available here. It no longer claims the category was built for a rewrite prescription. The public door is named resolveSeedTenancySeam; for a find-only host the seam's ledger is undefined. resolveSeedTenancyLedger no longer appears in the body. findMigrationPrescription returns null on the new body, and that is now true of the body rather than a detector miss. Levels unchanged: platform-objects minor, metadata-protocol minor, objectql patch. The PR body already reads Clause-②: no (narrowing) (the seat applied it). Round-0 content unchanged.",
    "tests": "This round is changeset-only and no code changed; round-0 test evidence stands. Gates at head 67d5021: node scripts/check-adr-0087-registration.mjs --base origin/main exit 0 (1 declared-breaking changeset [BREAKING+bang+clause-②-narrowing], runtime-interface-only verified: MigrationFlagEngine (interface), SeedTenancyLedger (interface)). node scripts/check-changeset-no-major.mjs --base origin/main exit 0 (no major bump). The same check with --event on the CURRENT PR #20766 body, fetched by REST (line 3 Clause-②: no (narrowing)), exit 0: LEVEL AXIS declares no (narrowing), platform-objects minor and metadata-protocol minor account for it, objectql patch is not refused. node scripts/check-empty-changeset.mjs --base origin/main exit 0. pnpm check:doc-authoring exit 0. CI Check Changeset on 67d5021: success. The worktree was re-created from the pushed head 225de70, and a full turbo build was 72/72 cache hits before the dist-reading gates ran.",
    "mcp_calls": "0 — no MCP GitHub tools used in any round",
    "api_writes": "Round 2: 1 — this os-dev-report addendum comment on #20648, through the fleet-write relay (POST /repos/objectstack-ai/objectstack/dispatches, which executes POST /repos//issues/20648/comments). git push (225de70..67d5021, no force) is not a REST write. Earlier rounds, for the record: round 0 had 3 relay writes (pr_create #20766, PR assignee, comment 5903382265); round 1 had 1 (comment 5903601935).",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: public door — objectstack dev --database file:NEW.sqlite (examples/app-crm), FIRST boot of a new database, prints once: "[sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... select * from sys_migration where id = 'adr-0104-file-references' limit 1 - no such table: sys_migration". Measured on base 01e78dc and on a build of 4ad574e (unchanged by this PR). · evidence: the engine's flag read runs before the table is created. The engine's own missing-table line is demoted to debug; the driver's is on its warn channel. It is the same false-alarm family as this card, one frame lower. · dedupe words: 'DATABASE_ERROR sys_migration no such table', 'backend refused a read fresh database first boot', 'readMigrationFlagVerified before schema sync', 'first boot sql-driver warn missing table' · filed by the seat as #20768 (open), per review 5903787430",
    "carrier: seat (stop-line census for the triage-ordered sweep) · noted, not filed — 50 non-test find calls with limit: 1 and an id equality remain in 13 packages at 2c59fca. By package: plugin-approvals 11, plugin-security 9, plugin-sharing 8, service-automation 7, runtime 4, plugin-email 3, mcp 2, and one each in plugin-auth, plugin-hono-server, service-datasource, service-queue, service-settings and service-storage. The file:line list is in the PR #20766 body. None warned in the measured boots (their tables are registered before they run), so each is dormant until read before registration. Not one-line moves include runtime/src/action-execution.ts:196/217/261 (unwraps .value and scans the array) and plugin-hono-server current-user-endpoints.ts:570 (find.call).",
    "carrier: 承接者:无 · noted only in the PR Acceptance notes — the header table of packages/objectql/src/engine-find-missing-table-log-level.test.ts still lists readMigrationFlagVerified among the find callers it measured. That is a historical reading; the reader now goes through findOne."
    ],
    "hypotheses": {
    "H1": "Partly falsified. Both named sites exist with the stated shape (seed-tenancy-backfill.ts:1101, migration-flag.ts:68 at base), but neither produces the observed warning; the producer is engine.ts readMigrationFlagVerified (:9599 at base). Held: engine findOne reaches driver.findOne, whose findRows gets singleRowLookup=true. The duck-typed handles (MigrationFlagEngine, SeedTenancyLedger) are narrower interfaces, so each now declares findOne. The options survive: where {id} passes requireFindOnePredicate, and context {isSystem:true} goes through mergeReadContext as on find. Both are pinned by recorded-call assertions.",
    "H2": "Measured with an AST census (object-literal arg with limit:1 and a where/filter object literal carrying an id key). Blind to a where or limit held in a variable, and to a top:1 alias. Lit control: both named sites hit. 55 at base in 16 packages; 5 on sys_migration are moved; 50 remain. Stop line breached, so the census is reported.",
    "H3": "Falsified as stated. os migrate plan does not read through the two named sites. Its single unregistered-table sys_migration read is the engine's haveFileColumnsMoved → readMigrationFlagVerified. Pinned by objectql sys-migration-single-row-route.test.ts ('the gate reads (boot, os migrate plan) are findOne by primary key') and the runtime e2e."
    },
    "gates": {
    "head": "67d5021948",
    "derivation": "node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack (no paths): 77 commands, the same list as at 225de70",
    "ran": 77,
    "exit_nonzero_final": 0,
    "reconcile": "dispatch-gates --ran: 77 derived famil(ies) accounted for — 77 run, 0 NOT-MEASURED (a DERIVED zero — all 77 recorded an exit code and none of them is 3)",
    "ci": "in_progress at report time on 67d5021: 33 check runs, 27 success (Check Changeset among them), 3 skipped, 3 in_progress, none red. No wait, per contract."
    },
    "line_budget": {
    "diff_vs_merge_base": "a6866da0c7..67d5021: 17 files; rounds 1 and 2 touch only the changeset",
    "changeset": "now 52 lines: the 4-line table became a 3-line prose fix, and the BREAKING paragraph was re-laid-out",
    "human_merge_threshold": "under 5000"
    },
    "deviations": [
    "Landing site extended beyond the two named files to packages/objectql/src/engine.ts (3 sites), under the producer clause: the measured producer of the boot and plan warning is ObjectQL.readMigrationFlagVerified. The two sibling engine reads are in the same file and the same ledger family, and seed-tenancy-backfill.ts documents all five as one enumerated reader set, so moving only some of them would falsify that comment.",
    "Stop line hit: the sweep census found 55 sites (50 outside sys_migration, in 13 packages, most in other lanes). They are not moved; the census is reported.",
    "Files outside the claimed surface, all test-only or gate ledgers: service-storage test doubles (forced by the MigrationFlagEngine type change); the runtime e2e pin; objectql engine.test.ts, value-shape-scan-advisory.test.ts and adr0104-attestation-evidence.test.ts (their doubles served the flag read through find); scripts/engine-double-contract.pinned.json and scripts/objectql-double-limit.baseline.json (gate-prescribed). No source outside the three packages.",
    "Merged origin/main into the branch (53650ad) because main had moved packages/objectql/src/engine.ts. objectql full suite, typecheck and runtime pins reran green after the merge.",
    "The first local commit carried the harness trailer with a model name. It was amended to the model-free pair before the first push, so it never reached the remote.",
    "Temporary gated instrumentation was added to packages/drivers/driver-sql/dist (gitignored build output) to trace the producer. It was restored byte-identical (cmp against a saved copy), and the tree was clean throughout.",
    "Round 1: Clause-② regraded by the seat to no (narrowing); platform-objects and metadata-protocol bumped minor, objectql stays patch.",
    "Round 1: the seat said "line 2" of the PR body; line 2 is blank, and the Clause line is line 3. The exact replacement is given in summary.",
    "Round 1: the worktree was re-created from pushed head 2c59fca (it had been removed after round 0) and pnpm install was re-run. No force-push.",
    "Round 2: the FROM/TO table was replaced with a prose fix line, and the marker justification was rewritten per review 5903787430 ②. Round 1's detector-miss caveat is withdrawn because the body now carries no prescription.",
    "Round 2: resolveSeedTenancyLedger in the changeset body became resolveSeedTenancySeam, the exported door.",
    "Round 2: the worktree was re-created from the pushed head 225de70. No force-push."
    ],
    "files_changed": [
    ".changeset/20648-sys-migration-flag-point-lookup.md",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts",
    "packages/metadata-protocol/src/migrations/seed-tenancy-backfill.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/sys-migration-single-row-route.test.ts",
    "packages/objectql/src/engine.test.ts",
    "packages/objectql/src/value-shape-scan-advisory.test.ts",
    "packages/objectql/src/adr0104-attestation-evidence.test.ts",
    "packages/platform-objects/src/system/migration-flag.ts",
    "packages/platform-objects/src/system/migration-flag.test.ts",
    "packages/platform-objects/src/system/migration-flag.column-move.test.ts",
    "packages/platform-objects/src/plugin.test.ts",
    "packages/runtime/src/sys-migration-point-lookup.integration.test.ts",
    "packages/services/service-storage/src/files-to-references-migration.test.ts",
    "packages/services/service-storage/src/lax-deviation-reclamation-gate.test.ts",
    "scripts/engine-double-contract.pinned.json",
    "scripts/objectql-double-limit.baseline.json"
    ],
    "round": "patch round 2 (at-tier review 5903787430 FAIL item ②, the ADR-0087 disposition); head 67d5021",
    "adr_0087_category": {
    "chosen": "runtime-interface-only",
    "why": "Per review 5903787430 and ADR-0087 D7/D8: the category fits the change (published runtime TypeScript interfaces with no metadata surface, with the compiler as the channel), and it inherits the no-migration-prescription refusal. So the body now carries no prescription: the pair-notation table was replaced with a one-line prose fix, which is the #18905 (a5ec0be) move. check-adr-0087-registration verified both symbols as interfaces at 67d5021, and findMigrationPrescription returns null on a body that has no table or arrow. Round 1's "handed up" caveat is withdrawn: the claim no longer rests on a detector miss."
    }
    }

  6. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    ACCEPT — PR #20766 @ 67d502194

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T04:31Z. The seat is the reviewer of record. Everything below was read on GitHub and on origin/main, not taken from the report.

    • Shape: the first line is Fixes #20648, then Clause-②: no (narrowing). The PR assignee is os-support-ai. The changeset is @objectstack/platform-objects and @objectstack/metadata-protocol minor with a BREAKING paragraph and one ADR-0087 marker (runtime-interface-only), and @objectstack/objectql patch.
    • The fix, and where it landed: the dev measured that the warning's producer is the engine's own migration-gate read, ObjectQL.readMigrationFlagVerified (behind haveFileColumnsMoved), not the two sites the card named. All five primary-key reads of sys_migration now go through findOne, the single-row route the driver already exempts: three in packages/objectql/src/engine.ts, readDataMigrationFlag in platform-objects, and the seed-tenancy receipt in metadata-protocol. The driver's determinism check is unchanged (triage: "⛔ Not option 2").
    • Measured at the door: on a SQLite database created by 17.4.0, the "Paged read of 'sys_migration' is NOT deterministic" warning went from 1 to 0, on boot and on os migrate plan; examples/app-crm boots went to 0 too. 17 files, +591/-75; not governed.
    • The seat's regrade (patch round 1): MigrationFlagEngine (@objectstack/platform-objects/system) and SeedTenancyLedger (reached through the exported SeedTenancySeam) replace a required find with findOne. A hand-written stand-in that satisfied either type before no longer does, which is a narrowing of two published accept sets.
    • Contract review: at-tier record 5903787430 on 225de7062, FAIL on one item: the ADR-0087 runtime-interface-only claim conflicted with the changeset's FROM/TO table (D7 inherits the prescription refusal). Patch round 2 rewrote the table as one prose line and corrected the marker's justification. Delta record 5904064738 on this head, PASS, with every other item of 5903787430 carried over.
    • Seat rulings, open to the maintainer's veto:
      • Fixes #20648 closes the card. The whole sys_migration family (5 sites, all in this lane's packages) is moved. The sweep's census found 50 more find calls with limit: 1 and an id equality, all in other lanes' packages (domain:services 43, domain:cli 7) and none warning in any measured boot. They stay listed in the PR body with carrier none, because a dormant site with no measured symptom is not a card under the filing gate.
    • Filed from this round: [finding] the first boot of a new database prints [sql-driver] DATABASE_ERROR … no such table: sys_migration on the warn channel: the engine's migration-gate read runs before the table is created #20768, the first boot of a new database printing DATABASE_ERROR … no such table: sys_migration on the driver's warn channel (the same false-alarm family, one frame lower).

    Landing: once every check on this head concludes green, the seat flips it ready and arms auto-merge.


    Generated by Claude Code

  7. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    Contributor

    Landed — PR #20766 as cd6d8a5ff

    domain:engine#1 · session_01DEvba2nBuD4tWzfq8r8NFY · 2026-09-30T05:06Z.

    • Verified on main: cd6d8a5ff is a squash with one parent (91e8fa194) and an ancestor of origin/main. The engine's [#20648] single-row read is in packages/objectql/src/engine.ts at the squash (1) and not at its parent (0), and the pin packages/objectql/src/sys-migration-single-row-route.test.ts exists at the squash and not at its parent. 17 files, +591/-75, as reviewed.
    • Route: ready and auto-merge through the relay at the reviewed head 67d502194 (delta PASS 5904064738 on that head), with all 3 skipped checks rostered. added_to_merge_queue at 04:36Z, then merged by the queue at 05:05Z.
    • What changed: the platform's five sys_migration primary-key reads go through findOne, so a database created by 17.4 no longer prints "Paged read of 'sys_migration' is NOT deterministic" on every boot and every os migrate plan. @objectstack/platform-objects and @objectstack/metadata-protocol ship minor with a BREAKING note (a hand-written MigrationFlagEngine or seed-tenancy ledger stand-in now provides findOne).
    • Card: closed completed by the PR's Fixes #20648; pm:dispatched is removed in this act.
    • Left, by the seat's ruling (open to veto): 50 dormant find calls with limit: 1 and an id equality in other lanes' packages (domain:services 43, domain:cli 7), listed in the PR body with carrier none. Filed: [finding] the first boot of a new database prints [sql-driver] DATABASE_ERROR … no such table: sys_migration on the warn channel: the engine's migration-gate read runs before the table is created #20768 (the first-boot no such table: sys_migration line).

    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions