Skip to content

fix(objectql,platform-objects,metadata-protocol): read sys_migration flag rows through findOne, ending the paging warning on every upgraded boot (#20648) - #20766

Merged
objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-20648-migration-flag-point-lookup
Sep 30, 2026
Merged

objectstack-fleet[bot] merged 10 commits into
mainfrom
claude/issue-20648-migration-flag-point-lookup

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20648

Clause-②: no (narrowing)

The platform reads its deployment ledger (sys_migration) one row at a time, by primary key. Five readers wrote that read as find(sys_migration, { where: { id }, limit: 1 }). The SQL driver cannot tell that apart from page one of a walk. On a table it has not registered, it warns that the walk is not deterministic. All five readers now use findOne, the single-row route the driver already exempts. The driver's check and its wording are unchanged. An unsorted limit read on a table the driver did not create still warns, and a test pins that control.

Reproduction on the base, and where the warning came from

Construction: the 17.4 code path. A one-object probe app, installed from npm with every @objectstack/* package pinned to 17.4.0 (npm ls shows 261 entries, all 17.4.0). objectstack dev on 17.4.0 created a new SQLite file and booted it three times. Each run below then used a fresh copy of that file, booted by this branch's base 01e78dceef (the same app, wrapped in defineStack, which main requires).

run code Paged read of 'sys_migration' warnings
boot 1, 2, 3 on the 17.4 file (control) npm 17.4.0 0, 0, 0
objectstack dev on the 17.4 file base 01e78dceef 1
os migrate plan on the 17.4 file base 01e78dceef 1 (the plan also lists + sys_migration [add_columns: columns_moved_at], as the card says)
objectstack dev on a new file (examples/app-crm), first boot base 01e78dceef 1
second boot of that file base 01e78dceef 1

So the warning is not specific to upgraded databases. Every boot of main prints it once, because the read runs before the schema pass registers sys_migration with the driver.

The producer was not either of the two sites the card names. I added a temporary stack-trace line to driver-sql's built dist/, behind an env var, logging every unsorted paged read of a table the driver has not registered. On boot and on os migrate plan, the only such read of sys_migration was ObjectQL.readMigrationFlagVerified in packages/objectql/src/engine.ts, reached through haveFileColumnsMoved(). The dist was restored afterwards, byte-identical to the build: cmp against the saved copy matches. 17.4.0 ships the same driver check but has no haveFileColumnsMoved, which is why 17.4.0 prints nothing. By the time the two named sites run in these compositions, the table is registered, so they order by id and never warn. They still have the defect's shape, so they move too. Landing site, per the dispatch's producer clause: the engine as well as the two named files.

What changed

  • packages/objectql/src/engine.ts: the three sys_migration reads (readMigrationFlagVerified, recordObservedDeviation, retractCreationAttestation) use this.findOne(...).
  • packages/platform-objects/src/system/migration-flag.ts: readDataMigrationFlag uses engine.findOne(...). The duck-typed MigrationFlagEngine declares findOne in place of find, since find is no longer called.
  • packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts: the receipt's existence check uses ledger.findOne(...). SeedTenancyLedger declares findOne in place of find, and resolveSeedTenancyLedger requires findOne. The doc comment listing the ledger's readers now says they read through findOne.
  • The ObjectQL engine has both methods, and every caller in the repo passes the engine, so they are unaffected. Only a hand-written stand-in that offers find alone changes behaviour: its read fails, which reads as "no flag row", and the gates stay closed. The changeset states the FROM and TO shapes.

H1 checked. ObjectQL.findOne calls driver.findOne, which calls findRows(..., singleRowLookup = true) in sql-driver.ts. The options survive the move. where: { id } is a predicate, so requireFindOnePredicate passes. The query's context: { isSystem: true } goes through the same mergeReadContext(query.context, options.context) as on find. The platform-objects and metadata-protocol tests pin the exact call: findOne('sys_migration', { where: { id }, context: { isSystem: true } }).

H3 checked. os migrate plan makes one unregistered-table read of sys_migration: the engine's gate read through haveFileColumnsMoved(). Neither named site is reached on that path. The pin is the engine test below.

After: this branch at 2c59fcaf18, built

run Paged read of 'sys_migration' warnings
objectstack dev on a fresh copy of the 17.4.0 file (booted twice from a build of 4ad574e769, once from a build of 2c59fcaf18) 0
os migrate plan on a fresh copy of the 17.4.0 file (from builds of 4ad574e769 and 2c59fcaf18) 0 (the pending add_columns: columns_moved_at line is still listed, as expected)
os migrate plan on the file this branch's boot had already opened 0
objectstack dev on a new file (examples/app-crm), first and second boot (build of 4ad574e769) 0, 0

The flag read still answers correctly on the old schema. The plan logs this deployment has verified the file-as-reference migration, read through findOne from a sys_migration table that has no columns_moved_at column. 4ad574e769 is the fix before the merge of origin/main, and 2c59fcaf18 is the final head.

The family sweep: census, and the stop line

The census is AST-based, over non-test packages/**/*.ts: a call with an object-literal argument that has limit: 1 (a numeric literal) and a where/filter object literal with an id key, as a shorthand, a scalar or { $eq }. Blind spots: a where or limit held in a variable, and a top: 1 alias. Lit control: both named sites are hits at the base.

  • Base 01e78dceef: 55 hits in 16 packages. plugin-approvals 11, plugin-security 9, plugin-sharing 8, service-automation 7, runtime 4, objectql 3, plugin-email 3, mcp 2, and one each in metadata-protocol, platform-objects, plugin-auth, plugin-hono-server, service-datasource, service-queue, service-settings and service-storage.
  • The 5 that read sys_migration move here. 50 hits remain in 13 packages at 2c59fcaf18.

That is far past the dispatch's ~15-site stop line, and most of the remainder is in other lanes' packages. Several are not one-line moves: runtime/src/action-execution.ts unwraps .value and scans the array, and plugin-hono-server's is find.call(ql, ...). So they are not moved here, and the census goes to the seat. None of the 50 printed a warning in the measured boots. The driver warns only when such a read reaches a table before the table is registered.

packages/mcp/src/plugin.ts:597
packages/mcp/src/stdio-data-bridge.ts:146
packages/plugins/plugin-approvals/src/approval-service.ts:1346 1548 2030 3030 3630 4126 4943 5008 5417 6401 6795
packages/plugins/plugin-auth/src/admin-set-user-manager.ts:241
packages/plugins/plugin-email/src/attachment-reclaim.ts:202
packages/plugins/plugin-email/src/email-plugin.ts:720 791
packages/plugins/plugin-hono-server/src/current-user-endpoints.ts:570
packages/plugins/plugin-security/src/delegated-admin-gate.ts:1238 1251 1328
packages/plugins/plugin-security/src/permission-set-projection.ts:151
packages/plugins/plugin-security/src/position-catalog-refusal.ts:433
packages/plugins/plugin-security/src/security-plugin.ts:4832 6386 6397 7372
packages/plugins/plugin-sharing/src/business-unit-graph.ts:267
packages/plugins/plugin-sharing/src/primary-bu-projection.ts:108
packages/plugins/plugin-sharing/src/sharing-service.ts:567 951 1008 1416 1484
packages/plugins/plugin-sharing/src/team-graph.ts:189
packages/runtime/src/action-execution.ts:196 217 261
packages/runtime/src/security/actor-user.ts:142
packages/services/service-automation/src/flow-dispatch-store.ts:136 153 205
packages/services/service-automation/src/suspended-run-store.ts:330 360 736 786
packages/services/service-datasource/src/datasource-secret-binder.ts:122
packages/services/service-queue/src/db-queue-adapter.ts:683
packages/services/service-settings/src/settings-service-plugin.ts:403
packages/services/service-storage/src/storage-service-plugin.ts:1183

Tests

  • packages/runtime/src/sys-migration-point-lookup.integration.test.ts (new). A real SqlDriver over one SQLite file with two driver instances: the first creates sys_migration and writes a flag row. The second is one the engine knows about but that has not registered the table. The test asserts paginationTieBreaker('sys_migration') is null, as a stated precondition. It then runs the engine's gate read, platform-objects' readDataMigrationFlag, and the receipt-shaped findOne: zero paging warnings. Control: an unsorted engine.find('sys_migration', { limit: 1 }) afterwards gives exactly one warning. It runs last because the warning is once per object per driver.
  • packages/objectql/src/sys-migration-single-row-route.test.ts (new). A recording driver double checks each engine reader on the production path that reaches it: the gate reads, the deviation marker after a lax-admitted write, and revocation of a contradicted creation attestation. It asserts that sys_migration reaches the driver only as findOne. The probe is lit: a caller's own limit-bound find is recorded as a find.
  • The existing flag-reader doubles move to findOne and are held to assertEngineFindOnePredicate. Files: platform-objects migration-flag.test.ts, migration-flag.column-move.test.ts, plugin.test.ts; metadata-protocol seed-tenancy-backfill.test.ts; service-storage files-to-references-migration.test.ts, lax-deviation-reclamation-gate.test.ts; objectql engine.test.ts (the 平台形态的迁移门禁:带自检的数据迁移 + 部署级标记 —— file-as-reference 回收与 strict 翻转都依赖它 #3617 flag block), value-shape-scan-advisory.test.ts, adr0104-attestation-evidence.test.ts (the memoization spy).
  • Suites, all green:
    • objectql --project local: 339 files, 6708 tests (after the merge of origin/main).
    • platform-objects: 59 files, 948 tests.
    • metadata-protocol: 190 files passed, 3 skipped; 2792 tests passed.
    • service-storage: 40 files, 627 tests.
    • runtime: the new pin plus seed-tenancy-autonumber-split.integration.test.ts, 15 tests.
  • Typecheck green for objectql, platform-objects, metadata-protocol, service-storage, runtime and cli. The check:test-typecheck debt is unchanged, and --listFiles confirms both new test files are in their packages' test programs.

Ablation, run from the committed fix. scripts/ablation-replace.mjs reverted readMigrationFlagVerified to find with limit: 1: anchor hit once, blob 607f1216 to b828359f. objectql was rebuilt, and ablation-dist-preflight found the marker in 2 built files. Result: the runtime pin is red (expected [ Array(1) ] to deeply equal []), and the objectql pin is red on 3 of 4 cases. The restore was proven (blob equals HEAD, git diff HEAD empty). objectql was rebuilt again, the preflight --absent passed (exit 0, tree clean), and both pins are green again.

Gates

node scripts/pm/dispatch-gates.mjs --commands at 2c59fcaf18 derives 77 commands, and all 77 ran on that head with exit 0. --ran reconciliation: "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)".

Three gates needed changes on the way:

  • check:engine-double-contract: the new pinned findOne doubles were recorded with --write, which grew the ledger only.
  • check:objectql-double-limit: ratcheted down for the doubles that no longer implement a limit-blind find.
  • check:query-options-erasure: the new tests type their options, so the count stays at its ceiling of 236.

check:dual-build-cjs-loads first answered PREREQUISITE NOT MET (unbuilt packages). It is green after a full turbo run build.

Acceptance notes

  • A new database's first boot still prints the driver's [sql-driver] DATABASE_ERROR — the backend refused a read on 'sys_migration' (SQLITE_ERROR) ... no such table line once. It did so before this change and does so after it. It is the same early read, on a table that does not exist yet. The driver logs it one frame below the engine, whose own missing-table line is already demoted to debug. Not changed here. Reported to the seat.
  • 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.

Generated by Claude Code

…flag rows through findOne

The platform's own sys_migration primary-key lookups went through find() with
limit: 1, which the SQL driver reads as page one of a walk. The engine's
readMigrationFlagVerified runs at boot before the table is registered with the
driver, so every boot and every os migrate plan printed the driver's
nondeterministic-paging warning. All five ledger reads now use findOne, the
single-row route the driver exempts.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
Move the flag-reader doubles to findOne (held to the engine predicate),
pin the three engine readers against a recording driver, and pin end to end
that a ledger read on a table another driver instance created prints no
paging warning while an unsorted page on it still does.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
…col for the sys_migration findOne lookups

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
…ates; record the new pinned findOne doubles

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
…own for the moved doubles

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Sep 30, 2026
@github-actions

github-actions Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/platform-objects, touching 11 documentable anchor(s).

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

  • content/docs/api/data-flow.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/automation/hook-bodies.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/automation/webhooks.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/kernel/contracts/data-engine.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/kernel/contracts/index.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/kernel/contracts/metadata-service.mdx (via getObject (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/kernel/events.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/permissions/attachments-access.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/permissions/field-level-security.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/permissions/record-view-auditing.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/permissions/rls.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/permissions/system-context.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/protocol/objectql/schema.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/ui/react-pages.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))

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

  • content/docs/releases/v15.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/releases/v16.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/releases/v17/17-0.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/releases/v17/17-5.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))
  • content/docs/releases/v17/index.mdx (via findOne (symbol, a method of interface MigrationFlagEngine; a method of interface SeedTenancyLedger), findOne (literal, a string literal in resolveSeedTenancyLedger))

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
  • 1 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 — 26 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 c9c182ed14b1ea1a28cbc56fe71a5a373aca0268 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4707e188297797f5fb4b9bc4cc0bed018c5ced8e — the merge of head 67d5021948ac82000adfcca2bbde972ffa126eef into base c9c182ed14b1ea1a28cbc56fe71a5a373aca0268, 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 4707e188297797f5fb4b9bc4cc0bed018c5ced8e && git checkout 4707e188297797f5fb4b9bc4cc0bed018c5ced8e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c9c182ed14b1ea1a28cbc56fe71a5a373aca0268 67d5021948ac82000adfcca2bbde972ffa126eef && git checkout -B drift-repro c9c182ed14b1ea1a28cbc56fe71a5a373aca0268 && git merge --no-ff 67d5021948ac82000adfcca2bbde972ffa126eef

