diff --git a/packages/core/package.json b/packages/core/package.json index 3931f0cd06..d46e635dcf 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -24,6 +24,7 @@ "test:watch": "vitest" }, "devDependencies": { + "@objectstack/metadata-core": "workspace:*", "@types/node": "^26.1.2", "esbuild": "^0.28.1", "typescript": "^6.0.3", diff --git a/packages/core/src/utils/migration-journal.test.ts b/packages/core/src/utils/migration-journal.test.ts index 20e17caef8..3302fe8315 100644 --- a/packages/core/src/utils/migration-journal.test.ts +++ b/packages/core/src/utils/migration-journal.test.ts @@ -13,6 +13,22 @@ */ import { describe, it, expect, vi } from 'vitest'; +// [#5855] The fake engine's write verbs route through the producer's OWN +// dispatch predicates (#4550 delete / #5480 update), so this double cannot +// accept a call `ObjectQL.` refuses. Imported from +// `@objectstack/metadata-core` and not from `@objectstack/objectql`: objectql +// depends on this package, so that import would close a dependency cycle turbo +// rejects — which is why both of this file's (file, verb) pairs sat in the +// gate's DEBT ledger until #5619 sank the two predicates into a package that +// depends on neither side. `@objectstack/metadata-core` is a devDependency +// here for exactly this import, and nothing else. +import { + assertEngineDeleteDispatch, + assertEngineUpdateDispatch, + type EngineDeleteDispatchInput, + type EngineUpdateDispatchData, + type EngineUpdateDispatchInput, +} from '@objectstack/metadata-core'; import { runMigrationJournal, resumeMigrationJournal, @@ -72,8 +88,23 @@ class FakeEngine { return (await this.find(objectName, query))[0] ?? null; } - async update(): Promise { throw new Error('not used'); } - async delete(): Promise { throw new Error('not used'); } + // Neither verb is driven by anything this suite runs — the journal writes + // rows and reads them back. They stay `not used`, but the dispatch assert + // comes FIRST so the stub can never become the lax double of #4434 the day a + // test starts writing through it: a call the real engine rejects is rejected + // here, with the producer's own message, before `not used` is ever reached. + async update( + _objectName: string, + data: EngineUpdateDispatchData, + options?: EngineUpdateDispatchInput, + ): Promise { + assertEngineUpdateDispatch(data, options); + throw new Error('not used'); + } + async delete(_objectName: string, options?: EngineDeleteDispatchInput): Promise { + assertEngineDeleteDispatch(options); + throw new Error('not used'); + } async count(): Promise { return 0; } async aggregate(): Promise { return []; } getObject(name: string): unknown { return { name }; } diff --git a/packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts b/packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts index 93871a462a..6923727168 100644 --- a/packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts +++ b/packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts @@ -1,6 +1,15 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; +// [#5855] The fake engine's write verbs route through the producer's OWN +// dispatch predicates (#4550 delete / #5480 update), so this double cannot +// accept a call `ObjectQL.` refuses. Imported from +// `@objectstack/metadata-core` (already a `dependencies` entry here) and not +// from `@objectstack/objectql`, which depends on this package — that import +// would close a dependency cycle turbo rejects, and is why both of this file's +// (file, verb) pairs sat in the gate's DEBT ledger until #5619 sank the two +// predicates into a package that depends on neither side. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { migrateSysNotificationToEvent } from './migrate-sys-notification-to-event.js'; /** Columns the legacy (pre-ADR-0030) sys_notification table physically has. */ @@ -42,13 +51,17 @@ function fakeEngine() { inserts.push({ object, row }); return { id: `${object}_${inserts.length}`, ...row }; }, - async update(object: string, data: any) { + async update(object: string, data: any, options?: Record) { + assertEngineUpdateDispatch(data, options); updates.push({ object, data }); return data; }, async find() { return []; }, async findOne() { return null; }, - async delete() { return {}; }, + async delete(_object?: string, options?: Record) { + assertEngineDeleteDispatch(options); + return {}; + }, async count() { return 0; }, async aggregate() { return []; }, } as any, diff --git a/packages/platform-objects/src/plugin.test.ts b/packages/platform-objects/src/plugin.test.ts index ccb76d903d..77e6ea264b 100644 --- a/packages/platform-objects/src/plugin.test.ts +++ b/packages/platform-objects/src/plugin.test.ts @@ -1,6 +1,14 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect } from 'vitest'; +// [#5855] The fake engine's `update` routes through the producer's OWN dispatch +// predicate (#5480), so this double cannot accept a call `ObjectQL.update` +// refuses. Imported from `@objectstack/metadata-core` (already a `dependencies` +// entry here) and not from `@objectstack/objectql`, which depends on this +// package — that import would close a dependency cycle turbo rejects, and is +// why this file's `update` entry sat in the gate's DEBT ledger until #5619 sank +// the predicate into a package that depends on neither side. +import { assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { PlatformObjectsPlugin } from './plugin.js'; import { SysMigration, SysMigrationJournal, SysSecret } from './system/index.js'; @@ -101,7 +109,14 @@ describe('PlatformObjectsPlugin: fresh-datastore attestation (#3438, ADR-0104)', rows.push({ ...data }); return data; }, - update: async () => ({}), + // `attestFreshDatastore` never overwrites an existing flag row, so no + // path this suite drives reaches `update` — the assert comes first + // anyway, so the day one does, the double refuses what a real server + // refuses instead of silently accepting it (#4434). + update: async (_object: string, data: any, options?: Record) => { + assertEngineUpdateDispatch(data, options); + return {}; + }, rows, }; if (createdFromEmpty !== undefined) { diff --git a/packages/platform-objects/src/system/migration-flag.test.ts b/packages/platform-objects/src/system/migration-flag.test.ts index 7b206a42d5..b748d0ff06 100644 --- a/packages/platform-objects/src/system/migration-flag.test.ts +++ b/packages/platform-objects/src/system/migration-flag.test.ts @@ -1,6 +1,14 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { describe, it, expect, vi } from 'vitest'; +// [#5855] The fake engine's `update` routes through the producer's OWN dispatch +// predicate (#5480), so this double cannot accept a call `ObjectQL.update` +// refuses. Imported from `@objectstack/metadata-core` (already a `dependencies` +// entry here) and not from `@objectstack/objectql`, which depends on this +// package — that import would close a dependency cycle turbo rejects, and is +// why this file's `update` entry sat in the gate's DEBT ledger until #5619 sank +// the predicate into a package that depends on neither side. +import { assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { CREATION_ATTESTED_MIGRATION_IDS } from '@objectstack/spec/system'; import { readDataMigrationFlag, @@ -26,7 +34,11 @@ function fakeEngine(rows: Array> = [], opts: { registere (tables[object] ??= []).push({ ...data }); return data; }, - async update(object, data: any) { + async update(object, data: any, options) { + // `recordDataMigrationRun` updates an existing flag row by its `id` in + // the payload — the shape `ObjectQL.update` routes `by-id`. Asserting it + // here binds this double to that verdict instead of re-deciding it. + assertEngineUpdateDispatch(data, options); const row = (tables[object] ?? []).find((r) => r.id === data.id); if (row) Object.assign(row, data); return row; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8cfcf3b68f..f90464009d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -776,6 +776,9 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@objectstack/metadata-core': + specifier: workspace:* + version: link:../metadata-core '@types/node': specifier: ^26.1.2 version: 26.1.2 diff --git a/scripts/engine-double-contract.baseline.json b/scripts/engine-double-contract.baseline.json index 4e72bb8bb2..5643b33870 100644 --- a/scripts/engine-double-contract.baseline.json +++ b/scripts/engine-double-contract.baseline.json @@ -98,38 +98,6 @@ "why": "MEASURED (#5480): the `update` slice of this gate is NEW — `resolveEngineUpdateDispatch` did not exist before #5480, so no double in the repo could route through it and the whole discovered set enters this ledger in one act. Not newly written looseness and not a raised ratchet: it is the first measurement of a contract that had no producer-side predicate to measure against, which is exactly what this script's header used to list under deliberately-not-covered (\"update's twin dispatch ... needs its own producer-side predicate extracted first\"). Discovered at line 100. The package does not depend on @objectstack/objectql yet, and the devDependency route is AVAILABLE rather than cyclic. This entry does not re-measure — the delete-slice entries for @objectstack/cloud-connection in this same ledger added the edge, recorded turbo's acceptance and reverted it, and the graph does not depend on which verb the pin is for. WHAT THIS ENTRY DOES NOT CLAIM: unlike the #5629 delete batch above it carries NO per-file dormancy probe. Nothing here says the looseness is unexercised — only that the double is structurally free to be looser than ObjectQL.update on the shape a hand-written guard always drops (`where: { id: { $in: [...] } }` looks like an id and is a multi-row predicate) and stricter on the one it invents (`data.id`, which the producer takes verbatim when truthy, ahead of both `where` and `multi`).", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic — see `why`), then open the fake's update with assertEngineUpdateDispatch(data, options)" }, - { - "file": "packages/core/src/utils/migration-journal.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5629): newly VISIBLE, not newly written — the fake's `delete` declares no parameters, and the gate's arity test (`params.length < 2` was the first line of `isEngineDeleteShape`) discarded such deletes before any other criterion ran. So this double reached neither PINNED nor this ledger and produced no output at all: the #4868 shape the DISCOVERED invariant above is written against. Discovered at line 34. Dormant looseness, probed rather than assumed: a `process.stderr.write` marker injected as the first statement of this delete printed NOTHING while the file's suite passed, so no path this suite drives calls it. The control that makes that silence evidence instead of a broken probe: the same injection in `run-summary.test.ts`'s PINNED delete DID print, in the same run of the same harness. Dormant is not harmless — it means the looseness is unexercised today, so a future test that starts deleting through this fake inherits a double that accepts what ObjectQL.delete refuses. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it (@objectstack/objectql -> @objectstack/core, both `dependencies`), so any reverse edge closes a cycle by construction. Measured on this branch: the edge was added to @objectstack/core's devDependencies and turbo 2.10.7 refused the graph outright — `WARNING Circular package dependency detected: @objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/metadata, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/core` and `x Cyclic dependency detected:` from `turbo run build --filter=@objectstack/core --dry` — then the edge was reverted. Same route, same refusal the #4987 / #5206 metadata-protocol entries recorded before #5619 closed them by moving the predicate instead of the edge.", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineDeleteDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's delete with `assertEngineDeleteDispatch(options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package does NOT depend on @objectstack/metadata-core yet, so the pin also adds it to `devDependencies`. Measured on #5619's branch, not asserted: with that edge added, `npx turbo run build --filter=@objectstack/core --dry` printed NO circular/cyclic warning (metadata-core's own dependencies are `{ @objectstack/spec, zod }` and reach neither this package nor @objectstack/objectql), then the edge was reverted. That is a DIFFERENT edge from the @objectstack/objectql one recorded in `why`, which stays cyclic and stays refused. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, - { - "file": "packages/core/src/utils/migration-journal.test.ts", - "verb": "update", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5480): the `update` slice of this gate is NEW — `resolveEngineUpdateDispatch` did not exist before #5480, so no double in the repo could route through it and the whole discovered set enters this ledger in one act. Not newly written looseness and not a raised ratchet: it is the first measurement of a contract that had no producer-side predicate to measure against, which is exactly what this script's header used to list under deliberately-not-covered (\"update's twin dispatch ... needs its own producer-side predicate extracted first\"). Discovered at line 34. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it, so any reverse edge closes a cycle by construction. This entry does not re-measure — the delete-slice entries for @objectstack/core in this same ledger added the edge and recorded turbo's outright refusal, and the blocker is a property of the dependency graph, not of the verb. WHAT THIS ENTRY DOES NOT CLAIM: unlike the #5629 delete batch above it carries NO per-file dormancy probe. Nothing here says the looseness is unexercised — only that the double is structurally free to be looser than ObjectQL.update on the shape a hand-written guard always drops (`where: { id: { $in: [...] } }` looks like an id and is a multi-row predicate) and stricter on the one it invents (`data.id`, which the producer takes verbatim when truthy, ahead of both `where` and `multi`).", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineUpdateDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's update with `assertEngineUpdateDispatch(data, options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package does NOT depend on @objectstack/metadata-core yet, so the pin also adds it to `devDependencies`. Measured on #5619's branch, not asserted: with that edge added, `npx turbo run build --filter=@objectstack/core --dry` printed NO circular/cyclic warning (metadata-core's own dependencies are `{ @objectstack/spec, zod }` and reach neither this package nor @objectstack/objectql), then the edge was reverted. That is a DIFFERENT edge from the @objectstack/objectql one recorded in `why`, which stays cyclic and stays refused. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, - { - "file": "packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5629): newly VISIBLE, not newly written — the fake's `delete` declares no parameters, and the gate's arity test (`params.length < 2` was the first line of `isEngineDeleteShape`) discarded such deletes before any other criterion ran. So this double reached neither PINNED nor this ledger and produced no output at all: the #4868 shape the DISCOVERED invariant above is written against. Discovered at line 40. Dormant looseness, probed rather than assumed: a `process.stderr.write` marker injected as the first statement of this delete printed NOTHING while the file's suite passed, so no path this suite drives calls it. The control that makes that silence evidence instead of a broken probe: the same injection in `run-summary.test.ts`'s PINNED delete DID print, in the same run of the same harness. Dormant is not harmless — it means the looseness is unexercised today, so a future test that starts deleting through this fake inherits a double that accepts what ObjectQL.delete refuses. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it (@objectstack/objectql -> @objectstack/metadata-protocol -> @objectstack/metadata, every edge `dependencies`), so any reverse edge closes a cycle by construction. This package's own edge was NOT probed separately on this branch, and does not need to be: @objectstack/metadata is named IN the cycle turbo 2.10.7 printed when the same edge was added to @objectstack/core here — `@objectstack/driver-sql, @objectstack/driver-sqlite-wasm, @objectstack/metadata, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/core`. Stated plainly so the next reader knows which measurement this rests on.", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineDeleteDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's delete with `assertEngineDeleteDispatch(options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package already depends on @objectstack/metadata-core (`dependencies`), so nothing else is needed. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, - { - "file": "packages/metadata/src/migrations/migrate-sys-notification-to-event.test.ts", - "verb": "update", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5480): the `update` slice of this gate is NEW — `resolveEngineUpdateDispatch` did not exist before #5480, so no double in the repo could route through it and the whole discovered set enters this ledger in one act. Not newly written looseness and not a raised ratchet: it is the first measurement of a contract that had no producer-side predicate to measure against, which is exactly what this script's header used to list under deliberately-not-covered (\"update's twin dispatch ... needs its own producer-side predicate extracted first\"). Discovered at line 40. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it, so any reverse edge closes a cycle by construction. This entry does not re-measure — the delete-slice entries for @objectstack/metadata in this same ledger added the edge and recorded turbo's outright refusal, and the blocker is a property of the dependency graph, not of the verb. WHAT THIS ENTRY DOES NOT CLAIM: unlike the #5629 delete batch above it carries NO per-file dormancy probe. Nothing here says the looseness is unexercised — only that the double is structurally free to be looser than ObjectQL.update on the shape a hand-written guard always drops (`where: { id: { $in: [...] } }` looks like an id and is a multi-row predicate) and stricter on the one it invents (`data.id`, which the producer takes verbatim when truthy, ahead of both `where` and `multi`).", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineUpdateDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's update with `assertEngineUpdateDispatch(data, options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package already depends on @objectstack/metadata-core (`dependencies`), so nothing else is needed. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, { "file": "packages/objectql/src/protocol-boot-hydration-scoped.test.ts", "verb": "delete", @@ -146,22 +114,6 @@ "why": "MEASURED (#5629): newly VISIBLE, not newly written — the fake's `delete` declares no parameters, and the gate's arity test (`params.length < 2` was the first line of `isEngineDeleteShape`) discarded such deletes before any other criterion ran. So this double reached neither PINNED nor this ledger and produced no output at all: the #4868 shape the DISCOVERED invariant above is written against. Discovered at line 255. Dormant looseness, probed rather than assumed: a `process.stderr.write` marker injected as the first statement of this delete printed NOTHING while the file's suite passed, so no path this suite drives calls it. The control that makes that silence evidence instead of a broken probe: the same injection in `run-summary.test.ts`'s PINNED delete DID print, in the same run of the same harness. Dormant is not harmless — it means the looseness is unexercised today, so a future test that starts deleting through this fake inherits a double that accepts what ObjectQL.delete refuses. This IS the producer's own package: `./engine-delete-dispatch.js` is a relative import away, exactly as objectql's already-pinned tests import it. A one-line pin whenever a batch takes it — deferred here because #5629's first batch is the criterion plus the ledger, not adoption.", "closes": "open the fake's delete with assertEngineDeleteDispatch(options) imported from ./engine-delete-dispatch.js, and run the package's suite" }, - { - "file": "packages/platform-objects/src/plugin.test.ts", - "verb": "update", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5480): the `update` slice of this gate is NEW — `resolveEngineUpdateDispatch` did not exist before #5480, so no double in the repo could route through it and the whole discovered set enters this ledger in one act. Not newly written looseness and not a raised ratchet: it is the first measurement of a contract that had no producer-side predicate to measure against, which is exactly what this script's header used to list under deliberately-not-covered (\"update's twin dispatch ... needs its own producer-side predicate extracted first\"). Discovered at line 96. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it, so any reverse edge closes a cycle by construction. Measured on this branch, not cited: the edge was added to @objectstack/platform-objects's devDependencies and turbo 2.10.7 refused the graph — `WARNING Circular package dependency detected: @objectstack/metadata, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/platform-objects` and `x Cyclic dependency detected:` from `turbo run build --filter=@objectstack/platform-objects --dry` — then the edge was reverted. WHAT THIS ENTRY DOES NOT CLAIM: unlike the #5629 delete batch above it carries NO per-file dormancy probe. Nothing here says the looseness is unexercised — only that the double is structurally free to be looser than ObjectQL.update on the shape a hand-written guard always drops (`where: { id: { $in: [...] } }` looks like an id and is a multi-row predicate) and stricter on the one it invents (`data.id`, which the producer takes verbatim when truthy, ahead of both `where` and `multi`).", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineUpdateDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's update with `assertEngineUpdateDispatch(data, options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package already depends on @objectstack/metadata-core (`dependencies`), so nothing else is needed. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, - { - "file": "packages/platform-objects/src/system/migration-flag.test.ts", - "verb": "update", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5480): the `update` slice of this gate is NEW — `resolveEngineUpdateDispatch` did not exist before #5480, so no double in the repo could route through it and the whole discovered set enters this ledger in one act. Not newly written looseness and not a raised ratchet: it is the first measurement of a contract that had no producer-side predicate to measure against, which is exactly what this script's header used to list under deliberately-not-covered (\"update's twin dispatch ... needs its own producer-side predicate extracted first\"). Discovered at line 18. The devDependency route DOES NOT EXIST for this package: @objectstack/objectql depends on it, so any reverse edge closes a cycle by construction. Measured on this branch, not cited: the edge was added to @objectstack/platform-objects's devDependencies and turbo 2.10.7 refused the graph — `WARNING Circular package dependency detected: @objectstack/metadata, @objectstack/metadata-protocol, @objectstack/objectql, @objectstack/platform-objects` and `x Cyclic dependency detected:` from `turbo run build --filter=@objectstack/platform-objects --dry` — then the edge was reverted. WHAT THIS ENTRY DOES NOT CLAIM: unlike the #5629 delete batch above it carries NO per-file dormancy probe. Nothing here says the looseness is unexercised — only that the double is structurally free to be looser than ObjectQL.update on the shape a hand-written guard always drops (`where: { id: { $in: [...] } }` looks like an id and is a multi-row predicate) and stricter on the one it invents (`data.id`, which the producer takes verbatim when truthy, ahead of both `where` and `multi`).", - "closes": "THE BLOCKER RECORDED IN `why` IS GONE. #5619 sank both dispatch predicates into @objectstack/metadata-core — a package that depends on neither this one nor @objectstack/objectql — so assertEngineUpdateDispatch is now importable here WITHOUT the cyclic edge, and @objectstack/objectql re-exports it from its original path so nothing else moved. What remains is the one-line pin: open the fake's update with `assertEngineUpdateDispatch(data, options)` imported from @objectstack/metadata-core, run the package's suite, and delete this entry. This package already depends on @objectstack/metadata-core (`dependencies`), so nothing else is needed. Tracked as #5855; #5619 removed the cycle, it deliberately did not do the pin (its file face was the move plus the 13 metadata-protocol files)." - }, { "file": "packages/plugins/plugin-approvals/src/admin-exemption-retired.test.ts", "verb": "update",