Skip to content

Commit b7f8a2e

Browse files
refactor(objectql): move the migration-flag and attestation bodies out of engine.ts into engine-migration-flags.ts (#25522 cut 13) (#26403)
<!-- claude-projects-attribution: {"github_login":"os-project-manager"} --> _Requested by **Leehom** · [project thread](https://claude.ai/code/project/chan_01TenzsRopZj4jMbvjX8uz1X?thread=cmsg_01TenzsRopZj4jMbvjX8uz1XAdqJmXbzB3BaxdJyTaKQQW)_ Part of #25522 Clause-②: no Metadata-protocol: none Ruling-ref: 6348891160 ## 维护者速读 - **改了什么**:把 `engine.ts` 里「迁移标志与证明」一段的 8 个方法体原样搬进新文件 `engine-migration-flags.ts`,类上方法签名与注释不变,只把方法体换成转交;顺手改正第 12 刀留下的两处过时说明(一行文件头注释、一个 ADR 锚条目)。`engine.ts` 22,517 → 22,423 行。 - **为什么改**:#25522 按缝拆分 `engine.ts` 的第 13 刀;本段在刀前重测的缝表里净减行最多,且不与在途 PR 重叠。席位为本刀裁定了「宿主 setter」:被搬方法体写入的类字段,经宿主上的一对 get/set 访问器写回实例。 - **风险与回滚**:纯搬移,无行为变化;公开 `.d.ts` 六个文件逐字节不变;setter 的读写回实例由一次性探针在新旧两版上逐行对照证明。回滚 = revert 本 PR。 - **先看**:`packages/objectql/src/engine.ts` 里 `announceOpenMigrationGates` 的转交桩(唯一带 setter 的一处)、`packages/objectql/src/engine-migration-flags.ts`(搬入的方法体与 `MigrationFlagsHost`)、`scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json`(锚段落去掉重复的一半)。 - **两分钟验证**:`pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2`,应见 472 个测试文件全部通过。 - **需要你做的**:无,席位落地。 Cut 13 of the `packages/objectql/src/engine.ts` split (#24586 ruling A, recorded in `6270097840`). The claim is `6430872749`, which carries the seat's host-setter ruling for this cut. Eight bodies of the "Migration flags and attestations" section move unedited into a new module, `packages/objectql/src/engine-migration-flags.ts`: `supplyTenantKeyCertifiedResolver` (hand a driver the "is this database certified" question), `isOrganizationOwnershipCertified` (its answer), `readFileReferencesFlagRow` and `isValueShapesMigrationVerified` (the two memoized flag readers), `readMigrationFlagVerified` (the one `sys_migration` row read behind them), `noteAdmittedValueShapeViolation` (tally a violation this boot admitted), `announceOpenMigrationGates` (the once-per-boot open-gate advisory) and `wasDatastoreCreatedFromEmpty`. Two stale path readers of cut 12 are corrected in the same PR. No behaviour changes. #25522 stays open for the later cuts. Branch `claude/project-thread-9q75rj` (the card thread's harness-pinned branch), cut from `origin/main` at `756220c4a`, which contains cut 12's merge (`2e7127bc3a`, #26371). Nothing was edited before that merge landed: the move was dry-run on #26371's head while it was in the queue, and the four files committed here are byte-identical to that dry run (`git hash-object`, 4 of 4; `engine.ts` at `756220c4a` is the same blob, `a17c9e9b93e6`, as at #26371's head). ## What changes ### Why this area Re-measured on `756220c4a` with cut 11's scripts, unchanged (both screens: query-options erasure sites and module-helper closure). `engine.ts` is 22,517 lines, 7,134 of them from the last 30 days (`git blame --since=2026-09-27T01:21Z`). `scripts/pm/git-history.mjs count`: 131 commits in 30 days, 35 in 7. `class ObjectQL` holds 318 members, 255 methods and accessors. <details><summary>Seam table at 756220c (41 sections)</summary> | range | section | first … last member | lines | 30-day lines | methods | inferred public | U | private statics | |---|---|---|---|---|---|---|---|---| | 4698-4923 | Instance state and constructor | `txStore` … `constructor` | 226 | 50 | 0 (0 public) | - | 0 | - | | 4924-4946 | Status and registry accessor | `getStatus` … `registry` | 23 | 0 | 2 (2 public) | getStatus | 0 | - | | 4947-5290 | Hook and function registry | `registerHook` … `triggerHooks` | 344 | 6 | 16 (16 public) | registerHook, triggerHooks | 1 | - | | 5291-5646 | Hook dispatch internals | `hasHooksFor` … `assertBulkPerRowHookBudget` | 356 | 61 | 7 (0 public) | - | 0 | - | | 5647-5807 | Action registry and activation | `registerAction` … `setActionActive` | 161 | 19 | 10 (10 public) | - | 1 | - | | 5808-6089 | Registered resolvers and write-gate probes | `_heldFileResolver` … `registerInsertRecomputeListener` | 282 | 116 | 6 (6 public) | - | 0 | - | | 6090-6843 | Readable-ids security and permission/formula resolution | `readableIdsSecurityReported` … `resolveDefaultPermissions` | 754 | 546 | 15 (2 public) | - | 0 | - | | 6844-6950 | Authz invalidation and middleware registration | `writeEpoch` … `hasObjectMiddleware` | 107 | 18 | 4 (4 public) | - | 0 | - | | 6951-7128 | Execution plumbing (middleware, session, provenance, users) | `executeWithMiddleware` … `buildUser` | 178 | 45 | 7 (0 public) | - | 0 | - | | 7129-7337 | Driver options and engine posture (stubs, cut 12) | `buildDriverOptions` … `resolveEnginePosture` | 209 | 150 | 6 (0 public) | - | 0 | - | | 7338-7547 | Organization probe and system-insert organization | `probeInstallOrganizations` … `transactionCoversDriverFor` | 210 | 45 | 4 (0 public) | - | 0 | - | | 7548-7913 | Field defaults | `resolveNowDefault` … `defaultFieldEntries` | 366 | 4 | 6 (0 public) | - | 0 | - | | 7914-8526 | Autonumber | `applyAutonumbers` … `seedAutonumber` | 613 | 80 | 6 (0 public) | - | 1 | - | | 8527-9191 | Package, app, plugin and driver registration | `registrationConversionWarned` … `registerDriver` | 665 | 170 | 6 (3 public) | registerApp, registerDriver | 0 | - | | 9192-9400 | Datasource registry | `supplyFileColumnsMovedResolver` … `listUnavailableDatasources` | 209 | 1 | 8 (7 public) | - | 3 | - | | 9401-9552 | Write guard and realtime publish | `assertWriteAllowed` … `publishBulkDataEvent` | 152 | 18 | 4 (1 public) | - | 0 | - | | 9553-9651 | Validation-message i18n | `setI18nService` … `objectDisplayLabel` | 99 | 14 | 4 (1 public) | - | 0 | - | | 9652-10655 | Referential integrity and master-detail resolution | `assertReferencesResolve` … `masterDeclaredFields` | 1004 | 415 | 12 (0 public) | - | 0 | - | | 10656-10771 | Dangling-reference and cascade inspection | `inspectDanglingReferences` … `inspectCascadeDelete` | 116 | 48 | 2 (2 public) | - | 0 | - | | 10772-10866 | Crypto and tenancy providers | `setCryptoProvider` … `setTenancyPostureProvider` | 95 | 20 | 4 (4 public) | - | 1 | - | | 10867-11286 | Secret and internal-field handling | `encryptSecretFields` … `resolveInternalField` | 420 | 61 | 9 (3 public) | - | 0 | - | | 11287-11355 | Schema lookup and filter judging | `getSchema` … `judgeFilter` | 69 | 48 | 3 (2 public) | - | 0 | - | | 11356-11621 | Datasource routing | `LIFECYCLE_DATASOURCE` … `setDatasourceMapping` | 266 | 20 | 8 (3 public) | setDatasourceMapping | 0 | - | | 11622-11789 | Lifecycle init and health | `init` … `resolvePrimaryDatasource` | 168 | 16 | 3 (3 public) | init | 0 | - | | 11790-11863 | Media and value-shape presence | `objectHasMediaField` … `valueShapeStrictFor` | 74 | 13 | 4 (0 public) | - | 0 | mediaFieldPresence, coveredValueFieldPresence | | **11864-12546** | **Migration flags and attestations** (this PR) | `isFileReferencesMigrationVerified` … `wasDatastoreCreatedFromEmpty` | 683 | 81 | 17 (8 public) | - | 0 | - | | 12547-12569 | destroy | `destroy` … `destroy` | 23 | 0 | 1 (1 public) | destroy | 0 | - | | 12570-12588 | Expand/cascade limits (statics) | `MAX_EXPAND_DEPTH` … `MAX_BULK_PER_ROW_HOOK_ROWS` | 19 | 0 | 0 (0 public) | - | 0 | MAX_EXPAND_DEPTH, MAX_CASCADE_DEPTH | | 12589-13039 | Roll-up summaries | `autonumberCounters` … `recomputeSummaries` | 451 | 0 | 7 (1 public) | - | 0 | - | | 13040-13293 | Related-record expansion | `expandRelatedRecords` … `expandRelatedRecords` | 254 | 10 | 1 (0 public) | - | 0 | - | | 13294-13491 | File-reference hydration (stubs, cut 11) | `resolveFileReferences` … `readParentDerivedFiles` | 198 | 183 | 5 (0 public) | - | 0 | - | | 13492-13970 | Query lowering | `resolveWhereTokens` … `requireFindOnePredicate` | 479 | 272 | 9 (0 public) | - | 0 | - | | 13971-14527 | find / findOne | `find` … `staticReadonlyCreateStrip` | 557 | 142 | 4 (2 public) | - | 0 | - | | 14528-15185 | validate | `validate` … `validate` | 658 | 447 | 1 (1 public) | - | 0 | - | | 15186-16532 | insert / insertMany | `readUpdatePriorRow` … `insertMany` | 1347 | 439 | 3 (2 public) | - | 1 | - | | 16533-18466 | update | `update` … `update` | 1934 | 275 | 1 (1 public) | - | 0 | - | | 18467-20007 | Delete cascade and reference walking | `planCascadeAtomicity` … `organizationOwnedRowCounts` | 1541 | 733 | 22 (1 public) | - | 0 | 6 statics | | 20008-20577 | delete | `delete` … `delete` | 570 | 47 | 1 (1 public) | - | 0 | - | | 20578-21197 | count / aggregate | `count` … `aggregate` | 620 | 236 | 3 (2 public) | - | 0 | - | | 21198-21506 | execute and transactions | `execute` … `enforceTransactionOrigin` | 309 | 74 | 8 (2 public) | - | 0 | - | | 21507-21799 | Compatibility API | `registerObject` … `create` | 293 | 52 | 16 (16 public) | - | 0 | - | </details> The full per-method screen and price list are in the project file `pm-orders/server/25522-cut13-seam-table.md`; the move script is `pm-orders/server/evidence/25522/move-cut13.mjs`. | area | paying bodies under both screens | 30-day lines in them | net | verdict | |---|---|---|---|---| | **Migration flags and attestations** (this PR) | 8 bodies (154 lines) | 50 of 154 | priced -91; **-94 measured** | pays most; its one field write is the setter the seat ruled | | Readable-ids security and permission/formula resolution | 6 bodies (three more stay on shared module helpers) | 78 of 127 | about -87 | second; the hottest section in the file (546 of 754 lines from the last 30 days) | | Package registration, count, Delete cascade and the rest | - | - | -30 to 0 | blocked by a shared helper, or pays little | Nine members of the section stay: `isFileReferencesMigrationVerified` (ADR-0104 names it by path), `recordObservedDeviation` and `retractCreationAttestation` (each writes through `this.update`, a self-call the screen keeps in the class), and six bodies too short to pay for a host (`haveFileColumnsMoved`, `readMigrationFlagMemoized`, `readMigrationFlagRowMemoized`, `valueShapeViolationsAdmitted`, `admittedViolationSink`, `invalidateDataMigrationFlags`). Every field of the section stays declared where it was. ### The shape - The class keeps all eight methods with name, modifiers, signature, docblock and position. Each body is now one host literal of exactly the `MigrationFlagsHost` members it reads, plus a `.call`, marked `[#25522 cut 13]`, as cuts 8, 10, 11 and 12 did. Fields go as getters over a `readX` closure (`logger`, `_registry`, `admittedValueShapeViolations`, `drivers`); methods go as late-bound closures (`isOrganizationOwnershipCertified`, `findOne`, `readMigrationFlagRowMemoized`, `readMigrationFlagMemoized`, `recordObservedDeviation`, `retractCreationAttestation`, `objectHasMediaField`, `objectHasCoveredValueField`, `readMigrationFlagVerified`), so a spy or double set on the engine is still the one a call reaches. - **The setter (seat ruling for this cut).** `announceOpenMigrationGates` is the one moved body that ASSIGNS a field (`this.migrationGatesAnnounced = true`). Its host carries the accessor pair `get migrationGatesAnnounced() { return readMigrationGatesAnnounced(); }, set migrationGatesAnnounced(v) { writeMigrationGatesAnnounced(v); }`, where `writeMigrationGatesAnnounced = (v: ObjectQL['migrationGatesAnnounced']) => { this.migrationGatesAnnounced = v; }` closes over the instance as the getters' `readX` do. The field stays declared on the class with its initialiser and modifiers untouched; `MigrationFlagsHost` types it `migrationGatesAnnounced: ObjectQL['migrationGatesAnnounced']`, not `readonly`. No other moved body assigns a field, so no other setter exists (`noteAdmittedValueShapeViolation` calls `.set` on the `admittedValueShapeViolations` map it reads; that is a read of the field). - **One named host.** `MigrationFlagsHost` types each member as the class member (`ObjectQL['logger']` and so on); each function's `this` is the `Pick` of what it reads. The module imports the engine only as a type. - **One type gains `export`.** `MigrationFlagRead` (the shape of one `sys_migration` flag read) is the return type of two moved bodies and of members that stay. It stays declared in `engine.ts`, byte for byte, and gains `export` in place so the module can name it with `import type`; no second copy, and no runtime import of `engine.ts`. `index.ts` names its exports singly, so the published `.d.ts` does not move (below). - **Imports.** The module imports `DATA_MIGRATION_FLAG_OBJECT`, the three migration ids, `ORGANIZATION_OWNERSHIP_SHAPE_GENERATION`, `organizationOwnershipShapeGenerationOf`, `isDataMigrationFlagVerified` and `hasMovedFileColumns` (`@objectstack/spec/system`), `ExecutionContext` (type), `IDataDriver` (type, `@objectstack/core`), `isMissingTableError`, `valueShapePostureSetByEnv`, `mediaPostureSetByEnv` and the type `AdmittedValueShapeViolation`. `engine.ts` drops `VALUE_SHAPES_MIGRATION_ID`, `ORGANIZATION_OWNERSHIP_MIGRATION_ID`, `ORGANIZATION_OWNERSHIP_SHAPE_GENERATION`, `organizationOwnershipShapeGenerationOf`, `hasMovedFileColumns`, `valueShapePostureSetByEnv` and `mediaPostureSetByEnv`, whose last code readers moved (`hasMovedFileColumns` is still named in a docblock that stays), and gains the module import with a three-line comment. - The module header names what moved, what stays and why, and names ADR-0104 and ADR-0131 without a decision letter. ### Net lines (the seat's rule) `engine.ts` goes from 22,517 to 22,423 lines, **net -94** (66 added, 160 removed). The module is 295 lines. With the two path readers below, the diff against `origin/main` is 527 changed lines over 4 files (+364 / -163). ### Readers of the moved lines - **No reader of the moved bodies needed a change.** No `objectql/src/engine.ts#NAME` pointer under `packages/spec/**`, `docs/adr/**`, `docs/qa/**`, `content/**` or `scripts/**` names a moved member (ADR-0104's pointer names `isFileReferencesMigrationVerified`, which stays). The prose that names them (`check-durability-degradation-log-level.mjs` comments, test headers, `sql-driver.ts`, `seed-tenancy-backfill.ts`, `packages/spec/src/contracts/objectql-engine.ts`) still resolves, because the methods stay on the class. No ADR anchor paragraph describes a moved body. No moved body reads `isSystem` (the two `{ isSystem: true }` literals are context construction, not reads; `check-system-context-census` passes unchanged at 153 read sites), holds a `query-options/no-any-erasure` site (8 at base and head), a tenant-audit untyped receiver, a `.canReadObject(` or `referenceTargetOf(` call (the two core census pins), or a `new Error(`. `isMissingTableError` keeps its two-argument form (`driver-error-classification.callers.test.ts`). - **Decision-letter count:** `check-adr-anchors` at base `756220c4a` (clean detached worktree): 91 anchored files, **1,877** decision-letter citations, 59,318 resolving citations across 8,374 files. At head `c7df09f88`: 91, **1,877**, 59,321 across 8,375. No moved body comment cites a decision letter, and the module is not anchored, so the count is equal. ### Fold-ins (stale path readers of cut 12) - `engine-realtime-publish.ts`'s header said `accessibleOrganizationsOf` stays in `engine.ts`; cut 12 moved it to `engine-driver-options.ts`. The sentence now says so (two comment lines). - `scripts/adr-anchors/packages__objectql__src__engine-execute-transactions.ts.json` (cut 7) still carried the transaction-seam paragraph's `buildDriverOptions` half, which `engine-driver-options.ts`'s entry has carried since cut 12. That half now has one home, with `buildDriverOptions`' body: this entry's invariant is now the `engine.ts` entry's text exactly (the opening sentence, the `enforceTransactionOrigin` sentences and the "Never add a companion transaction" clause), with its file and three ids unchanged. The count above includes this edit. ## Tests <details><summary>Published .d.ts identical (ruling 6348891160), with a positive control</summary> Base: `@objectstack/objectql` built with `engine.ts` restored to `756220c4a` (blob `a17c9e9b93e6` checked) and the module removed; head built at `c7df09f88`; the tree restored and checked (`git diff HEAD` empty, both blobs equal to HEAD). `announceOpenMigrationGates.call(host` occurs 0 times in the base `dist/index.mjs` and once in the head's. All six declaration files are **byte-identical**, and equal to the hashes cuts 7 to 12 recorded: | file | sha256, base = head | |---|---| | `core.d.mts` | `ef178bf0725eccc2f358f082ce13dc5122a6d5a53c19ef630017256f67de03b4` | | `core.d.ts` | `1d20303a455a909e4b55fd0802fb0b0bcb59ec51784daf27d5ef4c0659410953` | | `index.d.mts` | `ccf900240aa137abe72f82b8abfc872d42c5645467400134c580285f2a3b3090` | | `index.d.ts` | `0454d5e70b156d8effbf27b8915587c7cbee241dcd7d698013c0a4c369a02aa5` | | `util-CVzOGxAn.d.mts` | `85ef1cbec2b016d5f7afabaa21689c86c6dffc573076fdfdeb8ea0f1a1b1ad53` | | `util-CVzOGxAn.d.ts` | `85ef1cbec2b016d5f7afabaa21689c86c6dffc573076fdfdeb8ea0f1a1b1ad53` | **Positive control:** `scripts/ablation-replace.mjs` dropped `private` from the `readFileReferencesFlagRow` stub (anchor hit once, 1 to 0; blob `4e64ab23e37a` to `93b9d017a031`) and rebuilt: all six hashes and the chunk name changed (`util-BnUvXrEZ.d.ts` `8939a58e8fee…`, `index.d.ts` `63b9dacdf4c1…`, `core.d.ts` `b6d8ba14102d…`), and the chunk now prints `readFileReferencesFlagRow()` (returning a `Promise` of `MigrationFlagRead`) as a public member. The tool restored the file (blob `4e64ab23e37a` equal to HEAD, `git diff HEAD` empty); after a rebuild, all six files compared equal to the head build again. </details> <details><summary>Proof of move-only (TypeScript-AST leaf-token compare)</summary> `pm-orders/server/evidence/25522/astcmp.mjs` (unchanged from cuts 9 to 12), base `engine.ts` against the head module: - All eight bodies are **SAME** in parameters, return type, type parameters, body tokens and `async`: `supplyTenantKeyCertifiedResolver` 97 tokens / 16 lines, `isOrganizationOwnershipCertified` 166 / 19, `readFileReferencesFlagRow` 19 / 7, `isValueShapesMigrationVerified` 17 / 6, `readMigrationFlagVerified` 232 / 37, `noteAdmittedValueShapeViolation` 96 / 18, `announceOpenMigrationGates` 201 / 40, `wasDatastoreCreatedFromEmpty` 94 / 11. - Class: 318 members at both ends, in the same order. 310 are byte-identical, and the 8 stubs keep docblock, modifiers and signature byte-identical up to the body. - Top level: 225 of 229 base statements are byte-identical in head. The other four: the class; the `@objectstack/spec/system` import less the five bindings above; the `./validation/record-validator.js` import less `valueShapePostureSetByEnv` and `mediaPostureSetByEnv`; `interface MigrationFlagRead`, now `export interface` with the same body. Head adds the module import. </details> <details><summary>Late-binding probe for the setter (one-off, not committed)</summary> `pm-orders/server/evidence/25522/zz-probe-cut13.mjs` drives the built `dist/` of base and head with instance doubles. Both runs print the same 28 lines (diff empty, apart from the engine's own creation log timestamps). The setter lines: - `announceOpenMigrationGates()` on a fresh engine: `engine.migrationGatesAnnounced` reads `false` before and `true` after (the write through the moved body is read back on the instance); a second call returns before reading the registry. - Setting `engine.migrationGatesAnnounced = false` on the instance makes the next call run again; setting it `true` makes the next call skip; `invalidateDataMigrationFlags()` (stays in the class) resets it, and the next call runs. The rest: `announceOpenMigrationGates` reaches instance doubles of `objectHasMediaField`, `objectHasCoveredValueField` and `readMigrationFlagVerified` and logs the media advisory; the two public flag readers reach an instance `findOne` double once per memo slot with `{ isSystem: true }` and log their verified lines; a throwing `findOne` and an absent ledger answer inconclusive; `isOrganizationOwnershipCertified` answers `false` on a missing table and rethrows any other error; the resolver `supplyTenantKeyCertifiedResolver` hands a driver reaches an `isOrganizationOwnershipCertified` double set on the instance AFTER the hand-over, and a driver that throws is logged at debug; the admitted-violation tally and its two deviation calls; `wasDatastoreCreatedFromEmpty` reads a `drivers` map replaced on the instance. </details> <details><summary>Suites</summary> At `c7df09f88`: - `pnpm --filter @objectstack/objectql typecheck` (`tsc --noEmit` + `check:test-typecheck`): VERDICT command-exit 0. `pnpm exec eslint --no-inline-config packages/objectql`: exit 0. - `pnpm --filter @objectstack/objectql exec vitest run --project local --maxWorkers=2`: **472 files, 8,789 tests passed** (VERDICT command-exit 0). The suites that drive these bodies (`adr0131-tenant-key-certified-supply`, `value-shape-scan-advisory`, `sys-migration-single-row-route`, `adr0104-attestation-evidence`, `engine-find-missing-table-log-level`, `engine.test`) are among them. - `vitest run --project repo` in every package that has one, after the `^...` closure builds: `core` 6/6 files (83 tests), `objectql` 1/1 (5), `rest` 5/5 (280 passed, 18 skipped), `runtime` 3/3 (947), `spec` 79/79 (1,279), `types` 1/1 (11), all exit 0. - Dependants: all eight members keep their declarations and all six published `.d.ts` files are byte-identical, so no dependant can see a difference. CI's `TypeScript Type Check` covers them. </details> ## Acceptance notes - **Serial check**, re-read at 01:58Z (31 open PRs): three touch `packages/objectql/**`. #26337 (ADR-0142 phase 2, head `e45133760`) edits `engine.ts` in nine hunks (an import near line 245, three declarations above the class, and the `insert` / `update` bodies); none is in the moved section, on the import lines this cut edits, or on `MigrationFlagRead`. `git merge-tree --write-tree` of this head with its head merges cleanly (exit 0). Under the card's rule 4 whichever lands second merges `main`. #26378 edits `packages/objectql/README.md` only; #26138 edits `validation/rule-validator.test.ts` only. No open PR touches `engine-realtime-publish.ts` or `scripts/adr-anchors/`. - **Receiver notes, not a change in what runs:** `supplyTenantKeyCertifiedResolver` hands the driver `() => this.isOrganizationOwnershipCertified()`; `this` is now the host, whose member is a late-bound closure over the engine, so the call still lands on the engine's method (the probe sets a double after the hand-over and the resolver reaches it). `this.findOne(...)` keeps the engine as its receiver through the closure. `this._registry.getObject(...)`, `this.drivers.values()`, `this.admittedValueShapeViolations.get/set(...)` and `this.logger.info(...)` keep their own receivers. - **Next cut from this screen:** re-measured on this head, Migration flags pays nothing further (six short bodies, 22 lines). Readable-ids security and permission/formula resolution pays about -87 with six bodies (three more stay on shared module helpers) and is the hottest section in the file; the rest pay -30 or less. ## Verification At `c7df09f88`: - **Gates:** `dispatch-gates --commands` derived 64 families on the real diff. Every one was run, each exit code captured before any pipe, then reconciled with `--ran`: 64 accounted for, 63 run green, 1 **NOT MEASURED** (exit 3, PREREQUISITE NOT MET; CI runs it): `check:dual-build-cjs-loads` (needs every package's `dist/`). Two first answered a shallow-clone prerequisite and were re-run green after `git fetch --shallow-since=2026-07-22 origin main`: `check-engine-split-ratio --days 90` and `check-plugin-teardown-shape --self-test`. Named readings: `check:adr-anchors` OK, `check:nul-bytes`, `check-engine-split-ratio --self-test`, `check:lean-entry-closure`, `check-affected-docs`, all exit 0. - **Hand gates outside the derivation**, all exit 0: `check-adr-anchors` at base and head (above), `check-system-context-census` (153 read sites), `check-tenant-audit-census` (244 write sites certified, every `UNTYPED_RECEIVERS` row matched), `check-adr-symbol-anchors` (2,664 anchors resolve), `check-platform-checklist --changed-from origin/main` (13 anchors judged, 0 lost), `check-commit-card-trailers --range origin/main..HEAD` and `check-nul-bytes`. - **The dry run's wider list:** on #26371's head (the same `engine.ts` bytes and the same four files), the derivation named 111 families, because that branch's diff also carried cut 12's files; all 111 ran, 107 green and four answering PREREQUISITE NOT MET exactly as on cut 12 (shallow clone, client builds, every package's `dist/`), among them `check:query-options-erasure`, `check:engine-double-contract`, `check:type-check-debt`, `check:objectql-double-limit` and `check:where-matcher`, all exit 0. - `origin/main` moved eight commits after the cut (to `7c70f91ce`); none touches `packages/objectql/src/**`, `scripts/adr-anchors/**` or the census scripts (the nearest is #26244's `packages/spec` seed-file addition), so the branch is not merged with it here; CI runs the merge ref. - No ablation, because no tested behaviour changed. The whole `Test Core` farm is CI's. `skip-changeset`: move-only. The published `.d.ts` files are byte-identical, `liveness/` and every other published path are untouched, and the module's runtime is the same code under a different file name. The other edits are one source comment and one anchor file. ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. (No `packages/spec` edit.) - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes (scoped: `eslint --no-inline-config packages/objectql`, exit 0). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. (Nothing publishes differently: `skip-changeset`.) - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. (Not touched.) - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the maintainer channel the fleet writes in Chinese). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Wzm9du2KfnvCibhjZZYgyq --- _Generated by [Claude Code](https://claude.ai/code/session_01Wzm9du2KfnvCibhjZZYgyq)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 57ab06b commit b7f8a2e

4 files changed

Lines changed: 364 additions & 163 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* [#25522 cut 13] The migration-flag and attestation bodies of `ObjectQL`,
5+
* moved out of `engine.ts`: `supplyTenantKeyCertifiedResolver` (hand a driver
6+
* the "is this database certified" question), `isOrganizationOwnershipCertified`
7+
* (that question's answer), `readFileReferencesFlagRow` and
8+
* `isValueShapesMigrationVerified` (the two memoized flag readers),
9+
* `readMigrationFlagVerified` (the one `sys_migration` row read behind them),
10+
* `noteAdmittedValueShapeViolation` (tally a violation this boot admitted),
11+
* `announceOpenMigrationGates` (the once-per-boot open-gate advisory) and
12+
* `wasDatastoreCreatedFromEmpty` (did this process create its store).
13+
*
14+
* A MOVE, not a redesign. Each exported function below is the body of the
15+
* `ObjectQL` method of the same name as it stood, re-indented one level and
16+
* otherwise unedited. Each method's docblock stays on the method in
17+
* `engine.ts`, which is still the contract. ADR-0104 and ADR-0131 govern what
18+
* these bodies decide; their decisions are cited on those docblocks.
19+
*
20+
* What stays in `engine.ts`: `isFileReferencesMigrationVerified` (ADR-0104
21+
* names it by path), `recordObservedDeviation` and `retractCreationAttestation`
22+
* (each writes through `this.update`), the bodies too short to pay for a host
23+
* (`haveFileColumnsMoved`, `readMigrationFlagMemoized`,
24+
* `readMigrationFlagRowMemoized`, `valueShapeViolationsAdmitted`,
25+
* `admittedViolationSink`, `invalidateDataMigrationFlags`), and every field
26+
* of the section, each declared where it was.
27+
*
28+
* ## How the moved bodies reach the class
29+
*
30+
* The shape #23050 set for `rest-server.ts`, applied per method as
31+
* `engine-driver-options.ts` applies it. The class keeps all eight methods
32+
* with name, modifiers, signature, docblock and position, and each body is
33+
* one literal of exactly the {@link MigrationFlagsHost} members that method
34+
* reads, plus a `.call`. The fields a body reads (`logger`, `_registry`,
35+
* `admittedValueShapeViolations`, `migrationGatesAnnounced`, `drivers`) go as
36+
* GETTERS, so each read happens when the body makes it, as it did. The one
37+
* field a body ASSIGNS, `migrationGatesAnnounced`, goes as a getter and
38+
* SETTER pair over the instance, so the write lands on the engine and the
39+
* next read, from either side, sees it. The methods they call go as
40+
* late-bound closures over the instance, so a double or spy set on the engine
41+
* is still the one a call reaches.
42+
*
43+
* This module imports the engine only as a TYPE, so there is no module cycle
44+
* at run time, and nothing re-exports it, so the published surface does not
45+
* move.
46+
*/
47+
48+
import {
49+
DATA_MIGRATION_FLAG_OBJECT,
50+
FILE_REFERENCES_MIGRATION_ID,
51+
VALUE_SHAPES_MIGRATION_ID,
52+
ORGANIZATION_OWNERSHIP_MIGRATION_ID,
53+
ORGANIZATION_OWNERSHIP_SHAPE_GENERATION,
54+
organizationOwnershipShapeGenerationOf,
55+
isDataMigrationFlagVerified,
56+
hasMovedFileColumns,
57+
} from '@objectstack/spec/system';
58+
import type { ExecutionContext } from '@objectstack/spec/kernel';
59+
import type { IDataDriver } from '@objectstack/core';
60+
import { isMissingTableError } from '@objectstack/metadata/errors';
61+
import { valueShapePostureSetByEnv, mediaPostureSetByEnv } from './validation/record-validator.js';
62+
import type { AdmittedValueShapeViolation } from './validation/record-validator.js';
63+
import type { MigrationFlagRead, ObjectQL } from './engine.js';
64+
65+
/**
66+
* The `ObjectQL` members the functions below read — every one of them, and no
67+
* other; each function's `this` is the `Pick` of the members IT reads. Each is
68+
* typed AS the class member (an indexed access on the class), so a change to a
69+
* member's type on the class is a change here too, and naming a member the
70+
* class does not have is a compile error. See the module header for which
71+
* members are getters, and which one also has a setter.
72+
*/
73+
export interface MigrationFlagsHost {
74+
readonly logger: ObjectQL['logger'];
75+
readonly _registry: ObjectQL['_registry'];
76+
readonly admittedValueShapeViolations: ObjectQL['admittedValueShapeViolations'];
77+
migrationGatesAnnounced: ObjectQL['migrationGatesAnnounced'];
78+
readonly drivers: ObjectQL['drivers'];
79+
isOrganizationOwnershipCertified: ObjectQL['isOrganizationOwnershipCertified'];
80+
findOne: ObjectQL['findOne'];
81+
readMigrationFlagRowMemoized: ObjectQL['readMigrationFlagRowMemoized'];
82+
readMigrationFlagMemoized: ObjectQL['readMigrationFlagMemoized'];
83+
recordObservedDeviation: ObjectQL['recordObservedDeviation'];
84+
retractCreationAttestation: ObjectQL['retractCreationAttestation'];
85+
objectHasMediaField: ObjectQL['objectHasMediaField'];
86+
objectHasCoveredValueField: ObjectQL['objectHasCoveredValueField'];
87+
readMigrationFlagVerified: ObjectQL['readMigrationFlagVerified'];
88+
}
89+
90+
/** The body of `ObjectQL#supplyTenantKeyCertifiedResolver`; the method's docblock there is its contract. */
91+
export function supplyTenantKeyCertifiedResolver(
92+
this: Pick<MigrationFlagsHost, 'isOrganizationOwnershipCertified' | 'logger'>,
93+
driver: IDataDriver,
94+
): void {
95+
const sink = driver as unknown as {
96+
setTenantKeyCertifiedResolver?: (resolve: () => Promise<boolean>) => void;
97+
};
98+
if (typeof sink.setTenantKeyCertifiedResolver !== 'function') return;
99+
try {
100+
sink.setTenantKeyCertifiedResolver(() => this.isOrganizationOwnershipCertified());
101+
} catch (e: any) {
102+
// A driver that did not take the resolver answers "not certified": it
103+
// keeps the two forms distinct and REPORTS a destructive recreate_index,
104+
// which is never applied unattended (safe-only boot reconcile, the
105+
// artifact gate, `os migrate apply --allow-destructive`).
106+
this.logger.debug('Driver declined the ADR-0131 certification resolver', {
107+
driverName: driver.name,
108+
error: e?.message ?? String(e),
109+
});
110+
}
111+
}
112+
113+
/** The body of `ObjectQL#isOrganizationOwnershipCertified`; the method's docblock there is its contract. */
114+
export async function isOrganizationOwnershipCertified(
115+
this: Pick<MigrationFlagsHost, '_registry' | 'findOne'>,
116+
): Promise<boolean> {
117+
if (!this._registry.getObject(DATA_MIGRATION_FLAG_OBJECT)) return false;
118+
try {
119+
const row: any = await this.findOne(DATA_MIGRATION_FLAG_OBJECT, {
120+
where: { id: ORGANIZATION_OWNERSHIP_MIGRATION_ID },
121+
context: { isSystem: true } as ExecutionContext,
122+
});
123+
if (!row || row.id !== ORGANIZATION_OWNERSHIP_MIGRATION_ID) return false;
124+
const verified = isDataMigrationFlagVerified({
125+
id: ORGANIZATION_OWNERSHIP_MIGRATION_ID,
126+
last_run_at: String(row.last_run_at ?? ''),
127+
verified_at: row.verified_at == null ? null : String(row.verified_at),
128+
blocking: typeof row.blocking === 'number' ? row.blocking : Number(row.blocking ?? Number.NaN),
129+
});
130+
return verified && organizationOwnershipShapeGenerationOf(row.details) >= ORGANIZATION_OWNERSHIP_SHAPE_GENERATION;
131+
} catch (error) {
132+
// A store whose ledger table does not exist yet has no certificate.
133+
if (isMissingTableError(error, DATA_MIGRATION_FLAG_OBJECT)) return false;
134+
throw error;
135+
}
136+
}
137+
138+
/** The body of `ObjectQL#readFileReferencesFlagRow`; the method's docblock there is its contract. */
139+
export function readFileReferencesFlagRow(
140+
this: Pick<MigrationFlagsHost, 'readMigrationFlagRowMemoized'>,
141+
): Promise<MigrationFlagRead> {
142+
return this.readMigrationFlagRowMemoized(
143+
'fileReferencesMigrationVerified',
144+
FILE_REFERENCES_MIGRATION_ID,
145+
'[value-shape] this deployment has verified the file-as-reference migration — ' +
146+
'media value shapes are enforced and released field files may be collected ' +
147+
'(ADR-0104)',
148+
);
149+
}
150+
151+
/** The body of `ObjectQL#isValueShapesMigrationVerified`; the method's docblock there is its contract. */
152+
export async function isValueShapesMigrationVerified(
153+
this: Pick<MigrationFlagsHost, 'readMigrationFlagMemoized'>,
154+
): Promise<boolean> {
155+
return this.readMigrationFlagMemoized(
156+
'valueShapesMigrationVerified',
157+
VALUE_SHAPES_MIGRATION_ID,
158+
'[value-shape] this deployment has verified the value-shape scan — reference and ' +
159+
'structured-JSON value shapes are enforced (ADR-0104)',
160+
);
161+
}
162+
163+
/** The body of `ObjectQL#readMigrationFlagVerified`; the method's docblock there is its contract. */
164+
export async function readMigrationFlagVerified(
165+
this: Pick<MigrationFlagsHost, '_registry' | 'findOne' | 'logger'>,
166+
migrationId: string,
167+
verifiedLog?: string,
168+
): Promise<MigrationFlagRead> {
169+
if (!this._registry.getObject(DATA_MIGRATION_FLAG_OBJECT)) {
170+
return { verified: false, conclusive: false, columnsMoved: false };
171+
}
172+
try {
173+
// [#20648] The row by primary key, through the single-row route. ⛔ Not
174+
// `find` with `limit: 1`: the SQL driver reads that as page one of a
175+
// walk, and this read runs at boot BEFORE `sys_migration` is registered
176+
// with the driver — so every boot of an existing deployment printed the
177+
// driver's "Paged read ... is NOT deterministic" warning for a lookup
178+
// that cannot return two rows. `findOne` is the route it exempts.
179+
const row: any = await this.findOne(DATA_MIGRATION_FLAG_OBJECT, {
180+
where: { id: migrationId },
181+
context: { isSystem: true } as ExecutionContext,
182+
});
183+
if (!row || row.id !== migrationId) {
184+
return { verified: false, conclusive: true, columnsMoved: false };
185+
}
186+
const flag = {
187+
id: migrationId,
188+
last_run_at: String(row.last_run_at ?? ''),
189+
verified_at: row.verified_at == null ? null : String(row.verified_at),
190+
// A non-numeric count must read as "not zero", not as 0 — a bad
191+
// coercion lands on NaN, which fails the === 0 test.
192+
blocking: typeof row.blocking === 'number' ? row.blocking : Number(row.blocking ?? Number.NaN),
193+
// [#15989] Read here so both questions come off ONE row: an absent or
194+
// empty stamp is the JSON encoding, which is what every row written
195+
// before the column step existed holds.
196+
columns_moved_at: row.columns_moved_at == null ? null : String(row.columns_moved_at),
197+
};
198+
const verified = isDataMigrationFlagVerified(flag);
199+
if (verified && verifiedLog) this.logger.info(verifiedLog);
200+
return { verified, conclusive: true, columnsMoved: hasMovedFileColumns(flag) };
201+
} catch {
202+
// unreadable evidence → stay lenient, keep asking; and the columns read
203+
// as NOT moved, which is the encoding every deployment already writes.
204+
return { verified: false, conclusive: false, columnsMoved: false };
205+
}
206+
}
207+
208+
/** The body of `ObjectQL#noteAdmittedValueShapeViolation`; the method's docblock there is its contract. */
209+
export function noteAdmittedValueShapeViolation(
210+
this: Pick<MigrationFlagsHost, 'admittedValueShapeViolations' | 'recordObservedDeviation' | 'retractCreationAttestation'>,
211+
object: string,
212+
violation: AdmittedValueShapeViolation,
213+
): void {
214+
const migrationId =
215+
violation.gate === 'media' ? FILE_REFERENCES_MIGRATION_ID : VALUE_SHAPES_MIGRATION_ID;
216+
const existing = this.admittedValueShapeViolations.get(migrationId);
217+
if (existing) {
218+
existing.count += 1;
219+
} else {
220+
this.admittedValueShapeViolations.set(migrationId, {
221+
count: 1,
222+
first: { object, field: violation.field, type: violation.type, detail: violation.detail },
223+
});
224+
}
225+
// Two consumers of the same counterexample, answering different questions.
226+
// The deviation marker goes first because it is the one that applies to
227+
// EVERY verified deployment (#4797); the retraction below applies only to
228+
// the narrow fresh-datastore case (#4769) and closes the gate outright
229+
// there. Each is the other's backstop if one write fails.
230+
this.recordObservedDeviation(migrationId);
231+
this.retractCreationAttestation(migrationId);
232+
}
233+
234+
/** The body of `ObjectQL#announceOpenMigrationGates`; the method's docblock there is its contract. */
235+
export async function announceOpenMigrationGates(
236+
this: Pick<MigrationFlagsHost, 'migrationGatesAnnounced' | '_registry' | 'objectHasMediaField' | 'objectHasCoveredValueField' | 'readMigrationFlagVerified' | 'logger'>,
237+
): Promise<void> {
238+
if (this.migrationGatesAnnounced) return;
239+
this.migrationGatesAnnounced = true;
240+
try {
241+
const mediaByEnv = mediaPostureSetByEnv();
242+
const coveredByEnv = valueShapePostureSetByEnv();
243+
if (mediaByEnv && coveredByEnv) return;
244+
245+
let media = false;
246+
let covered = false;
247+
for (const obj of this._registry.getAllObjects() as any[]) {
248+
if (!media && !mediaByEnv && this.objectHasMediaField(obj)) media = true;
249+
if (!covered && !coveredByEnv && this.objectHasCoveredValueField(obj)) covered = true;
250+
if ((media || mediaByEnv) && (covered || coveredByEnv)) break;
251+
}
252+
253+
// [#4769] Read the ledger, not the memo. This advisory runs once, at
254+
// `kernel:bootstrapped`, and the fresh-datastore attestation may have
255+
// written its rows moments earlier — after the boot's first write had
256+
// already memoized "not verified". Reporting from that memo tells a
257+
// brand-new deployment to run a migration whose gate is already closed.
258+
// One query per applicable gate, once per boot.
259+
if (media && !(await this.readMigrationFlagVerified(FILE_REFERENCES_MIGRATION_ID)).verified) {
260+
this.logger.info(
261+
'[value-shape] media values are checked but NOT enforced here, and released files are ' +
262+
'never collected — this deployment has not verified its file migration. Run ' +
263+
'`os migrate files-to-references` (dry run) to see what it would do, then `--apply` ' +
264+
'to close the gate (ADR-0104).',
265+
);
266+
}
267+
if (covered && !(await this.readMigrationFlagVerified(VALUE_SHAPES_MIGRATION_ID)).verified) {
268+
this.logger.info(
269+
'[value-shape] reference and structured-JSON values are checked but NOT enforced here — ' +
270+
'this deployment has not verified its value-shape scan. Run `os migrate value-shapes` ' +
271+
'(dry run) to see what it would report, then `--apply` to close the gate ' +
272+
'(ADR-0104).',
273+
);
274+
}
275+
} catch {
276+
// An advisory must never be the reason a boot fails.
277+
}
278+
}
279+
280+
/** The body of `ObjectQL#wasDatastoreCreatedFromEmpty`; the method's docblock there is its contract. */
281+
export function wasDatastoreCreatedFromEmpty(
282+
this: Pick<MigrationFlagsHost, 'drivers'>,
283+
): boolean {
284+
let created = 0;
285+
let existing = 0;
286+
let reporting = 0;
287+
for (const driver of this.drivers.values()) {
288+
const stats = (driver as any).getSchemaSyncStats?.();
289+
if (!stats) continue;
290+
reporting += 1;
291+
created += Number(stats.created) || 0;
292+
existing += Number(stats.existing) || 0;
293+
}
294+
return reporting > 0 && existing === 0 && created > 0;
295+
}

‎packages/objectql/src/engine-realtime-publish.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
* docblocks included; no other code in `engine.ts` called them.
1717
*
1818
* What stays in `engine.ts`: `setRealtimeService` (a two-line field setter),
19-
* the `realtimeService` field itself and the three write-path call sites, and
19+
* the `realtimeService` field itself and the three write-path call sites.
2020
* `accessibleOrganizationsOf`, which sat between the helpers but is read by the
21-
* tenant wall, not by an event.
21+
* tenant wall, not by an event, now lives in `engine-driver-options.ts`.
2222
*
2323
* ## How the moved bodies reach the class
2424
*

0 commit comments

Comments
 (0)