node scripts/docs-audit/affected-docs.mjs --json c9c182ed14b1ea1a28cbc56fe71a5a373aca0268

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

…wing of two published engine interfaces

platform-objects and metadata-protocol go to minor with a BREAKING paragraph;
Clause-② reads no (narrowing); one ADR-0087 disposition marker,
not-required (runtime-interface-only) naming both interfaces.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 225de70625bf89045e68b26405891cd6ce2f6d90
Local-runs: none

Inputs read: card #20648 (body; comments 5891790566 triage, 5902376781 claim, 5903382265 and 5903601935 os-dev-report); PR #20766 (body, 17-file list, net diff against main from merge-base a6866da0c7: 17 files, +592/-75); the check-runs on the head. The PR head was still 225de70625 when read.

Check-runs on the head as read for this record: 41 runs — 35 success, 5 skipped (Auto Label and Check PR Size duplicates, Console Pin Gate, Build Docs, Packed-tarball smoke), 1 in_progress (Lint & Repo Gates). Green include Check Changeset (both runs), Test Core and all six shards, TypeScript Type Check, the four Type Check jobs, Build Core, the Dogfood gates, Temporal Conformance, Governed Surface Queue Guard, and the claim/single-writer guards. Not waited on. The Test Core failure on 2c59fcaf18 is the previous head's (shards cancelled by the round-1 push); this head's Test Core is success.

① Derived judgments

  1. MigrationFlagEngine (@objectstack/platform-objects/system, published) — required find replaced by required findOne: right, and rightly declared BREAKING. It is the parameter type of six exported helpers — readDataMigrationFlag, isDataMigrationVerified, mayActIrreversibly, recordDataMigrationRun, recordFileColumnMove, attestFreshDatastore (migration-flag.ts:71/120/142/189/258/383 at head) — exactly the six the changeset names. Accept set for a hand-written stand-in: narrowed — one lacking findOne stops compiling, and a fresh object literal typed as the interface that still spells find fails the excess-property check. The formally widened half (a stand-in need not spell find) is unusable against the base and adds no capability. At run time a find-only stand-in's read throws inside readDataMigrationFlag's try, the catch answers null, the gates stay closed — the changeset sentence is true. The new TSDoc ("the one read these helpers make") is true: typecheck green with find gone from the type proves no other read remains in that file.
  2. SeedTenancyLedger (@objectstack/metadata-protocol) — same swap: right. It reaches the public surface only as SeedTenancySeam.ledger (SeedTenancySeam is the package export; resolveSeedTenancyLedger is module-level, not on the package index). What a consumer observes: resolveSeedTenancySeam(host) answers ledger: undefined for a host with find but no findOne, and recordSeedTenancyReceipt then logs at warn that it could not record its receipt (verified at the if (!ledger) branch). The docblock enumerating the ledger's readers now says findOne — true. Nit for the same changeset edit: the body names resolveSeedTenancyLedger, a name no consumer can import; the reachable name is resolveSeedTenancySeam.
  3. @objectstack/objectql — no published signature moves: right, patch. Three internal readers (readMigrationFlagVerified, recordObservedDeviation, retractCreationAttestation) go through this.findOne; no exported type changes. Driver-facing, sys_migration now arrives as driver.findOne, and both verbs are required IDataDriver members. The options survive the move: where: { id } is a non-empty predicate so requireFindOnePredicate passes, and context goes through mergeReadContext(query.context, options.context) on both verbs (engine.ts:11323 vs 11595). SqlDriver.findOne calls findRows(object, { ...query, limit: 1 }, options, true) (sql-driver.ts:7694), so the PR body's H1 sentences are true. No non-test find(...) reader of sys_migration remains at head.
  4. Transitive: FilesToReferencesEngine (@objectstack/service-storage) = BackfillEngine & MigrationFlagEngine narrows too. service-storage is not in the changeset front matter. Acceptable, not a defect: every package sits in the fixed lockstep group and re-versions together; the BREAKING paragraph names FilesToReferencesEngine by package; the compiler error a stand-in author gets names MigrationFlagEngine. Recorded so the seat sees the surface.
  5. Engine failure logging on the new path: no regression. find demotes a missing table to debug through reportFindFailure; findOne has no catch of its own and logs nothing, and readMigrationFlagVerified's own catch handles the miss. So a first boot gains no engine-level line. The PR body's acceptance note ("the engine's own missing-table line is already demoted to debug") describes the old find path — imprecise, harmless. The driver's DATABASE_ERROR line on a new database's first boot is pre-existing and filed as [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 (open).
  6. Triage rulings honoured: right. No orderBy added anywhere in the diff; sql-driver.ts is not in the file list ("⛔ Not option 2"); the driver's check and wording unchanged.
  7. Pins: met at the seam the ruling names. packages/runtime/src/sys-migration-point-lookup.integration.test.ts runs a real SqlDriver over one SQLite file with a second driver instance that never registered the table (precondition asserted: paginationTieBreaker('sys_migration') is null), drives the engine's gate read, readDataMigrationFlag and the receipt-shaped findOne (0 paging warnings), then the control find('sys_migration', { limit: 1 }) (exactly 1). The 17.4-created-file readings on objectstack dev and os migrate plan (1 before, 0 after, both builds) are the dev's manual measurement in the PR body; the automated pin sits at the engine/driver seam, not the CLI. The objectql pin covers all three engine readers on their production paths, with a lit probe.
  8. Gate ledgers: right. scripts/engine-double-contract.pinned.json only grew (new findOne rows, update pin 3 to 4); scripts/objectql-double-limit.baseline.json ratcheted down (3 entries removed, one blind 2 to 1). Test doubles now hold to assertEngineFindOnePredicate, a tightening.
  9. Shipped prose. Changeset body sentences checked true: the five readers and their names; the boot-order mechanism; "the driver's check is unchanged"; the six helper names; the run-time behaviour of a find-only stand-in; "ships as minor under the launch-window convention for accept-set narrowings" (scripts/check-changeset-no-major.mjs, echoed across the current stock). PR body factual sentences true against the code (H1, H3, "what changed", tests named exist). One false sentence sits inside the adr-0087 marker — see ③ and the FAIL item under ②.

② Semver level

③ Boundary flags

  • Producer flag (H1, H3 falsified; landing site extends to packages/objectql/src/engine.ts, 3 sites): answered, right. The measured producer is readMigrationFlagVerified via haveFileColumnsMoved, reached at boot before the schema pass registers the table; the two sibling engine reads are the same enumerated reader set the seed-tenancy docblock names, so moving all five keeps that comment true. Producer clause honoured.
  • Sweep stop line (55 hits, 5 moved, 50 left in 13 packages): escalated, right. The census is AST-based with its blind spots stated (variable-held where/limit, top: 1), lit on both named sites; several remainders are not one-line moves. Carrier none by the seat's ruling; the 50 stay listed in the PR body. Not this PR's.
  • Files outside the claimed surface: answered. All test doubles, the runtime pin, and the two gate ledgers; no source outside the three packages.
  • origin/main merged once (53650adcfe), changeset-only patch round (225de70625), no force-push: answered. The net diff against main is clean; round 1 changed only the changeset.
  • Out-of-scope finding filed as [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: answered (open; the driver's first-boot line, pre-existing).
  • Stale header in engine-find-missing-table-log-level.test.ts listing readMigrationFlagVerified as a find caller: answered. It is a "what was measured" table from os migrate plan prints 4 ERROR records with stack traces on a not-yet-migrated database — every one of them is a read whose caller treats absence as a normal answer #13273 — history, not a claim about head. Leave it.
  • Dev's "the seat should re-grade if it reads this as a narrowing": answered — the seat did (round 1); the grade is right (② above).
  • Dev's ADR-0087 doubt ("if the seat reads the inherited refusal as covering a source-code prescription too, no category the gate offers fits"): answered — the ADR does read it so (D8), but a category still fits: the change is runtime-interface-only; the body must drop the pair-notation prescription, as docs(changeset): drop the migration framing from the EvalContext.api removal note #18905 did. This is the FAIL item.
  • PR opens with Fixes #20648, dev offered Part of if the card stays open for the sweep: answered — the seat ruled carrier none for the remainder, so Fixes stands.
  • open_questions: empty in both reports. None to answer.
  • Check-runs: Lint & Repo Gates was in_progress at read; not waited on.

Implemented-by: claude/issue-20648-migration-flag-point-lookup
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: FAIL


Generated by Claude Code

…e-interface-only claim on a body with no migration prescription

The FROM/TO table goes; the fix is one prose line. The marker says the
body carries no migration prescription and the fix is compiler-carried.
The public door is named resolveSeedTenancySeam.

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

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 67d5021948ac82000adfcca2bbde972ffa126eef
Local-runs: none

Delta review after FAIL record 5903787430 on 225de70625. Inputs read, and nothing else: record 5903787430 in full (its FAIL item ② and the prescribed fix); the diff 225de70625..67d5021948 (one commit, one file, .changeset/20648-sys-migration-flag-point-lookup.md, +19/-20) and the changeset's full text at head; os-dev-report addendum 5903977926 on #20648; ADR-0087 D7 / D8 and scripts/check-adr-0087-registration.mjs's category block at origin/main (eead9dcf40); #18905's changeset move a5ec0bebe8 as precedent; the check-runs on the head. The PR head was still 67d5021948 when read. Not the dispatch order, not the seat's conclusions.

Check-runs on the head as read for this record: 34 runs — 30 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 1 in_progress (Lint & Repo Gates). Green include Check Changeset — the changeset-check job whose step "Require an ADR-0087 disposition on a declared-breaking changeset" runs check-adr-0087-registration.mjs --base MERGE_BASE, so its success is the gate's own verdict on the rewritten marker — plus Test Core and all six shards, TypeScript Type Check, the four Type Check jobs, Build Core, the four Dogfood gates, Temporal Conformance, Governed Surface Queue Guard, the three claim / single-writer guards and Part-of PR must not also close its card. Not waited on.

① Derived judgments

  1. FAIL item ② of 5903787430 — fixed as prescribed.

    • Category and both path#Symbol refs kept: the marker still opens not-required (runtime-interface-only packages/platform-objects/src/system/migration-flag.ts#MigrationFlagEngine, packages/metadata-protocol/src/migrations/seed-tenancy-backfill.ts#SeedTenancyLedger), byte-identical to the previous head.
    • The | Surface | FROM | TO | table is gone. A mechanical scan of the head changeset finds no arrow in any of the three spellings the gate names, no FROM or TO token and no table row; the word "migration" survives only inside names (sys_migration, migration-flag.ts, "the engine's migration-gate read", readDataMigrationFlag, objectstack migrate meta), none of them a label on the fix. The fix is one prose line under a **Your fix:** label — "a stand-in that implemented find for these helpers implements findOne(object, options) instead, answering the row whose where.id matches, or null" — the docs(changeset): drop the migration framing from the EvalContext.api removal note #18905 form: a5ec0bebe8 replaced a **Migration — … → delete the property.** lead with **Your fix — delete the … property.**, its commit message naming the label and the arrow as the substantive halves and keeping operand and verb. A colon in place of docs(changeset): drop the migration framing from the EvalContext.api removal note #18905's dash is immaterial.
    • The marker's justification is true against D7 / D8. The false sentence 5903787430 named ("the body does prescribe a rewrite … which is the shape this category was built for") and the detector-miss caveat ("the prescription detector does not flag the FROM/TO table below") are both removed. What stands: D7's four predicates restated in the author's words (exported interfaces; "Neither is a Zod schema, a packages/spec declaration or an object definition"; "neither is a projection of a schema"; "no metadata surface references either, so objectstack migrate meta has nothing to rewrite"); "The body carries no migration prescription" — true under the inherited refusal's own reading in the gate ("did the author FRAME a rewrite as a migration, not is a rewrite required") and under the docs(changeset): drop the migration framing from the EvalContext.api removal note #18905 precedent; "the only party affected is the TypeScript author of a hand-written stand-in, and that author's fix is carried by the compiler at their own call site, which names the missing findOne" — D8's first paragraph, and a missing required member is reported by name (TS2741); "not type-surface-only: both interfaces were concretely typed at the merge base, not erased" — at a6866da0c7 both declared a typed find method, so D8 predicate 4 is rightly failed; unpublished closed — neither @objectstack/platform-objects nor @objectstack/metadata-protocol is private; registered / already-registered closed — no packages/spec/** file in the 17-file net diff. The closing sentence ("This category, not the broader no-migration-prescription, because the positive reading it verifies is available here") is D7's own rationale ("an exemption that rests on a positive, re-runnable finding").
    • resolveSeedTenancySeam is named where the public door is meant: the last paragraph reads "resolveSeedTenancySeam now attaches a ledger only for a host that has getObject, findOne, insert and update. For a find-only host the seam's ledger is undefined". resolveSeedTenancyLedger no longer appears in the body. resolveSeedTenancySeam is on the metadata-protocol package index; resolveSeedTenancyLedger is module-level only (the index's own comment says the receipt exports nothing).
    • The gate's verdict is Check Changeset success on this head. The addendum's local run (check-adr-0087-registration --base origin/main exit 0, "runtime-interface-only verified") agrees but is not what this record rests on.
  2. Every rewritten factual sentence re-verified against the code at head (the BREAKING paragraph, the fix line, the marker, the last paragraph; the unchanged paragraphs carry 5903787430 item 9's verdict):

    • "MigrationFlagEngine in @objectstack/platform-objects/system" — src/system/index.ts re-exports the type; package.json maps ./system. "the parameter type of" the six named helpers — engine: MigrationFlagEngine at migration-flag.ts:72, 121, 143, 190, 259, 384 (readDataMigrationFlag, isDataMigrationVerified, mayActIrreversibly, recordDataMigrationRun, recordFileColumnMove, attestFreshDatastore). "part of FilesToReferencesEngine in @objectstack/service-storage" — files-to-references-migration.ts:54 is BackfillEngine & MigrationFlagEngine, exported at index.ts:94.
    • "SeedTenancyLedger in @objectstack/metadata-protocol, the type of a SeedTenancySeam's ledger" — ledger?: SeedTenancyLedger at seed-tenancy-backfill.ts:327; SeedTenancySeam is a package export (index.ts:106). The marker's unchanged "each the parameter type of a published helper" holds for the ledger through backfillSeedTenancy(seam: SeedTenancySeam | undefined, …).
    • "Each now requires findOne where it required find" — base: a required find returning a promise of an array; head: a required findOne(object, options) returning a promise of a row or null, in both interfaces. "a hand-written stand-in that provides only find no longer satisfies either type" — a required member, true. "ships as minor under the launch-window convention for accept-set narrowings" — check-changeset-no-major.mjs's header, text unchanged. "The ObjectQL engine has both methods" — carried from 5903787430 item 3.
    • The fix line: findOne(object, options) matches the declared signature; "the row whose where.id matches, or null" matches the engine's contract and readDataMigrationFlag's own if (!row || row.id !== migrationId) return null.
    • The last paragraph: readDataMigrationFlag wraps the read in try … catch { return null } (migration-flag.ts:76-111), so a find-only stand-in's failed read answers null — true. resolveSeedTenancySeam calls resolveSeedTenancyLedger(engine) (:380), which returns undefined unless all four of getObject, findOne, insert, update are functions (:1027), and the seam is returned as { exec, client, ledger } — ledger is undefined for a find-only host, true. "when the seed-tenancy repair applies, it says at warn that it could not record its receipt" — recordSeedTenancyReceipt is called on the applied branch only (:1532-1541), and its if (!ledger) branch logs at warn that the repair ran but the deployment has no durable record because sys_migration could not be written (:1152-1160). True.
  3. The delta touches nothing else. 225de70625..67d5021948 is one commit (chore(changeset): …) touching one file. The front matter (@objectstack/objectql: patch, @objectstack/platform-objects: minor, @objectstack/metadata-protocol: minor), the ! title and Clause-②: no (narrowing) sit outside both hunks — unchanged. No source, test or gate-ledger file moved; the net diff against main from merge-base a6866da0c7 is the same 17 files, +591/-75, that 5903787430 judged. No force-push (225de70625..67d5021948 is a fast-forward).

  4. Carry-over. Items 1-9 of 5903787430's ① stand unchanged on this head — every code path they judged is byte-identical between 225de70625 and 67d5021948. Item 2's nit (resolveSeedTenancyLedger named in the body) and item 9's "one false sentence sits inside the adr-0087 marker" are both closed by this round.

② Semver level

  • Levels unchanged from what 5903787430 judged right: @objectstack/platform-objects minor + BREAKING, @objectstack/metadata-protocol minor + BREAKING, @objectstack/objectql patch. The **BREAKING** banner and the ! title carry the break; the ADR-0087 disposition is now consistent with its own body. Right.
  • Clause-②: no (narrowing) unchanged in the changeset (outside the delta) and on PR body line 3. Right, for the reason 5903787430 gave: the diff relaxes no accept set a consumer can use and adds no capability; the level is minor either way.
  • The FAIL item is closed. The disposition no longer contradicts its body: the consumer's fix is prose under a Your fix label with no FROM/TO pair, no arrow and no migration label, and the marker says only what D7 / D8 let it say.

③ Boundary flags

Implemented-by: claude/issue-20648-migration-flag-point-lookup
Reviewed-by: session_01DEvba2nBuD4tWzfq8r8NFY

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 30, 2026 04:34
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 30, 2026
Merged via the queue into main with commit cd6d8a5 Sep 30, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20648-migration-flag-point-lookup branch September 30, 2026 05:05
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…once at the engine and RLS seams (objectstack-ai#5930 step 2) (objectstack-ai#20794)

Part of objectstack-ai#5930 — step 2 of ruling 5902355785 (the seam lowering in the
engine / RLS seams). Steps 3 (the analytics-face seams and the F5 / F11
vocabulary) and 4 (the per-face deletions) remain, so the card stays
open.
Clause-②: yes

## What this does

This implements ADR-0053 D-D1 as amended on 2026-09-30, items 1–7 and 9.
The bare-day upper bound, the `$between` split and the NULL-polarity
guards are applied once, by one shared `FilterCondition →
FilterCondition` lowering. It runs at the engine and RLS seams, after
the comparand doors and after filter-token resolution. Drivers receive
the lowered filter.

- **`@objectstack/spec/data`**: new pure module `filter-lowering.ts`,
exporting `lowerFilterCondition(filter, options?)` and
`FilterLoweringOptions`. It is exported from the `./data` subpath only,
never the root entry (the ruling's D3). The rules:
1. `$between` becomes `$gte` min and `$lte` max. A range holding a `{
$field }` or a non-pair is left whole, for the face that refuses it.
2. A `$lte` on a bare `YYYY-MM-DD` becomes `$lt`
`nextUtcCalendarDay(day)`, in the calendar-string domain, never a
storage form (D-A1). `UNBOUNDED_ABOVE` turns a lone `$lte` into `{
$null: false }`, and a `$between` keeps its minimum. Instants and
`Date`s are never widened.
3. NULL polarity, cell for cell from the four hand copies' tables. `$ne`
of a value, `$nin` and `$notContains` get `{ $or: [{ f: { $null: true }
}, { f: op }] }` (objectstack-ai#5298). Every leaf of a `$not` operand is made total
(objectstack-ai#5146).
- The module is copy-on-write, so a filter it does not change comes back
as the same reference. It is idempotent, and it recognises its own
guards and the same shapes written by an author. It never refuses. It
carries the objectstack-ai#8220 provenance mark onto every node it replaces.
`options.isDatetimeColumn` scopes rules 1–2 on a typed seam (item 7).
- **`@objectstack/objectql` `engine.ts`**: one stage function,
`resolveThenLowerWhere` (resolve, then lower), is the only way any
filter position resolves. That covers `find`, `findOne` and `count`
(`resolveWhereTokens`); `update` and `delete` (`withResolvedWhere`); and
`aggregate`'s `where`, each `aggregations[i].filter` and `having`.
`resolveWhereTokens` and `withResolvedWhere` now require the lowering
options, so no verb can resolve without lowering. The judge
(`judgeWhereAdmission`) runs the same stage. The type reader for `where`
and `aggregations[i].filter` is the object's declared `type ===
'datetime'`, the same test `SqlDriver` indexes `datetimeFields` by. For
`having` it is the aggregated row's column types
(`aggregatedRowColumnTypes`, where `max(datetime)` is `datetime`).
- **`@objectstack/plugin-security`**: `judgeCompiledComparands` (the RLS
compile seam, serving `using` and `check`) lowers every compiled policy
filter right after the two faces. `RlsFieldGuard` gains an optional
`datetime` set. `SecurityPlugin` fills it from the same declaration pass
as the field-name set (`loadObjectFieldNames`) and hands it in at both
compile sites. A guard without types reads no column as `datetime`.
-
**`scripts/adr-anchors/packages__spec__src__data__filter-lowering.ts.json`**:
pins ADR-0053 to the module (Prime Directive objectstack-ai#13).
- Changeset `.changeset/5930-shared-filter-lowering.md`:
`@objectstack/spec` minor, `@objectstack/objectql` and
`@objectstack/plugin-security` patch. It cites ADR-0053 D-D1 (amended).

No face copy is deleted, no driver file is touched, and the analytics
`where` / preview door, the read scope and the memory cube face are
untouched (step 3).

## The stop line was reached: one evaluator's answers move, on rows with
no value

The acceptance is answer invariance. Measured, the answers of every
driver face stay the same. The engine's own in-process evaluator for
`aggregations[i].filter` and `having` (F8, `having-filter.ts`) moves,
and only on rows or groups with no value. Before this change F8 was the
only face that disagreed with the others on those rows. After it, all
faces agree.

A/B probe, not committed. Each face answers the filter as written and
the lowered filter, on sqlite `SqlDriver` (F1), `InMemoryDriver` (F3),
`matchesFilterCondition` (F7) and `matchesAggregationFilter` (F8):

| Case set | Filters | Cells | Moved | Filters where the faces disagree,
before → after |
|---|---|---|---|---|
| `FILTER_LOGIC_CASES` over `FILTER_LOGIC_ROWS` | 36 | 144 | 0 | 0 → 0 |
| `TEMPORAL_CASES` (plus the resolved `tokenFilter`s) over
`TEMPORAL_ROWS` | 32 | 128 | 0 | 0 → 0 |
| the same plus a row with no value, each filter also under `$not`, plus
`$between` / `$ne` / `$nin` / `$notContains` probes | 70 | 280 | **14,
all F8** | **14 → 0** |

All 14 moved cells are a `$between` on a `datetime` column with a row
whose value is null. F8 kept that row in the range (7 cells) and dropped
it under `$not` (7 cells). F1, F3 and F7 exclude it from the range and
keep it under `$not`, which is the objectstack-ai#5146 / objectstack-ai#5298 reading. A second probe
found the same class on a number column: `{ $not: { amount: { $lt: 5 } }
}` dropped a null `amount` in F8, because JS compares `null < 5` as
true. Everywhere else the null row is kept. That probe covered the
per-aggregation filter and `having`, one cell each.

The decision on whether to keep the two aggregate seams wired is in the
report on objectstack-ai#5930, with the four-axis frame. This PR carries option A
(keep them). Dropping them (option B) removes the two `aggregate` hunks
and their two pin rows.

## Mechanism hypotheses — which held

- **H1 held**: the doors run in `lowerWhereFilterArray` and tokens
resolve after it on every verb. One helper (`resolveThenLowerWhere`)
holds "resolve, then lower", and every verb has its own pin.
- **H2 — measured: yes, a compiled policy CAN carry an unresolved date
token.** `record.signed_on <= '{today}'` compiles to `{ signed_on: {
$lte: '{today}' } }`, passes both faces, and reaches `using`'s drivers
and `check`'s `matchesFilterCondition` verbatim. Nothing resolves a
placeholder on either RLS clause. The RLS lowering therefore runs after
the faces (item 3) and reads `'{today}'` as a non-day string it leaves
as written, the same as every face does today. This is pinned.
- **H3 — measured.** (a) The RLS seam could not read declared types:
`RlsFieldGuard` carried names only, but the types are in the same
declaration `loadObjectFieldNames` reads. (b) No in-repo or example
policy compares any column against a bare day or a date token: 72
non-test `using`/`check` predicate lines, all `==`, `in`, `== null`, `!=
null` or `1 == 1`. So no real policy's rows or admitted writes change
under either reading. The seam takes the typed reading, because the
type-blind one would move `using` answers on SQL for a non-`datetime`
column (a text column holding day-prefixed strings, and `$lte
'9999-12-31'` on text) in constructible policies.
- **H4 held on F1/F3/F7; F8 is the stop-line item above.** Face suites
are green after the change. Before: `main` at `085ca6bc1c` has `Test
Core` (6/6), `Temporal Conformance (live PG + MySQL)` and `Dogfood
Regression Gate` green.
- **H5 held**: the output introduces only `$and`, `$or`, `$lt`, `$gte`,
`$lte` and `$null`. The unit table pins that closure over
`FILTER_LOGIC_CASES`, `TEMPORAL_CASES` and every row. F1, F2, F3, F6, F7
and F8 already compile those.
- **H6 held**: the three polarity functions and the `$not` totaliser are
the SQL copies' tables cell for cell. The copies stay.

## Evidence (all on head `9ca3698b67`)

- New pins:
- `packages/spec/src/data/filter-lowering.test.ts`: 46 tests. The rule
table, the item-7 scope, idempotence over the table and both case sets,
vocabulary closure, copy-on-write, provenance, and pass-through.
- `packages/objectql/src/engine-shared-filter-lowering-seam.test.ts`: 11
tests, one per verb and position, plus `{today}` resolved-then-widened,
the typed scope, the last supported day, copy-on-write and the judge.
-
`packages/plugins/plugin-security/src/rls-shared-lowering-seam.test.ts`:
14 tests. Both clauses through `RLSCompiler`, plus
`SecurityPlugin.getReadFilter` and `computeWriteCheckFilter` fed the
declared `datetime` set.
- Existing shape pins updated to expect the lowered driver input. No
asserted row count changed. The door suites for number comparands, text
operators and filter arrays now compare against the lowering of the
door's output. `rls-compiled-comparand-faces` gets the same treatment.
`rls-empty-membership-polarity`'s `not in` shape is updated, and its
admitted-row count stays 3.
- Suites after the change:
  - spec: 578 files, 17065 passed.
  - objectql: 341 files, 6729 passed.
  - plugin-security: 148 files, 3216 passed.
  - driver-sql: 200 files passed and 11 skipped (live PG/MySQL cells).
  - driver-memory: 65 files, 1470 passed.
  - driver-turso: 80 files, 2195 passed.
  - driver-sqlite-wasm: 36 files, 675 passed.
- driver-mongodb: 29 files passed; 5 skipped, because they need a real
mongod.
  - formula: 42 files, 1240 passed.
- `typecheck` is green for spec, objectql, plugin-security, the five
drivers and formula. `check:driver-conformance` is OK (50 cells).
- Ablations, each with the seam committed and then mutated on disk
through `scripts/ablation-replace.mjs` and restored (blob equals HEAD,
`git diff HEAD` empty):
- engine `resolveThenLowerWhere` without the lowering: 8 of 11 seam pins
red, across every verb and all three `aggregate` positions.
  - RLS `judgeCompiledComparands` without the lowering: 8 of 14 red.
- `SecurityPlugin` without the `datetime` hand-off: the 2 plugin-level
pins red.
- The pins that stay green are the pass-through rows, which hold with or
without the lowering.
- Gates: `node scripts/pm/dispatch-gates.mjs --commands` derived 103
families; all 103 were run with exit 0, and `--ran` reports 0
NOT-MEASURED and 0 UNRUN. Three gates (`check:dual-build-cjs-loads`,
`check:i18n`, `check:type-check-debt`) first refused with `PREREQUISITE
NOT MET`. They were re-run green after `turbo run build
--filter='./packages/*' --filter='./packages/*/*'`. `check:generated`
shows 15 of 15 up to date after regenerating `api-surface/` and
`export-origins/`.
- Lint, as a narrowed run:
- The population is the `eslint.config.mjs` block `files:
['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}']`.
- `eslint --no-inline-config --format json` over the 13 changed source
files reports 13 files, 0 errors and 0 warnings.
- The config enables no type-aware linting (every `parserOptions` is
`ecmaVersion` / `sourceType` only), so this diff cannot move a verdict
on an untouched file.
- NOT MEASURED locally: live PostgreSQL / MySQL (the CI job covers
them), a real mongod, and Turso remote against a live server.
- Changed lines: 1318 (+1289 / −29, 17 files).

## Acceptance notes

- **Item 7, when declarations are absent at run time.** Both seams read
NO column as `datetime` when the object's declaration is missing (a
registry-less host, or a guard without types). They do not apply the
rule type-blind. This keeps every driver's answer where it was
(`SqlDriver` also widens nothing without a declaration). The step-4
deletion cards should re-read this: once a face's copy is gone, a
declaration-less path gets no whole-day bound.
- **Two sibling files outside the declared file surface.**
`security-plugin.ts` changes in two places: the `datetime` set is read
in the existing declaration pass, and it is handed in at the two compile
sites. The ADR anchor is one new JSON file. Neither adds a seam.
- **RLS `check` with a date token.** `os validate` refuses a
`{placeholder}` in a read-scope `using` clause
(`validate-rls-predicate-enforceability.ts` judges it through the
engine). A `check` clause is not judged there, and nothing resolves the
token at run time. A `check` of `record.signed_on <= '{today}'`
therefore compares against the literal text, and every ISO value sorts
below `{`. Public-door reach is not measured. Carrier: none.
- **The analytics read scope.** It reads `using` through
`getReadFilter`, so from this PR on it receives the RLS seam's lowered
policy. For a policy with a bare-day `$lte` on a `datetime` column (none
in-repo), it now keeps the whole day, which is objectstack-ai#20733's direction.
objectstack-ai#20733 itself (the scope's own bound on a caller's filter) is step 3 and
is not addressed here.
- Serial: objectstack-ai#20766 landed before this PR and was merged in cleanly. objectstack-ai#20745
has not landed. It edits other regions of `engine.ts` and
`filter.zod.ts` prose, and whichever lands second merges `main`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants