From e7fc6e74219f63494f985ba8b3be79d1204a491f Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Thu, 6 Aug 2026 10:12:44 +0000 Subject: [PATCH] refactor(metadata-core,objectql): sink both write-verb dispatch predicates into metadata-core (#5619) Move `engine-delete-dispatch.ts` (#4550) and `engine-update-dispatch.ts` (#5480) from `packages/objectql/src/` to `packages/metadata-core/src/` unchanged, and keep the original objectql paths as re-export shims so no caller, no public export and no pinned call site moves. `@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so that package's 13 fake engines could not import either predicate without closing a dependency cycle turbo 2.10.7 refuses outright. `@objectstack/metadata-core` is a package both sides already depend on and which depends on neither, so the sink is the one route that pins those doubles without inventing an edge. - 13 metadata-protocol test files: both fake write verbs now open with the producer's own predicate, imported from `@objectstack/metadata-core`. - 26 (file, verb) ledger entries deleted from `scripts/engine-double-contract.baseline.json`; the gate's counts move from 37 pinned / 165 ledger / 2 exempt to 63 pinned / 139 ledger / 2 exempt. - The gate's two slices now accept `@objectstack/metadata-core` alongside `@objectstack/objectql`, and its remediation message names the right one. - Six ledger entries whose `closes` prescribed exactly this sink ("tracked as #5619") are rewritten: the blocker is gone, what remains is the one-line pin, tracked as #5855. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019Q7oc7ASjh8yxyS3Yz78We --- .../sink-engine-dispatch-to-metadata-core.md | 31 +++ .../src/engine-delete-dispatch.ts | 204 ++++++++++++++ .../src/engine-update-dispatch.ts | 248 +++++++++++++++++ packages/metadata-core/src/index.ts | 11 + .../migrations/recorded-by-sentinel.test.ts | 15 +- ...tocol-publish-drafts-endpoint-gate.test.ts | 10 + .../protocol-publish-drafts-org-scope.test.ts | 10 + .../src/protocol.code-only-types.test.ts | 20 +- .../src/protocol.read-decorations.test.ts | 14 +- .../protocol.runtime-authoring-gate.test.ts | 10 + ...rotocol.save-flow-canonicalization.test.ts | 16 +- .../src/protocol.save-union-issues.test.ts | 18 +- .../src/protocol.stored-conversions.test.ts | 18 +- .../src/protocol.stored-migration.test.ts | 14 +- ...ys-metadata-repository.draft-drain.test.ts | 10 + ...tadata-repository.history-counters.test.ts | 10 + ...ys-metadata-repository.recorded-by.test.ts | 10 + .../objectql/src/engine-delete-dispatch.ts | 203 +++----------- .../objectql/src/engine-update-dispatch.ts | 250 ++---------------- scripts/check-engine-double-contract.mjs | 56 +++- scripts/engine-double-contract.baseline.json | 246 ++--------------- 21 files changed, 790 insertions(+), 634 deletions(-) create mode 100644 .changeset/sink-engine-dispatch-to-metadata-core.md create mode 100644 packages/metadata-core/src/engine-delete-dispatch.ts create mode 100644 packages/metadata-core/src/engine-update-dispatch.ts diff --git a/.changeset/sink-engine-dispatch-to-metadata-core.md b/.changeset/sink-engine-dispatch-to-metadata-core.md new file mode 100644 index 0000000000..e07ce3fabd --- /dev/null +++ b/.changeset/sink-engine-dispatch-to-metadata-core.md @@ -0,0 +1,31 @@ +--- +"@objectstack/metadata-core": patch +"@objectstack/objectql": patch +--- + +两个写动词的派发判定下沉到 `@objectstack/metadata-core` —— 公共 API 零变化,一次关闭 26 条 engine-double 基线条目 + +`ObjectQL.delete` / `ObjectQL.update` 的三分支派发判定(`engine-delete-dispatch.ts` #4550、 +`engine-update-dispatch.ts` #5480)从 `packages/objectql/src/` **原样搬到** +`packages/metadata-core/src/`。这是一次搬移,不是重构:两个模块本来就零 import、纯自包含, +判定逻辑一个字未改。 + +**为什么搬。** `@objectstack/objectql` 的 `dependencies` 含 `@objectstack/metadata-protocol`, +所以那个包里 13 个假引擎结构性地无法 import 这两个谓词 —— 反向 devDependency 即成环, +turbo 2.10.7 直接拒绝任务图。判据来自门禁台账里 +`packages/spec/src/contracts/data-engine.test.ts` 那条 EXEMPT:反向 import 不可行时,唯一 +出路是下沉到**两边都已依赖**的包。`@objectstack/metadata-core` 正是这个包 +(`objectql -> metadata-core` 与 `metadata-protocol -> metadata-core` 都是既有边),而它自己 +的 `dependencies` 只有 `{ @objectstack/spec, zod }`,不含 objectql,故不引入新环。 + +**公共 API 与既有调用点零变化。** `packages/objectql/src/engine-delete-dispatch.ts` / +`engine-update-dispatch.ts` 保留在原路径,改为 re-export shim,因此 +`@objectstack/objectql` 仍然导出 +`resolveEngineDeleteDispatch` / `assertEngineDeleteDispatch` / `scalarDeleteId` / +`ENGINE_DELETE_REJECT_MESSAGE` / `ENGINE_DELETE_DISPATCH_CASES` 及 update 侧的五个同名对应物 +(与全部类型),`engine.ts` 与 37 个既有 pinned 调用点一行未动。同一批符号现在也从 +`@objectstack/metadata-core` 导出。 + +搭配的门禁改动:`scripts/check-engine-double-contract.mjs` 的两个 slice 现在同时接受 +`@objectstack/metadata-core` 与 `@objectstack/objectql` 两种拼写(它们指向同一个函数), +失败提示也改为在「objectql 依赖该包」时优先建议 metadata-core。 diff --git a/packages/metadata-core/src/engine-delete-dispatch.ts b/packages/metadata-core/src/engine-delete-dispatch.ts new file mode 100644 index 0000000000..d26fd4410c --- /dev/null +++ b/packages/metadata-core/src/engine-delete-dispatch.ts @@ -0,0 +1,204 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The **one** answer to "what does `ObjectQLEngine.delete` do with this call?" + * — extracted so that the engine and every test double that stands in for it + * read the same predicate rather than two hand-written approximations of it + * (objectstack#4550, from objectstack#4434). + * + * ## Why this is a shared module and not four lines inside `engine.ts` + * + * `#4434` shipped green. `DELETE /api/v1/sharing/rules/:idOrName` answered 500 + * for **both** address forms the route advertises, for every rule, from the day + * it was written — and `plugin-sharing`'s `deleteRule drops rule + all its + * grants` test asserted success against it the whole time. The route was not + * untested; it was tested against a **fake engine whose `delete` accepted a + * call the real engine refuses**. A predicate-shaped purge of + * `sys_record_share` (no scalar `where.id`, no `options.multi`) is precisely + * the one shape `delete()` throws on, and the fake happily deleted by + * predicate. + * + * The fix for #4434 mirrored the guard into that fake by hand. That closes one + * fake and starts a second copy of the contract — the failure mode this module + * exists to remove. A double that *imports the producer's own decision* cannot + * be looser than the producer, ever, which is the property the gate wants and + * the property a copy can only have until someone edits one side. + * + * Same reasoning as `packages/spec/src/data/*-conformance.ts` for drivers, and + * the same shape as objectstack#4455: **the scan and the validator must answer + * with one predicate.** + * + * ## Why this module lives in `@objectstack/metadata-core` and not in `objectql` + * + * It was written in `packages/objectql/src/` next to its only production caller + * (objectstack#4550) and moved here unchanged by objectstack#5619 — a **move**, + * not a rewrite: not one line of the predicate below differs from the version + * `ObjectQL.delete` has been dispatching on since #4550. + * + * The move is what made a whole package's doubles pinnable. Thirteen fake + * engines in `@objectstack/metadata-protocol` were structurally unable to reach + * this predicate: `@objectstack/objectql` **depends on** + * `@objectstack/metadata-protocol`, so the import a pin needs would have closed + * a cycle — measured, not assumed, on turbo 2.10.7: + * + * ``` + * WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol + * x Cyclic dependency detected: + * | @objectstack/objectql#build, @objectstack/metadata-protocol#build + * ``` + * + * When a reverse import is impossible, the only honest way out is to sink the + * predicate into a package **both sides already depend on** — the criterion + * `packages/spec/src/contracts/data-engine.test.ts`'s EXEMPT entry in the gate's + * ledger states. `@objectstack/metadata-core` is exactly that package: + * `objectql -> metadata-core` and `metadata-protocol -> metadata-core` both + * pre-date this change, and this package's own dependencies are + * `{ @objectstack/spec, zod }` — no `objectql`, so no new edge and no new cycle. + * This module importing nothing at all is what makes that free. + * + * `@objectstack/objectql` re-exports every symbol below from its original path, + * so the 24+ call sites already pinned to it, and the public API, are unchanged. + * + * ## The contract, normatively + * + * `delete(object, options)` dispatches on exactly one question — *does this + * call identify a single row by primary key?* + * + * - `options.where.id` is a **scalar** (`string` / `number` / `bigint`, not + * `null`) → `by-id`: routes to `driver.delete`, runs cascade-delete and the + * by-id RLS pre-image check. + * - otherwise, `options.multi` is truthy → `multi`: routes to + * `driver.deleteMany` with the middleware-composed AST. + * - otherwise → **`reject`**. The call names neither one row nor a bulk + * intent, and the engine throws rather than guessing. + * + * The scalar test is load-bearing and is the half a hand-written double most + * often drops: `where: { id: { $in: [...] } }` is a *multi-row predicate*, not + * an id. Treating it as an id would bind the operator object literally into + * `driver.delete(object, {$in: […]})` **and** skip both the row-scoping AST + * seeding (#2982) and the by-id pre-image check. So it is `reject` unless the + * caller also said `multi`. + * + * @see ObjectQL.delete in `packages/objectql/src/engine.ts` — the only production caller. + * @see packages/objectql/src/engine-delete-dispatch.ts — the re-export shim that keeps + * objectql's original import path (and its public API) working. + * @see packages/objectql/src/engine-delete-dispatch.test.ts — the test that drives the + * REAL engine over `ENGINE_DELETE_DISPATCH_CASES`; it stays in objectql because it + * needs `ObjectQL`, which this package must never depend on. + * @see scripts/check-engine-double-contract.mjs — the gate that keeps doubles on it. + */ + +/** The message `delete()` throws when a call identifies neither one row nor a bulk intent. */ +export const ENGINE_DELETE_REJECT_MESSAGE = 'Delete requires an ID or options.multi=true'; + +/** What `ObjectQLEngine.delete` will do with a given options bag. */ +export type EngineDeleteDispatch = + /** A scalar `where.id` — `driver.delete`, cascade + by-id RLS pre-image. */ + | { readonly kind: 'by-id'; readonly id: string | number | bigint } + /** No single id but `options.multi` — `driver.deleteMany` with the composed AST. */ + | { readonly kind: 'multi' } + /** Neither — the engine throws `ENGINE_DELETE_REJECT_MESSAGE`. */ + | { readonly kind: 'reject'; readonly message: string }; + +/** The subset of `EngineDeleteOptions` the dispatch decision actually reads. */ +export interface EngineDeleteDispatchInput { + readonly where?: unknown; + readonly multi?: unknown; + readonly [k: string]: unknown; +} + +/** + * Extract the SCALAR `where.id`, or `undefined` when the call does not name one + * row by primary key. + * + * `null`, `undefined`, arrays, and operator objects (`{ $in: [...] }`, + * `{ $ne: … }`) all yield `undefined` — they are predicates over many rows, not + * a primary key. + */ +export function scalarDeleteId( + options?: EngineDeleteDispatchInput | null, +): string | number | bigint | undefined { + const where = options?.where; + if (!where || typeof where !== 'object') return undefined; + if (!('id' in (where as Record))) return undefined; + const whereId = (where as Record).id; + const t = typeof whereId; + if (whereId !== null && (t === 'string' || t === 'number' || t === 'bigint')) { + return whereId as string | number | bigint; + } + return undefined; +} + +/** + * Decide what `ObjectQLEngine.delete` does with `options`, without doing it. + * + * Pure and side-effect free, so a test double can call it to *classify* a call + * and then implement `by-id` / `multi` however its fixture stores rows — while + * being bound to the real engine's `reject` surface for free. + */ +export function resolveEngineDeleteDispatch( + options?: EngineDeleteDispatchInput | null, +): EngineDeleteDispatch { + const id = scalarDeleteId(options); + if (id !== undefined) return { kind: 'by-id', id }; + if (options?.multi) return { kind: 'multi' }; + return { kind: 'reject', message: ENGINE_DELETE_REJECT_MESSAGE }; +} + +/** + * Throw exactly what `ObjectQLEngine.delete` throws when a call is neither + * `by-id` nor `multi`; return the resolved dispatch otherwise. + * + * This is the line a fake engine's `delete` opens with. One call pins the fake + * to the producer's rejection surface, and — unlike a mirrored `if` — it cannot + * drift when the producer's rule changes. + * + * ```ts + * async delete(object: string, options?: any) { + * assertEngineDeleteDispatch(options); // refuses what a real server refuses + * … + * } + * ``` + */ +export function assertEngineDeleteDispatch( + options?: EngineDeleteDispatchInput | null, +): Exclude { + const dispatch = resolveEngineDeleteDispatch(options); + if (dispatch.kind === 'reject') throw new Error(dispatch.message); + return dispatch; +} + +/** + * The shared conformance case-set for the delete dispatch — the same role + * `packages/spec/src/data/*-conformance.ts` plays for drivers. + * + * Every case names a call shape and the verdict the **real engine** gives it. + * A double proved against these is proved against the producer, including the + * three shapes that look like an id and are not. + */ +export interface EngineDeleteDispatchCase { + /** What the shape is, in the words a failure message should use. */ + readonly what: string; + /** The options bag handed to `delete(object, options)`. */ + readonly options: EngineDeleteDispatchInput | undefined; + /** The verdict the engine gives it. */ + readonly expect: EngineDeleteDispatch['kind']; +} + +export const ENGINE_DELETE_DISPATCH_CASES: readonly EngineDeleteDispatchCase[] = [ + { what: 'scalar string id', options: { where: { id: 'rec_1' } }, expect: 'by-id' }, + { what: 'scalar number id', options: { where: { id: 42 } }, expect: 'by-id' }, + { what: 'scalar id alongside other predicates', options: { where: { id: 'rec_1', tenant: 't1' } }, expect: 'by-id' }, + { what: 'multi with a predicate', options: { where: { rule_id: 'r1' }, multi: true }, expect: 'multi' }, + { what: 'multi with no predicate at all', options: { multi: true }, expect: 'multi' }, + { what: 'multi alongside an $in id set', options: { where: { id: { $in: ['a', 'b'] } }, multi: true }, expect: 'multi' }, + // ── The rejects. Everything below is what #4434 shipped against a fake that + // accepted it, and what a running server answers 500 to. + { what: 'predicate on a non-id column, no multi', options: { where: { rule_id: 'r1' } }, expect: 'reject' }, + { what: '$in over ids, no multi (an operator object is NOT an id)', options: { where: { id: { $in: ['a', 'b'] } } }, expect: 'reject' }, + { what: 'array id, no multi', options: { where: { id: ['a', 'b'] } }, expect: 'reject' }, + { what: 'null id, no multi', options: { where: { id: null } }, expect: 'reject' }, + { what: 'empty where, no multi', options: { where: {} }, expect: 'reject' }, + { what: 'no options at all', options: undefined, expect: 'reject' }, + { what: 'multi explicitly false with a predicate', options: { where: { rule_id: 'r1' }, multi: false }, expect: 'reject' }, +]; diff --git a/packages/metadata-core/src/engine-update-dispatch.ts b/packages/metadata-core/src/engine-update-dispatch.ts new file mode 100644 index 0000000000..5579d4e1be --- /dev/null +++ b/packages/metadata-core/src/engine-update-dispatch.ts @@ -0,0 +1,248 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The **one** answer to "what does `ObjectQLEngine.update` do with this call?" + * — the twin of `engine-delete-dispatch.ts`, extracted for the same reason and + * on the same terms (objectstack#5480, from objectstack#4550 / objectstack#4434). + * + * ## Why this exists as a module + * + * `delete` got its shared predicate because #4434 shipped a dead REST route + * green: a fake engine accepted the one call shape the real `delete` refuses, + * so the suite proved nothing about the path it was written for. `update` has + * the *same* three-way dispatch and — until this module — none of the defence. + * #5393 hit the asymmetry from the consumer side: writing real contract tests + * for the flow `update_record` / `delete_record` executors, the delete half + * could bind its fake to `assertEngineDeleteDispatch` while the update half + * could only assert the options bag the executor hands over, "without a second + * opinion on whether the engine would accept it" — because the only + * alternative was hand-copying the rule into the fake, which is the failure + * mode this family of modules exists to remove. + * + * A double that *imports the producer's own decision* cannot be looser than + * the producer, ever. A hand-mirrored `if` can only stay honest until someone + * edits one side — and the half a copy drops is always the same one: + * `where: { id: { $in: [...] } }` looks like an id and is a multi-row + * predicate. + * + * ## Why this module lives in `@objectstack/metadata-core` and not in `objectql` + * + * Same story as the twin, and the same answer — see the corresponding section of + * `engine-delete-dispatch.ts` for the measured turbo cycle that forced it. In + * short: `@objectstack/objectql` depends on `@objectstack/metadata-protocol`, so + * that package's thirteen fake engines could not import this predicate without + * closing a dependency cycle; `@objectstack/metadata-core` is a package **both** + * already depend on and which does not depend on `objectql`, so sinking the two + * dispatch modules here (objectstack#5619) is the one route that pins those + * doubles without inventing an edge. A **move**, not a rewrite: the predicate + * below is byte-for-byte the decision `ObjectQL.update` has dispatched on since + * #5480, and `@objectstack/objectql` re-exports every symbol from its original + * path so no caller and no public export changes. + * + * ## The contract, normatively + * + * `update(object, data, options)` dispatches on exactly one question — *does + * this call identify a single row by primary key?* — asked of two places, in + * this order: + * + * - **`data.id`, taken verbatim when truthy** → `by-id`: routes to + * `driver.update(object, id, data, …)`. + * - otherwise, `options.where.id` is a **scalar** (`string` / `number` / + * `bigint`, not `null`) **and truthy** → `by-id`, same route. + * - otherwise, `options.multi` is truthy → `multi`: routes to + * `driver.updateMany` with the middleware-composed AST (#2982). + * - otherwise → **`reject`**. The call names neither one row nor a bulk + * intent, and the engine throws rather than rewriting every row it can see. + * + * Three things about that list are load-bearing and easy to get wrong when + * copying it by hand — which is the whole argument for importing it instead: + * + * 1. **The `where.id` scalar test.** `{ id: { $in: [...] } }` / `{ id: [...] }` + * / `{ id: null }` are predicates over many rows. Treating one as an id + * would bind the operator object literally into `driver.update(object, + * {$in: […]}, …)` **and** skip the #2982 row-scoping AST seeding. So they + * are `reject` unless the caller also said `multi`. + * 2. **`data.id` is NOT scalar-tested.** `ObjectQL.update` reads `data.id` + * first and uses it as-is whenever it is truthy, so an operator object + * parked there wins over everything below it — including an explicit + * `multi: true`. This module reports that verdict rather than quietly + * improving on it: a predicate that is *better* than the producer is a + * second opinion, which is exactly what #4550 removed. (The asymmetry + * itself is filed as objectstack#5748; when it is fixed, it is fixed **here + * and in `engine.ts` together**, which is now one edit instead of two.) + * 3. **Truthiness, not `!== undefined`.** The engine branches on + * `if (hookContext.input.id)`, so a falsy scalar id — `where: { id: 0 }`, + * `where: { id: '' }` — does **not** take the by-id route; it falls through + * to `multi`/`reject` like any other non-identifying call. + * + * ## What the predicate deliberately does NOT model + * + * `engine.ts`'s bulk branch reads `options.multi && driver.updateMany`, i.e. a + * driver with no `updateMany` turns a `multi` call into the same throw. That + * is a *driver capability*, not a property of the call, and a double answering + * for a fixture's own storage has no `driver.updateMany` to consult. Same + * choice `resolveEngineDeleteDispatch` makes for `driver.deleteMany`: this + * module classifies the CALL, and the engine keeps the capability check. + * + * @see ObjectQL.update in `packages/objectql/src/engine.ts` — the only production caller. + * @see engine-delete-dispatch.ts — the twin, and the precedent. + * @see packages/objectql/src/engine-update-dispatch.ts — the re-export shim that keeps + * objectql's original import path (and its public API) working. + * @see packages/objectql/src/engine-update-dispatch.test.ts — the test that drives the + * REAL engine over `ENGINE_UPDATE_DISPATCH_CASES`; it stays in objectql because it + * needs `ObjectQL`, which this package must never depend on. + * @see scripts/check-engine-double-contract.mjs — the gate that keeps doubles on both. + */ + +/** The message `update()` throws when a call identifies neither one row nor a bulk intent. */ +export const ENGINE_UPDATE_REJECT_MESSAGE = 'Update requires an ID or options.multi=true'; + +/** What `ObjectQLEngine.update` will do with a given `(data, options)` pair. */ +export type EngineUpdateDispatch = + /** A truthy `data.id`, or a truthy scalar `where.id` — `driver.update`. */ + | { readonly kind: 'by-id'; readonly id: unknown } + /** No single id but `options.multi` — `driver.updateMany` with the composed AST. */ + | { readonly kind: 'multi' } + /** Neither — the engine throws `ENGINE_UPDATE_REJECT_MESSAGE`. */ + | { readonly kind: 'reject'; readonly message: string }; + +/** The subset of `EngineUpdateOptions` the dispatch decision actually reads. */ +export interface EngineUpdateDispatchInput { + readonly where?: unknown; + readonly multi?: unknown; + readonly [k: string]: unknown; +} + +/** The subset of the update PAYLOAD the dispatch decision reads: `id`, and nothing else. */ +export interface EngineUpdateDispatchData { + readonly id?: unknown; + readonly [k: string]: unknown; +} + +/** + * Extract the SCALAR `where.id`, or `undefined` when the call's `where` does + * not name one row by primary key. + * + * Byte-for-byte the same rule as `scalarDeleteId` — `null`, `undefined`, + * arrays and operator objects (`{ $in: [...] }`, `{ $ne: … }`) all yield + * `undefined`, because they are predicates over many rows. + * + * Note this covers only the `where` half of the update decision; `data.id` + * outranks it and is taken verbatim (see the module header, point 2). Use + * {@link resolveEngineUpdateDispatch} for the whole answer. + */ +export function scalarUpdateId( + options?: EngineUpdateDispatchInput | null, +): string | number | bigint | undefined { + const where = options?.where; + if (!where || typeof where !== 'object') return undefined; + if (!('id' in (where as Record))) return undefined; + const whereId = (where as Record).id; + const t = typeof whereId; + if (whereId !== null && (t === 'string' || t === 'number' || t === 'bigint')) { + return whereId as string | number | bigint; + } + return undefined; +} + +/** + * Decide what `ObjectQLEngine.update` does with `(data, options)`, without + * doing it. + * + * Pure and side-effect free, so a test double can call it to *classify* a call + * and then implement `by-id` / `multi` however its fixture stores rows — while + * being bound to the real engine's `reject` surface for free. + * + * `data` is read UNGUARDED (`data.id`, no optional chaining) on purpose: + * `ObjectQL.update` reads it that way, so `update(object, undefined)` is a + * `TypeError` there and must be a `TypeError` here. A double that is kinder + * than the producer about a missing payload is a double that hides the + * producer's behaviour — the thing this module exists to prevent. + */ +export function resolveEngineUpdateDispatch( + data: EngineUpdateDispatchData, + options?: EngineUpdateDispatchInput | null, +): EngineUpdateDispatch { + // `let id = data.id; if (!id && ) id = whereId;` — the + // producer's own two lines, in the producer's own order. + let id: unknown = data.id; + if (!id) { + const fromWhere = scalarUpdateId(options); + if (fromWhere !== undefined) id = fromWhere; + } + // The engine branches on `if (hookContext.input.id)` — truthiness, so a + // falsy scalar id is not an identifying call. See header point 3. + if (id) return { kind: 'by-id', id }; + if (options?.multi) return { kind: 'multi' }; + return { kind: 'reject', message: ENGINE_UPDATE_REJECT_MESSAGE }; +} + +/** + * Throw exactly what `ObjectQLEngine.update` throws when a call is neither + * `by-id` nor `multi`; return the resolved dispatch otherwise. + * + * This is the line a fake engine's `update` opens with. One call pins the fake + * to the producer's rejection surface, and — unlike a mirrored `if` — it cannot + * drift when the producer's rule changes. + * + * ```ts + * async update(object: string, data: any, options?: any) { + * assertEngineUpdateDispatch(data, options); // refuses what a real server refuses + * … + * } + * ``` + */ +export function assertEngineUpdateDispatch( + data: EngineUpdateDispatchData, + options?: EngineUpdateDispatchInput | null, +): Exclude { + const dispatch = resolveEngineUpdateDispatch(data, options); + if (dispatch.kind === 'reject') throw new Error(dispatch.message); + return dispatch; +} + +/** + * The shared conformance case-set for the update dispatch — the same role + * `ENGINE_DELETE_DISPATCH_CASES` plays for `delete`, and the same role + * `packages/spec/src/data/*-conformance.ts` plays for drivers. + * + * Every case names a call shape and the verdict the **real engine** gives it. + * A double proved against these is proved against the producer, including the + * shapes that look like an id and are not — and the one that does not look + * like an id and is (`data.id`). + */ +export interface EngineUpdateDispatchCase { + /** What the shape is, in the words a failure message should use. */ + readonly what: string; + /** The payload handed to `update(object, data, options)`. */ + readonly data: EngineUpdateDispatchData; + /** The options bag handed to `update(object, data, options)`. */ + readonly options: EngineUpdateDispatchInput | undefined; + /** The verdict the engine gives it. */ + readonly expect: EngineUpdateDispatch['kind']; +} + +export const ENGINE_UPDATE_DISPATCH_CASES: readonly EngineUpdateDispatchCase[] = [ + // ── by-id via `where`. + { what: 'scalar string where.id', data: { title: 'x' }, options: { where: { id: 'rec_1' } }, expect: 'by-id' }, + { what: 'scalar number where.id', data: { title: 'x' }, options: { where: { id: 42 } }, expect: 'by-id' }, + { what: 'scalar where.id alongside other predicates', data: { title: 'x' }, options: { where: { id: 'rec_1', tenant: 't1' } }, expect: 'by-id' }, + // ── by-id via the PAYLOAD, which outranks `where` and `multi` alike. + { what: 'id carried in the data payload, no where at all', data: { id: 'rec_1', title: 'x' }, options: undefined, expect: 'by-id' }, + { what: 'data.id wins over an explicit multi:true', data: { id: 'rec_1', title: 'x' }, options: { where: { tenant: 't1' }, multi: true }, expect: 'by-id' }, + // ── multi. + { what: 'multi with a predicate', data: { title: 'x' }, options: { where: { tenant: 't1' }, multi: true }, expect: 'multi' }, + { what: 'multi with no predicate at all', data: { title: 'x' }, options: { multi: true }, expect: 'multi' }, + { what: 'multi alongside an $in id set', data: { title: 'x' }, options: { where: { id: { $in: ['a', 'b'] } }, multi: true }, expect: 'multi' }, + { what: 'multi with a FALSY data.id (0 does not identify a row)', data: { id: 0, title: 'x' }, options: { multi: true }, expect: 'multi' }, + // ── The rejects. Every one of these is a call a fake that mirrors the rule + // by hand tends to accept, and a running server answers 500 to. + { what: 'predicate on a non-id column, no multi', data: { title: 'x' }, options: { where: { tenant: 't1' } }, expect: 'reject' }, + { what: '$in over ids, no multi (an operator object is NOT an id)', data: { title: 'x' }, options: { where: { id: { $in: ['a', 'b'] } } }, expect: 'reject' }, + { what: 'array id, no multi', data: { title: 'x' }, options: { where: { id: ['a', 'b'] } }, expect: 'reject' }, + { what: 'null id, no multi', data: { title: 'x' }, options: { where: { id: null } }, expect: 'reject' }, + { what: 'falsy scalar where.id (0), no multi', data: { title: 'x' }, options: { where: { id: 0 } }, expect: 'reject' }, + { what: 'empty where, no multi', data: { title: 'x' }, options: { where: {} }, expect: 'reject' }, + { what: 'no options at all', data: { title: 'x' }, options: undefined, expect: 'reject' }, + { what: 'multi explicitly false with a predicate', data: { title: 'x' }, options: { where: { tenant: 't1' }, multi: false }, expect: 'reject' }, +]; diff --git a/packages/metadata-core/src/index.ts b/packages/metadata-core/src/index.ts index e9343ddbe1..1c09f5ed3a 100644 --- a/packages/metadata-core/src/index.ts +++ b/packages/metadata-core/src/index.ts @@ -15,3 +15,14 @@ export * from './cache.js'; export * from './layered-repository.js'; export * from './protocol-handshake.js'; export * from './objects/index.js'; + +// [#5619] The ObjectQL WRITE-VERB dispatch predicates (#4550 delete / #5480 +// update), sunk here from `@objectstack/objectql` so that a package objectql +// itself depends on — `@objectstack/metadata-protocol` — can bind its fake +// engines to the producer's own decision instead of a hand-written copy of it. +// The reverse import would have closed a turbo-rejected cycle; this package is +// the one both sides already depend on, and it depends on neither. `objectql` +// re-exports all of it from the original paths, so its public API is unchanged. +// See `scripts/check-engine-double-contract.mjs` — the gate over the doubles. +export * from './engine-delete-dispatch.js'; +export * from './engine-update-dispatch.js'; diff --git a/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts b/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts index 16dcd308c4..ed2839058e 100644 --- a/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts +++ b/packages/metadata-protocol/src/migrations/recorded-by-sentinel.test.ts @@ -22,6 +22,15 @@ import { describe, it, expect } from 'vitest'; import { runMigrationJournal, readRunJournal } from '@objectstack/core'; +// [#5619] The producer's OWN write-verb dispatch decisions. Imported from +// `@objectstack/metadata-core` rather than `@objectstack/objectql` because +// objectql depends on THIS package — the import that would pin these doubles to +// objectql closes a cycle turbo refuses. #5619 sank the two predicates into a +// package both sides already depend on, which is what makes this line legal. +import { + assertEngineDeleteDispatch, + assertEngineUpdateDispatch, +} from '@objectstack/metadata-core'; import { createRecordedBySentinelPlan, findSentinelHistoryRows, @@ -76,6 +85,7 @@ class FakeEngine { data: FakeRow, options?: { where?: Record; context?: unknown }, ): Promise { + assertEngineUpdateDispatch(data, options); this.updateContexts.push(options?.context); const id = options?.where?.id; if (this.failOnUpdateOfId !== null && id === this.failOnUpdateOfId) { @@ -87,7 +97,10 @@ class FakeEngine { return row; } - async delete(): Promise { return { deleted: 0 }; } + async delete(_objectName: string, options?: { where?: Record; multi?: unknown }): Promise { + assertEngineDeleteDispatch(options); + return { deleted: 0 }; + } async count(): Promise { return 0; } async aggregate(): Promise { return []; } getObject(name: string): unknown { return { name }; } diff --git a/packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts b/packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts index 563e086b10..8f2268f842 100644 --- a/packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts +++ b/packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts @@ -1,6 +1,14 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { identityFreeEndpointGateFailure, ApiEndpointSchema } from '@objectstack/spec/api'; import { ObjectStackProtocolImplementation } from './protocol.js'; @@ -146,6 +154,7 @@ function makeStubEngine(namespace?: string) { return { id: row.id }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) return { id: null }; const merged = { ...found.row, ...(data as any) }; @@ -154,6 +163,7 @@ function makeStubEngine(namespace?: string) { return { id: found.row.id }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts b/packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts index e44c4ec782..a5d4d6b350 100644 --- a/packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts +++ b/packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts @@ -1,6 +1,14 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from './protocol.js'; /** @@ -143,6 +151,7 @@ function makeStubEngine() { return { id: row.id }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) return { id: null }; const merged = { ...found.row, ...(data as any) }; @@ -151,6 +160,7 @@ function makeStubEngine() { return { id: found.row.id }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/metadata-protocol/src/protocol.code-only-types.test.ts b/packages/metadata-protocol/src/protocol.code-only-types.test.ts index 1ec050abdf..7412be76b5 100644 --- a/packages/metadata-protocol/src/protocol.code-only-types.test.ts +++ b/packages/metadata-protocol/src/protocol.code-only-types.test.ts @@ -29,6 +29,14 @@ * cannot be tested against a harness that mocks `saveMetaItem`. */ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { DEFAULT_METADATA_TYPE_REGISTRY } from '@objectstack/spec/kernel'; import { ObjectStackProtocolImplementation } from './protocol.js'; import { resetEnvWritableMetadataTypes } from './sys-metadata-repository.js'; @@ -108,8 +116,16 @@ function makeStubEngine(artifacts: Array<{ type: string; name: string }> = []) { rows.set(keyOf(data), row); return { id: row.id }; }, - async update(_t: string) { writes.push({ op: 'update', table: _t }); return { id: null }; }, - async delete(_t: string) { writes.push({ op: 'delete', table: _t }); return { deleted: 0 }; }, + async update(_t: string, data: Record, opts?: Record) { + assertEngineUpdateDispatch(data, opts); + writes.push({ op: 'update', table: _t }); + return { id: null }; + }, + async delete(_t: string, opts?: Record) { + assertEngineDeleteDispatch(opts); + writes.push({ op: 'delete', table: _t }); + return { deleted: 0 }; + }, registry: { registerItem: () => {}, registerObject: () => {}, diff --git a/packages/metadata-protocol/src/protocol.read-decorations.test.ts b/packages/metadata-protocol/src/protocol.read-decorations.test.ts index 30ea3b37ea..cfd2820075 100644 --- a/packages/metadata-protocol/src/protocol.read-decorations.test.ts +++ b/packages/metadata-protocol/src/protocol.read-decorations.test.ts @@ -22,6 +22,14 @@ * future third decoration key fails here instead of in a production boot log. */ import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { FlowSchema } from '@objectstack/spec/automation'; import { METADATA_READ_DECORATIONS } from '@objectstack/spec/kernel'; import { ObjectStackProtocolImplementation, stripReadDecorations } from './index.js'; @@ -71,6 +79,7 @@ function makeStubEngine() { return { id: row.id }; }, async update(table: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); if (table !== 'sys_metadata') return { id: null }; const found = findRow(opts.where); if (!found) return { id: null }; @@ -79,7 +88,10 @@ function makeStubEngine() { rows.set(keyOf(merged), merged); return { id: found.row.id }; }, - async delete() { return { deleted: 0 }; }, + async delete(_t: string, opts?: Record) { + assertEngineDeleteDispatch(opts); + return { deleted: 0 }; + }, async transaction(cb: (ctx: any) => Promise): Promise { return cb(undefined); }, async syncObjectSchema() { /* no DDL in this stub */ }, registry: { diff --git a/packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts b/packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts index 5eeb2c1827..50a759ddfa 100644 --- a/packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts +++ b/packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts @@ -23,6 +23,14 @@ * `saveMetaItem` cannot be tested against a harness that mocks `saveMetaItem`. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from './protocol.js'; /** The issue's body. Zod-valid: `approvers[].value` is just a string to the schema. */ @@ -104,12 +112,14 @@ function makeStubEngine() { return { id: row.id }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) return { id: null }; rows.set(found.key, { ...found.row, ...(data as any) }); return { id: found.row.id }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts b/packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts index dd80a814f7..5ab263c193 100644 --- a/packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts +++ b/packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts @@ -17,7 +17,19 @@ * a fix INSIDE `saveMetaItem` cannot use. */ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { hashSpec } from '@objectstack/metadata-core'; +// [#5619] `assertEngine{Delete,Update}Dispatch` are the producer's OWN write-verb +// dispatch decisions (#4550 delete / #5480 update), so the fake engine below +// cannot accept a call ObjectQL refuses. They arrive from +// `@objectstack/metadata-core` and not from `@objectstack/objectql`: objectql +// DEPENDS ON this package, so that import would close a dependency cycle turbo +// rejects outright — which is why all 26 of this package's (file, verb) pairs sat +// in the gate's DEBT ledger until #5619 sank the two predicates into a package +// both sides already depend on. +import { + hashSpec, + assertEngineDeleteDispatch, + assertEngineUpdateDispatch, +} from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from './protocol.js'; /** A flow body that passes `saveMetaItem`'s schema gate. */ @@ -107,12 +119,14 @@ function makeStubEngine() { return { id: row.id }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) return { id: null }; rows.set(found.key, { ...found.row, ...(data as any) }); return { id: found.row.id }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/metadata-protocol/src/protocol.save-union-issues.test.ts b/packages/metadata-protocol/src/protocol.save-union-issues.test.ts index 1cd2e4f70d..7bbc8c21c0 100644 --- a/packages/metadata-protocol/src/protocol.save-union-issues.test.ts +++ b/packages/metadata-protocol/src/protocol.save-union-issues.test.ts @@ -23,6 +23,14 @@ * cannot use a harness that mocks `saveMetaItem`. */ import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { getMetadataTypeSchema } from '@objectstack/spec/kernel'; import { ObjectStackProtocolImplementation, zodIssuesToMetadataIssues } from './protocol.js'; @@ -52,8 +60,14 @@ function makeProtocol() { rows.set(keyOf(data), row); return { id: row.id }; }, - async update() { return { id: null }; }, - async delete() { return { deleted: 0 }; }, + async update(_t: string, data: Record, opts?: Record) { + assertEngineUpdateDispatch(data, opts); + return { id: null }; + }, + async delete(_t: string, opts?: Record) { + assertEngineDeleteDispatch(opts); + return { deleted: 0 }; + }, registry: { registerItem: () => {}, registerObject: () => {} }, }; const protocol: any = new ObjectStackProtocolImplementation(engine, () => new Map()); diff --git a/packages/metadata-protocol/src/protocol.stored-conversions.test.ts b/packages/metadata-protocol/src/protocol.stored-conversions.test.ts index bc46b94a27..e79e68ad3d 100644 --- a/packages/metadata-protocol/src/protocol.stored-conversions.test.ts +++ b/packages/metadata-protocol/src/protocol.stored-conversions.test.ts @@ -16,6 +16,14 @@ * years ago under an older protocol. */ import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from './protocol.js'; interface Row { @@ -56,8 +64,14 @@ function makeStubEngine(seedRows: Array & { type: string; name: str return rows.filter((r) => matches(r, opts.where)); }, async insert() { return { id: 'x' }; }, - async update() { return { id: 'x' }; }, - async delete() { return { deleted: 0 }; }, + async update(_t: string, data: Record, opts?: Record) { + assertEngineUpdateDispatch(data, opts); + return { id: 'x' }; + }, + async delete(_t: string, opts?: Record) { + assertEngineDeleteDispatch(opts); + return { deleted: 0 }; + }, registry: { listItems: () => [], isPackageDisabled: () => false, diff --git a/packages/metadata-protocol/src/protocol.stored-migration.test.ts b/packages/metadata-protocol/src/protocol.stored-migration.test.ts index f69d88cd67..c4fc9cde89 100644 --- a/packages/metadata-protocol/src/protocol.stored-migration.test.ts +++ b/packages/metadata-protocol/src/protocol.stored-migration.test.ts @@ -18,6 +18,14 @@ * it declines to touch says so instead of being silently counted clean. */ import { describe, expect, it } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { ObjectStackProtocolImplementation } from './protocol.js'; import { formatStoredMigrationReport, storedMigrationClean } from './stored-migration.js'; @@ -83,11 +91,15 @@ function makeStubEngine( return withId; }, async update(t: string, patch: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(patch, opts); const target = rowsOf(t).find((r) => matches(r, opts.where)); if (target) Object.assign(target, patch); return target ?? { id: 'x' }; }, - async delete() { return { deleted: 0 }; }, + async delete(_t: string, opts?: Record) { + assertEngineDeleteDispatch(opts); + return { deleted: 0 }; + }, registry: { listItems: () => [], isPackageDisabled: () => false, diff --git a/packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts b/packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts index 3c84be631d..77bfbb1f22 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts @@ -27,6 +27,14 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { readFileSync } from 'node:fs'; import { SysMetadataRepository } from './sys-metadata-repository.js'; @@ -158,12 +166,14 @@ function makeFakeEngine() { data: Record, opts: { where: Record }, ) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) throw new Error('not found'); rows.set(found.key, { ...found.row, ...data }); return { id: found.row.id as string }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (draftDeleteFailure && found?.row.state === 'draft') throw draftDeleteFailure(); if (!found) return { deleted: 0 }; diff --git a/packages/metadata-protocol/src/sys-metadata-repository.history-counters.test.ts b/packages/metadata-protocol/src/sys-metadata-repository.history-counters.test.ts index 6bf44b3700..c5b9b27c6c 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.history-counters.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.history-counters.test.ts @@ -24,6 +24,14 @@ */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { readFileSync } from 'node:fs'; import { SysMetadataRepository } from './sys-metadata-repository.js'; import { isMissingTableError } from '@objectstack/metadata/errors'; @@ -137,12 +145,14 @@ function makeFakeEngine() { return { id: row.id as string }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) throw new Error('not found'); rows.set(found.key, { ...found.row, ...data }); return { id: found.row.id as string }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts b/packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts index 89d2283539..a0cc471023 100644 --- a/packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts +++ b/packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts @@ -17,6 +17,14 @@ */ import { describe, it, expect, beforeEach } from 'vitest'; +// [#5619] The producer's OWN write-verb dispatch decisions (#4550 delete / +// #5480 update), so the fake engine below 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 outright — which is why all 26 +// of this package's (file, verb) pairs sat in the gate's DEBT ledger until +// #5619 sank the two predicates into a package both sides already depend on. +import { assertEngineDeleteDispatch, assertEngineUpdateDispatch } from '@objectstack/metadata-core'; import { SysMetadataRepository } from './sys-metadata-repository.js'; interface Row { [k: string]: unknown } @@ -75,12 +83,14 @@ function makeFakeEngine() { return { id: row.id as string }; }, async update(_t: string, data: Record, opts: { where: Record }) { + assertEngineUpdateDispatch(data, opts); const found = findRow(opts.where); if (!found) throw new Error('not found'); rows.set(found.key, { ...found.row, ...data }); return { id: found.row.id as string }; }, async delete(_t: string, opts: { where: Record }) { + assertEngineDeleteDispatch(opts); const found = findRow(opts.where); if (!found) return { deleted: 0 }; rows.delete(found.key); diff --git a/packages/objectql/src/engine-delete-dispatch.ts b/packages/objectql/src/engine-delete-dispatch.ts index 27366cabbe..90551e4b97 100644 --- a/packages/objectql/src/engine-delete-dispatch.ts +++ b/packages/objectql/src/engine-delete-dispatch.ts @@ -1,168 +1,45 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * The **one** answer to "what does `ObjectQLEngine.delete` do with this call?" - * — extracted so that the engine and every test double that stands in for it - * read the same predicate rather than two hand-written approximations of it - * (objectstack#4550, from objectstack#4434). - * - * ## Why this is a shared module and not four lines inside `engine.ts` - * - * `#4434` shipped green. `DELETE /api/v1/sharing/rules/:idOrName` answered 500 - * for **both** address forms the route advertises, for every rule, from the day - * it was written — and `plugin-sharing`'s `deleteRule drops rule + all its - * grants` test asserted success against it the whole time. The route was not - * untested; it was tested against a **fake engine whose `delete` accepted a - * call the real engine refuses**. A predicate-shaped purge of - * `sys_record_share` (no scalar `where.id`, no `options.multi`) is precisely - * the one shape `delete()` throws on, and the fake happily deleted by - * predicate. - * - * The fix for #4434 mirrored the guard into that fake by hand. That closes one - * fake and starts a second copy of the contract — the failure mode this module - * exists to remove. A double that *imports the producer's own decision* cannot - * be looser than the producer, ever, which is the property the gate wants and - * the property a copy can only have until someone edits one side. - * - * Same reasoning as `packages/spec/src/data/*-conformance.ts` for drivers, and - * the same shape as objectstack#4455: **the scan and the validator must answer - * with one predicate.** - * - * ## The contract, normatively - * - * `delete(object, options)` dispatches on exactly one question — *does this - * call identify a single row by primary key?* - * - * - `options.where.id` is a **scalar** (`string` / `number` / `bigint`, not - * `null`) → `by-id`: routes to `driver.delete`, runs cascade-delete and the - * by-id RLS pre-image check. - * - otherwise, `options.multi` is truthy → `multi`: routes to - * `driver.deleteMany` with the middleware-composed AST. - * - otherwise → **`reject`**. The call names neither one row nor a bulk - * intent, and the engine throws rather than guessing. - * - * The scalar test is load-bearing and is the half a hand-written double most - * often drops: `where: { id: { $in: [...] } }` is a *multi-row predicate*, not - * an id. Treating it as an id would bind the operator object literally into - * `driver.delete(object, {$in: […]})` **and** skip both the row-scoping AST - * seeding (#2982) and the by-id pre-image check. So it is `reject` unless the - * caller also said `multi`. - * - * @see ObjectQL.delete in `engine.ts` — the only production caller. - * @see scripts/check-engine-double-contract.mjs — the gate that keeps doubles on it. - */ - -/** The message `delete()` throws when a call identifies neither one row nor a bulk intent. */ -export const ENGINE_DELETE_REJECT_MESSAGE = 'Delete requires an ID or options.multi=true'; - -/** What `ObjectQLEngine.delete` will do with a given options bag. */ -export type EngineDeleteDispatch = - /** A scalar `where.id` — `driver.delete`, cascade + by-id RLS pre-image. */ - | { readonly kind: 'by-id'; readonly id: string | number | bigint } - /** No single id but `options.multi` — `driver.deleteMany` with the composed AST. */ - | { readonly kind: 'multi' } - /** Neither — the engine throws `ENGINE_DELETE_REJECT_MESSAGE`. */ - | { readonly kind: 'reject'; readonly message: string }; - -/** The subset of `EngineDeleteOptions` the dispatch decision actually reads. */ -export interface EngineDeleteDispatchInput { - readonly where?: unknown; - readonly multi?: unknown; - readonly [k: string]: unknown; -} - -/** - * Extract the SCALAR `where.id`, or `undefined` when the call does not name one - * row by primary key. - * - * `null`, `undefined`, arrays, and operator objects (`{ $in: [...] }`, - * `{ $ne: … }`) all yield `undefined` — they are predicates over many rows, not - * a primary key. - */ -export function scalarDeleteId( - options?: EngineDeleteDispatchInput | null, -): string | number | bigint | undefined { - const where = options?.where; - if (!where || typeof where !== 'object') return undefined; - if (!('id' in (where as Record))) return undefined; - const whereId = (where as Record).id; - const t = typeof whereId; - if (whereId !== null && (t === 'string' || t === 'number' || t === 'bigint')) { - return whereId as string | number | bigint; - } - return undefined; -} - -/** - * Decide what `ObjectQLEngine.delete` does with `options`, without doing it. - * - * Pure and side-effect free, so a test double can call it to *classify* a call - * and then implement `by-id` / `multi` however its fixture stores rows — while - * being bound to the real engine's `reject` surface for free. - */ -export function resolveEngineDeleteDispatch( - options?: EngineDeleteDispatchInput | null, -): EngineDeleteDispatch { - const id = scalarDeleteId(options); - if (id !== undefined) return { kind: 'by-id', id }; - if (options?.multi) return { kind: 'multi' }; - return { kind: 'reject', message: ENGINE_DELETE_REJECT_MESSAGE }; -} - -/** - * Throw exactly what `ObjectQLEngine.delete` throws when a call is neither - * `by-id` nor `multi`; return the resolved dispatch otherwise. - * - * This is the line a fake engine's `delete` opens with. One call pins the fake - * to the producer's rejection surface, and — unlike a mirrored `if` — it cannot - * drift when the producer's rule changes. - * - * ```ts - * async delete(object: string, options?: any) { - * assertEngineDeleteDispatch(options); // refuses what a real server refuses - * … - * } - * ``` - */ -export function assertEngineDeleteDispatch( - options?: EngineDeleteDispatchInput | null, -): Exclude { - const dispatch = resolveEngineDeleteDispatch(options); - if (dispatch.kind === 'reject') throw new Error(dispatch.message); - return dispatch; -} - -/** - * The shared conformance case-set for the delete dispatch — the same role - * `packages/spec/src/data/*-conformance.ts` plays for drivers. - * - * Every case names a call shape and the verdict the **real engine** gives it. - * A double proved against these is proved against the producer, including the - * three shapes that look like an id and are not. + * The delete-dispatch predicate's original path, kept as a **re-export of its + * new home** — `@objectstack/metadata-core` (objectstack#5619). + * + * ## Why the module moved, and why this file did not + * + * The predicate was born here (objectstack#4550) next to its only production + * caller, `ObjectQL.delete` in `engine.ts`. That location made it unreachable + * for one whole package of test doubles: `@objectstack/objectql` **depends on** + * `@objectstack/metadata-protocol`, so the thirteen fake engines there could not + * import it without closing a cycle turbo refuses outright. Sinking it into + * `@objectstack/metadata-core` — a package both sides already depend on, and + * which depends on neither — is the only route that pins those doubles without + * inventing a dependency edge. The reasoning, with the measured cycle, is in the + * module header at the new location. + * + * This file stays because the move must be invisible to every caller. `engine.ts` + * and this package's own pinned test doubles import `./engine-delete-dispatch.js`; + * `index.ts` re-exports the public API from it. One re-export keeps all of that + * byte-identical, so the sink changes no import in the repo that did not have to + * change. + * + * @see @objectstack/metadata-core `src/engine-delete-dispatch.ts` — the implementation. + * @see engine-delete-dispatch.test.ts — the case-set driven against the REAL engine, + * which stays in this package because it needs `ObjectQL`. + * @see scripts/check-engine-double-contract.mjs — the gate; its `delete` slice accepts + * `@objectstack/objectql` (this path's public spelling), `@objectstack/metadata-core` + * (the new home) and the relative path, so a double may pin through any of them. */ -export interface EngineDeleteDispatchCase { - /** What the shape is, in the words a failure message should use. */ - readonly what: string; - /** The options bag handed to `delete(object, options)`. */ - readonly options: EngineDeleteDispatchInput | undefined; - /** The verdict the engine gives it. */ - readonly expect: EngineDeleteDispatch['kind']; -} -export const ENGINE_DELETE_DISPATCH_CASES: readonly EngineDeleteDispatchCase[] = [ - { what: 'scalar string id', options: { where: { id: 'rec_1' } }, expect: 'by-id' }, - { what: 'scalar number id', options: { where: { id: 42 } }, expect: 'by-id' }, - { what: 'scalar id alongside other predicates', options: { where: { id: 'rec_1', tenant: 't1' } }, expect: 'by-id' }, - { what: 'multi with a predicate', options: { where: { rule_id: 'r1' }, multi: true }, expect: 'multi' }, - { what: 'multi with no predicate at all', options: { multi: true }, expect: 'multi' }, - { what: 'multi alongside an $in id set', options: { where: { id: { $in: ['a', 'b'] } }, multi: true }, expect: 'multi' }, - // ── The rejects. Everything below is what #4434 shipped against a fake that - // accepted it, and what a running server answers 500 to. - { what: 'predicate on a non-id column, no multi', options: { where: { rule_id: 'r1' } }, expect: 'reject' }, - { what: '$in over ids, no multi (an operator object is NOT an id)', options: { where: { id: { $in: ['a', 'b'] } } }, expect: 'reject' }, - { what: 'array id, no multi', options: { where: { id: ['a', 'b'] } }, expect: 'reject' }, - { what: 'null id, no multi', options: { where: { id: null } }, expect: 'reject' }, - { what: 'empty where, no multi', options: { where: {} }, expect: 'reject' }, - { what: 'no options at all', options: undefined, expect: 'reject' }, - { what: 'multi explicitly false with a predicate', options: { where: { rule_id: 'r1' }, multi: false }, expect: 'reject' }, -]; +export { + ENGINE_DELETE_REJECT_MESSAGE, + scalarDeleteId, + resolveEngineDeleteDispatch, + assertEngineDeleteDispatch, + ENGINE_DELETE_DISPATCH_CASES, +} from '@objectstack/metadata-core'; + +export type { + EngineDeleteDispatch, + EngineDeleteDispatchInput, + EngineDeleteDispatchCase, +} from '@objectstack/metadata-core'; diff --git a/packages/objectql/src/engine-update-dispatch.ts b/packages/objectql/src/engine-update-dispatch.ts index 800c05ae6c..d3fd11562d 100644 --- a/packages/objectql/src/engine-update-dispatch.ts +++ b/packages/objectql/src/engine-update-dispatch.ts @@ -1,229 +1,31 @@ // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. /** - * The **one** answer to "what does `ObjectQLEngine.update` do with this call?" - * — the twin of `engine-delete-dispatch.ts`, extracted for the same reason and - * on the same terms (objectstack#5480, from objectstack#4550 / objectstack#4434). - * - * ## Why this exists as a module - * - * `delete` got its shared predicate because #4434 shipped a dead REST route - * green: a fake engine accepted the one call shape the real `delete` refuses, - * so the suite proved nothing about the path it was written for. `update` has - * the *same* three-way dispatch and — until this module — none of the defence. - * #5393 hit the asymmetry from the consumer side: writing real contract tests - * for the flow `update_record` / `delete_record` executors, the delete half - * could bind its fake to `assertEngineDeleteDispatch` while the update half - * could only assert the options bag the executor hands over, "without a second - * opinion on whether the engine would accept it" — because the only - * alternative was hand-copying the rule into the fake, which is the failure - * mode this family of modules exists to remove. - * - * A double that *imports the producer's own decision* cannot be looser than - * the producer, ever. A hand-mirrored `if` can only stay honest until someone - * edits one side — and the half a copy drops is always the same one: - * `where: { id: { $in: [...] } }` looks like an id and is a multi-row - * predicate. - * - * ## The contract, normatively - * - * `update(object, data, options)` dispatches on exactly one question — *does - * this call identify a single row by primary key?* — asked of two places, in - * this order: - * - * - **`data.id`, taken verbatim when truthy** → `by-id`: routes to - * `driver.update(object, id, data, …)`. - * - otherwise, `options.where.id` is a **scalar** (`string` / `number` / - * `bigint`, not `null`) **and truthy** → `by-id`, same route. - * - otherwise, `options.multi` is truthy → `multi`: routes to - * `driver.updateMany` with the middleware-composed AST (#2982). - * - otherwise → **`reject`**. The call names neither one row nor a bulk - * intent, and the engine throws rather than rewriting every row it can see. - * - * Three things about that list are load-bearing and easy to get wrong when - * copying it by hand — which is the whole argument for importing it instead: - * - * 1. **The `where.id` scalar test.** `{ id: { $in: [...] } }` / `{ id: [...] }` - * / `{ id: null }` are predicates over many rows. Treating one as an id - * would bind the operator object literally into `driver.update(object, - * {$in: […]}, …)` **and** skip the #2982 row-scoping AST seeding. So they - * are `reject` unless the caller also said `multi`. - * 2. **`data.id` is NOT scalar-tested.** `ObjectQL.update` reads `data.id` - * first and uses it as-is whenever it is truthy, so an operator object - * parked there wins over everything below it — including an explicit - * `multi: true`. This module reports that verdict rather than quietly - * improving on it: a predicate that is *better* than the producer is a - * second opinion, which is exactly what #4550 removed. (The asymmetry - * itself is filed as objectstack#5748; when it is fixed, it is fixed **here - * and in `engine.ts` together**, which is now one edit instead of two.) - * 3. **Truthiness, not `!== undefined`.** The engine branches on - * `if (hookContext.input.id)`, so a falsy scalar id — `where: { id: 0 }`, - * `where: { id: '' }` — does **not** take the by-id route; it falls through - * to `multi`/`reject` like any other non-identifying call. - * - * ## What the predicate deliberately does NOT model - * - * `engine.ts`'s bulk branch reads `options.multi && driver.updateMany`, i.e. a - * driver with no `updateMany` turns a `multi` call into the same throw. That - * is a *driver capability*, not a property of the call, and a double answering - * for a fixture's own storage has no `driver.updateMany` to consult. Same - * choice `resolveEngineDeleteDispatch` makes for `driver.deleteMany`: this - * module classifies the CALL, and the engine keeps the capability check. - * - * @see ObjectQL.update in `engine.ts` — the only production caller. - * @see engine-delete-dispatch.ts — the twin, and the precedent. - * @see scripts/check-engine-double-contract.mjs — the gate that keeps doubles on both. - */ - -/** The message `update()` throws when a call identifies neither one row nor a bulk intent. */ -export const ENGINE_UPDATE_REJECT_MESSAGE = 'Update requires an ID or options.multi=true'; - -/** What `ObjectQLEngine.update` will do with a given `(data, options)` pair. */ -export type EngineUpdateDispatch = - /** A truthy `data.id`, or a truthy scalar `where.id` — `driver.update`. */ - | { readonly kind: 'by-id'; readonly id: unknown } - /** No single id but `options.multi` — `driver.updateMany` with the composed AST. */ - | { readonly kind: 'multi' } - /** Neither — the engine throws `ENGINE_UPDATE_REJECT_MESSAGE`. */ - | { readonly kind: 'reject'; readonly message: string }; - -/** The subset of `EngineUpdateOptions` the dispatch decision actually reads. */ -export interface EngineUpdateDispatchInput { - readonly where?: unknown; - readonly multi?: unknown; - readonly [k: string]: unknown; -} - -/** The subset of the update PAYLOAD the dispatch decision reads: `id`, and nothing else. */ -export interface EngineUpdateDispatchData { - readonly id?: unknown; - readonly [k: string]: unknown; -} - -/** - * Extract the SCALAR `where.id`, or `undefined` when the call's `where` does - * not name one row by primary key. - * - * Byte-for-byte the same rule as `scalarDeleteId` — `null`, `undefined`, - * arrays and operator objects (`{ $in: [...] }`, `{ $ne: … }`) all yield - * `undefined`, because they are predicates over many rows. - * - * Note this covers only the `where` half of the update decision; `data.id` - * outranks it and is taken verbatim (see the module header, point 2). Use - * {@link resolveEngineUpdateDispatch} for the whole answer. - */ -export function scalarUpdateId( - options?: EngineUpdateDispatchInput | null, -): string | number | bigint | undefined { - const where = options?.where; - if (!where || typeof where !== 'object') return undefined; - if (!('id' in (where as Record))) return undefined; - const whereId = (where as Record).id; - const t = typeof whereId; - if (whereId !== null && (t === 'string' || t === 'number' || t === 'bigint')) { - return whereId as string | number | bigint; - } - return undefined; -} - -/** - * Decide what `ObjectQLEngine.update` does with `(data, options)`, without - * doing it. - * - * Pure and side-effect free, so a test double can call it to *classify* a call - * and then implement `by-id` / `multi` however its fixture stores rows — while - * being bound to the real engine's `reject` surface for free. - * - * `data` is read UNGUARDED (`data.id`, no optional chaining) on purpose: - * `ObjectQL.update` reads it that way, so `update(object, undefined)` is a - * `TypeError` there and must be a `TypeError` here. A double that is kinder - * than the producer about a missing payload is a double that hides the - * producer's behaviour — the thing this module exists to prevent. - */ -export function resolveEngineUpdateDispatch( - data: EngineUpdateDispatchData, - options?: EngineUpdateDispatchInput | null, -): EngineUpdateDispatch { - // `let id = data.id; if (!id && ) id = whereId;` — the - // producer's own two lines, in the producer's own order. - let id: unknown = data.id; - if (!id) { - const fromWhere = scalarUpdateId(options); - if (fromWhere !== undefined) id = fromWhere; - } - // The engine branches on `if (hookContext.input.id)` — truthiness, so a - // falsy scalar id is not an identifying call. See header point 3. - if (id) return { kind: 'by-id', id }; - if (options?.multi) return { kind: 'multi' }; - return { kind: 'reject', message: ENGINE_UPDATE_REJECT_MESSAGE }; -} - -/** - * Throw exactly what `ObjectQLEngine.update` throws when a call is neither - * `by-id` nor `multi`; return the resolved dispatch otherwise. - * - * This is the line a fake engine's `update` opens with. One call pins the fake - * to the producer's rejection surface, and — unlike a mirrored `if` — it cannot - * drift when the producer's rule changes. - * - * ```ts - * async update(object: string, data: any, options?: any) { - * assertEngineUpdateDispatch(data, options); // refuses what a real server refuses - * … - * } - * ``` - */ -export function assertEngineUpdateDispatch( - data: EngineUpdateDispatchData, - options?: EngineUpdateDispatchInput | null, -): Exclude { - const dispatch = resolveEngineUpdateDispatch(data, options); - if (dispatch.kind === 'reject') throw new Error(dispatch.message); - return dispatch; -} - -/** - * The shared conformance case-set for the update dispatch — the same role - * `ENGINE_DELETE_DISPATCH_CASES` plays for `delete`, and the same role - * `packages/spec/src/data/*-conformance.ts` plays for drivers. - * - * Every case names a call shape and the verdict the **real engine** gives it. - * A double proved against these is proved against the producer, including the - * shapes that look like an id and are not — and the one that does not look - * like an id and is (`data.id`). + * The update-dispatch predicate's original path, kept as a **re-export of its + * new home** — `@objectstack/metadata-core` (objectstack#5619). + * + * Same move, same reason, same day as its twin: see + * `engine-delete-dispatch.ts` beside this file for the short version and the + * module header at the new location for the measured turbo cycle that forced + * it. The predicate itself is unchanged — this is a move, not a rewrite. + * + * @see @objectstack/metadata-core `src/engine-update-dispatch.ts` — the implementation. + * @see engine-update-dispatch.test.ts — the case-set driven against the REAL engine, + * which stays in this package because it needs `ObjectQL`. + * @see scripts/check-engine-double-contract.mjs — the gate that keeps doubles on it. */ -export interface EngineUpdateDispatchCase { - /** What the shape is, in the words a failure message should use. */ - readonly what: string; - /** The payload handed to `update(object, data, options)`. */ - readonly data: EngineUpdateDispatchData; - /** The options bag handed to `update(object, data, options)`. */ - readonly options: EngineUpdateDispatchInput | undefined; - /** The verdict the engine gives it. */ - readonly expect: EngineUpdateDispatch['kind']; -} -export const ENGINE_UPDATE_DISPATCH_CASES: readonly EngineUpdateDispatchCase[] = [ - // ── by-id via `where`. - { what: 'scalar string where.id', data: { title: 'x' }, options: { where: { id: 'rec_1' } }, expect: 'by-id' }, - { what: 'scalar number where.id', data: { title: 'x' }, options: { where: { id: 42 } }, expect: 'by-id' }, - { what: 'scalar where.id alongside other predicates', data: { title: 'x' }, options: { where: { id: 'rec_1', tenant: 't1' } }, expect: 'by-id' }, - // ── by-id via the PAYLOAD, which outranks `where` and `multi` alike. - { what: 'id carried in the data payload, no where at all', data: { id: 'rec_1', title: 'x' }, options: undefined, expect: 'by-id' }, - { what: 'data.id wins over an explicit multi:true', data: { id: 'rec_1', title: 'x' }, options: { where: { tenant: 't1' }, multi: true }, expect: 'by-id' }, - // ── multi. - { what: 'multi with a predicate', data: { title: 'x' }, options: { where: { tenant: 't1' }, multi: true }, expect: 'multi' }, - { what: 'multi with no predicate at all', data: { title: 'x' }, options: { multi: true }, expect: 'multi' }, - { what: 'multi alongside an $in id set', data: { title: 'x' }, options: { where: { id: { $in: ['a', 'b'] } }, multi: true }, expect: 'multi' }, - { what: 'multi with a FALSY data.id (0 does not identify a row)', data: { id: 0, title: 'x' }, options: { multi: true }, expect: 'multi' }, - // ── The rejects. Every one of these is a call a fake that mirrors the rule - // by hand tends to accept, and a running server answers 500 to. - { what: 'predicate on a non-id column, no multi', data: { title: 'x' }, options: { where: { tenant: 't1' } }, expect: 'reject' }, - { what: '$in over ids, no multi (an operator object is NOT an id)', data: { title: 'x' }, options: { where: { id: { $in: ['a', 'b'] } } }, expect: 'reject' }, - { what: 'array id, no multi', data: { title: 'x' }, options: { where: { id: ['a', 'b'] } }, expect: 'reject' }, - { what: 'null id, no multi', data: { title: 'x' }, options: { where: { id: null } }, expect: 'reject' }, - { what: 'falsy scalar where.id (0), no multi', data: { title: 'x' }, options: { where: { id: 0 } }, expect: 'reject' }, - { what: 'empty where, no multi', data: { title: 'x' }, options: { where: {} }, expect: 'reject' }, - { what: 'no options at all', data: { title: 'x' }, options: undefined, expect: 'reject' }, - { what: 'multi explicitly false with a predicate', data: { title: 'x' }, options: { where: { tenant: 't1' }, multi: false }, expect: 'reject' }, -]; +export { + ENGINE_UPDATE_REJECT_MESSAGE, + scalarUpdateId, + resolveEngineUpdateDispatch, + assertEngineUpdateDispatch, + ENGINE_UPDATE_DISPATCH_CASES, +} from '@objectstack/metadata-core'; + +export type { + EngineUpdateDispatch, + EngineUpdateDispatchInput, + EngineUpdateDispatchData, + EngineUpdateDispatchCase, +} from '@objectstack/metadata-core'; diff --git a/scripts/check-engine-double-contract.mjs b/scripts/check-engine-double-contract.mjs index 127f490888..594d9e0988 100644 --- a/scripts/check-engine-double-contract.mjs +++ b/scripts/check-engine-double-contract.mjs @@ -76,10 +76,12 @@ // print OK while checking nothing -- the #4868 family, where a // check runs, is green, and structurally cannot reach its subject. // PINNED every discovered engine double's verb routes through that -// slice's `assert…Dispatch` / `resolve…Dispatch` from -// `@objectstack/objectql` -- the predicate the real -// `ObjectQL.` itself uses -- or its file carries a measured -// baseline entry for that verb. +// slice's `assert…Dispatch` / `resolve…Dispatch` -- the predicate +// the real `ObjectQL.` itself uses, importable from +// `@objectstack/metadata-core` (where it lives since #5619) or +// from `@objectstack/objectql` (which re-exports it from the +// original path) -- or its file carries a measured baseline entry +// for that verb. // RECONCILED in both directions. A baseline entry for a file with no // unguarded doubles left, for a file that no longer exists, or // whose count is now lower, is an error. A ratchet that can only @@ -130,15 +132,41 @@ const SCAN_ROOTS = ['packages', 'examples']; * * `symbols` is the pair the producer exports (`assert…` throws, `resolve…` * classifies -- a double may legitimately use either), and `modules` is where - * they may come from: the public export, or objectql's own relative path for - * objectql's own tests. + * they may come from. + * + * ## Why `modules` names TWO packages and not one (#5619) + * + * The predicates were written in `packages/objectql/src/` and MOVED to + * `@objectstack/metadata-core` by #5619 -- the implementation lives there now, + * and `@objectstack/objectql` re-exports every symbol from the original paths. + * Both spellings therefore reach the SAME function, which is the only property + * this list has ever been about: a double that imports the producer's decision + * cannot be looser than the producer, whichever door it came through. + * + * The move was not cosmetic. `@objectstack/objectql` DEPENDS ON + * `@objectstack/metadata-protocol`, so that package's thirteen fake engines + * could not import from `@objectstack/objectql` at all -- turbo 2.10.7 rejects + * the resulting task graph outright ("Circular package dependency detected") -- + * and all 26 of their (file, verb) pairs sat in the ledger for that one + * structural reason. Sinking the predicate into a package both sides already + * depend on is the route the EXEMPT entry for + * `packages/spec/src/contracts/data-engine.test.ts` names, and #5619 took it. + * + * Note what this does NOT relax: `@objectstack/spec` stays unpinnable in + * principle, because `metadata-core` depends on `spec` -- the import would + * invert that edge exactly as it inverted `objectql -> spec`. That entry's + * EXEMPT reasoning survives the move unchanged. */ const SLICES = [ { verb: 'delete', producer: 'ObjectQL.delete', symbols: new Set(['assertEngineDeleteDispatch', 'resolveEngineDeleteDispatch']), - modules: [/^@objectstack\/objectql$/, /engine-delete-dispatch(\.js)?$/], + modules: [ + /^@objectstack\/objectql$/, + /^@objectstack\/metadata-core$/, + /engine-delete-dispatch(\.js)?$/, + ], pinCall: 'assertEngineDeleteDispatch(options)', origin: '#4434', }, @@ -146,7 +174,11 @@ const SLICES = [ verb: 'update', producer: 'ObjectQL.update', symbols: new Set(['assertEngineUpdateDispatch', 'resolveEngineUpdateDispatch']), - modules: [/^@objectstack\/objectql$/, /engine-update-dispatch(\.js)?$/], + modules: [ + /^@objectstack\/objectql$/, + /^@objectstack\/metadata-core$/, + /engine-update-dispatch(\.js)?$/, + ], pinCall: 'assertEngineUpdateDispatch(data, options)', origin: '#5480', }, @@ -533,8 +565,12 @@ function audit() { + `(line${unguarded.length > 1 ? 's' : ''} ${unguarded.map((d) => d.line).join(', ')}). ` + `A fake looser than ${slice.producer} is how #4434 shipped a dead REST route with its ` + `suite green. Open the fake's ${slice.verb} with \`${slice.pinCall}\` from ` - + "'@objectstack/objectql' (add it as a devDependency if the package lacks it), or add a " - + 'MEASURED entry to scripts/engine-double-contract.baseline.json saying why not — with ' + + "'@objectstack/metadata-core' (where the predicate lives since #5619) or from " + + "'@objectstack/objectql' (which re-exports it) — add whichever you pick as a " + + 'devDependency if the package lacks it, and prefer metadata-core when ' + + '@objectstack/objectql DEPENDS ON the package you are pinning, since that reverse edge ' + + 'is a cycle turbo refuses. Or add a MEASURED entry to ' + + 'scripts/engine-double-contract.baseline.json saying why not — with ' + `"verb": ${JSON.stringify(slice.verb)}.`, ); continue; diff --git a/scripts/engine-double-contract.baseline.json b/scripts/engine-double-contract.baseline.json index 35797e3b5e..4e72bb8bb2 100644 --- a/scripts/engine-double-contract.baseline.json +++ b/scripts/engine-double-contract.baseline.json @@ -71,7 +71,7 @@ "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 81. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/cloud-connection --dry`, no circular-dependency warning), then the edge was reverted.", + "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 81. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/cloud-connection --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -87,7 +87,7 @@ "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 100. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/cloud-connection --dry`, no circular-dependency warning), then the edge was reverted.", + "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 100. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/cloud-connection --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -103,8 +103,8 @@ "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 and #5206 entries in this ledger already record for metadata-protocol.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." + "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", @@ -112,215 +112,7 @@ "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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/migrations/recorded-by-sentinel.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 35. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/migrations/recorded-by-sentinel.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 35. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#5206): the devDependency route this ledger's other metadata-protocol entries prescribe is CYCLIC, re-measured on this branch rather than cited. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (`workspace:*`), so the reverse edge was added, `pnpm install` run, and turbo refused the graph outright — `WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol` / `x Cyclic dependency detected: @objectstack/objectql#build, @objectstack/metadata-protocol#build` (turbo 2.10.7, `turbo run build --filter=@objectstack/metadata-protocol --dry`) — then the edge and lockfile were reverted. Same cycle, same measurement method as the #4867 and #4981 entries below; this is the sixth metadata-protocol file to hit the route those entries already recorded as closed. The fake's delete was probed by replacing it with a throw: it IS exercised (the promote drops the published draft row) and only ever as `{ where: { id } }`, a scalar by-id delete routed through SysMetadataRepository.delete, so removing the method to escape the scan was not available either. But that is an argument about this file, not about the contract, so the entry stays DEBT rather than EXEMPT per this ledger's own rule.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #4987 — then open the fake's delete with it; the devDependency route is closed by the cycle above, for this file and for the five sibling metadata-protocol entries alike" - }, - { - "file": "packages/metadata-protocol/src/protocol-publish-drafts-endpoint-gate.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 122. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol-publish-drafts-org-scope.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4987): the devDependency route this entry used to prescribe DOES NOT EXIST — it is cyclic, not merely unreviewed. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (`workspace:*`), so any reverse edge closes a cycle by construction. Re-measured on #4987's branch rather than cited: the edge was added to metadata-protocol's devDependencies and turbo 2.10.7 refused BOTH task graphs outright — `WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol` / `x Cyclic dependency detected: @objectstack/objectql#build, @objectstack/metadata-protocol#build`, exit 1 from `turbo run build --filter=@objectstack/metadata-protocol --dry` and from the same command with `test` — then the edge was reverted. This is exactly the criterion the `packages/spec/src/contracts/data-engine.test.ts` EXEMPT entry below already states ('it cannot be pinned even in principle ... the import would invert the dependency'); it was simply never applied to the metadata-protocol entries. The entry stays DEBT and not EXEMPT because what cannot exist is the devDependency ROUTE, while the entry itself is closable by sinking the predicate — see `closes`. Whether this file's own fake delete is currently exercised was NOT probed: #4987's file face is this ledger's text only. Per this ledger's own rule that changes nothing, since it would be an argument about this file rather than about the contract.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. Verified available on #4987's branch: @objectstack/objectql and @objectstack/metadata-protocol both already depend on @objectstack/metadata-core (`workspace:*`), whose own `dependencies` are just @objectstack/spec + zod and do NOT include objectql, so the sink adds no new edge; the producer `packages/objectql/src/engine-delete-dispatch.ts` has zero imports, so this is a move and not a refactor. @objectstack/spec/contracts is the other candidate, but only if the predicate belongs to the contract layer — do not pick it by default. The devDependency route is closed by the cycle recorded in `why`, for this file and for every other metadata-protocol entry in this ledger alike." - }, - { - "file": "packages/metadata-protocol/src/protocol-publish-drafts-org-scope.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 119. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.code-only-types.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 88. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/protocol.code-only-types.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 88. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.read-decorations.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 60. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/protocol.read-decorations.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 60. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.runtime-authoring-gate.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4987): the devDependency route this entry used to prescribe DOES NOT EXIST — it is cyclic, not merely unreviewed. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (`workspace:*`), so any reverse edge closes a cycle by construction. Re-measured on #4987's branch rather than cited: the edge was added to metadata-protocol's devDependencies and turbo 2.10.7 refused BOTH task graphs outright — `WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol` / `x Cyclic dependency detected: @objectstack/objectql#build, @objectstack/metadata-protocol#build`, exit 1 from `turbo run build --filter=@objectstack/metadata-protocol --dry` and from the same command with `test` — then the edge was reverted. This is exactly the criterion the `packages/spec/src/contracts/data-engine.test.ts` EXEMPT entry below already states ('it cannot be pinned even in principle ... the import would invert the dependency'); it was simply never applied to the metadata-protocol entries. The entry stays DEBT and not EXEMPT because what cannot exist is the devDependency ROUTE, while the entry itself is closable by sinking the predicate — see `closes`. Whether this file's own fake delete is currently exercised was NOT probed: #4987's file face is this ledger's text only. Per this ledger's own rule that changes nothing, since it would be an argument about this file rather than about the contract.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. Verified available on #4987's branch: @objectstack/objectql and @objectstack/metadata-protocol both already depend on @objectstack/metadata-core (`workspace:*`), whose own `dependencies` are just @objectstack/spec + zod and do NOT include objectql, so the sink adds no new edge; the producer `packages/objectql/src/engine-delete-dispatch.ts` has zero imports, so this is a move and not a refactor. @objectstack/spec/contracts is the other candidate, but only if the predicate belongs to the contract layer — do not pick it by default. The devDependency route is closed by the cycle recorded in `why`, for this file and for every other metadata-protocol entry in this ledger alike." - }, - { - "file": "packages/metadata-protocol/src/protocol.runtime-authoring-gate.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 86. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.save-flow-canonicalization.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4987): the devDependency route this entry used to prescribe DOES NOT EXIST — it is cyclic, not merely unreviewed. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (`workspace:*`), so any reverse edge closes a cycle by construction. Re-measured on #4987's branch rather than cited: the edge was added to metadata-protocol's devDependencies and turbo 2.10.7 refused BOTH task graphs outright — `WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol` / `x Cyclic dependency detected: @objectstack/objectql#build, @objectstack/metadata-protocol#build`, exit 1 from `turbo run build --filter=@objectstack/metadata-protocol --dry` and from the same command with `test` — then the edge was reverted. This is exactly the criterion the `packages/spec/src/contracts/data-engine.test.ts` EXEMPT entry below already states ('it cannot be pinned even in principle ... the import would invert the dependency'); it was simply never applied to the metadata-protocol entries. The entry stays DEBT and not EXEMPT because what cannot exist is the devDependency ROUTE, while the entry itself is closable by sinking the predicate — see `closes`. Whether this file's own fake delete is currently exercised was NOT probed: #4987's file face is this ledger's text only. Per this ledger's own rule that changes nothing, since it would be an argument about this file rather than about the contract.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. Verified available on #4987's branch: @objectstack/objectql and @objectstack/metadata-protocol both already depend on @objectstack/metadata-core (`workspace:*`), whose own `dependencies` are just @objectstack/spec + zod and do NOT include objectql, so the sink adds no new edge; the producer `packages/objectql/src/engine-delete-dispatch.ts` has zero imports, so this is a move and not a refactor. @objectstack/spec/contracts is the other candidate, but only if the predicate belongs to the contract layer — do not pick it by default. The devDependency route is closed by the cycle recorded in `why`, for this file and for every other metadata-protocol entry in this ledger alike." - }, - { - "file": "packages/metadata-protocol/src/protocol.save-flow-canonicalization.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 89. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.save-union-issues.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 45. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/protocol.save-union-issues.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 45. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.stored-conversions.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 51. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/protocol.stored-conversions.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 51. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/protocol.stored-migration.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 73. 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, both `dependencies`), so any reverse edge closes a cycle by construction. Measured twice already, and this entry does not re-measure: the #4987 and #5206 entries in this ledger added the edge to @objectstack/metadata-protocol's devDependencies and recorded turbo's outright refusal. This package is also named in the cycle turbo printed on THIS branch when the same edge was added to @objectstack/core.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." - }, - { - "file": "packages/metadata-protocol/src/protocol.stored-migration.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 73. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.draft-drain.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4981): identical to the sibling `sys-metadata-repository.history-counters.test.ts` entry below, and closed by the same route. The devDependency this ledger's other metadata-protocol entries prescribe is CYCLIC, not merely unreviewed: @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (re-verified statically on this branch), so adding objectql to metadata-protocol's devDependencies makes turbo refuse the graph outright — `Cyclic dependency detected: @objectstack/metadata-protocol#build, @objectstack/objectql#build`, measured in #4867 by adding the edge and reverting it, and deliberately NOT re-run here. The fake's delete is exercised only by the #4981 drain path and is a by-id delete routed through SysMetadataRepository.delete, but that is an argument about this file, not about the contract, so the entry stays DEBT rather than EXEMPT per this ledger's own rule.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on (@objectstack/metadata-core is the common dep; @objectstack/spec/contracts is the other candidate) — tracked as #4987 — then open the fake's delete with it; the devDependency route is closed by the cycle above, for this file and for the five sibling metadata-protocol entries alike" - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.draft-drain.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 77. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.history-counters.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4867): the devDependency this ledger's sibling entries prescribe is not available here — it is CYCLIC, not merely unreviewed. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies`, so adding objectql to metadata-protocol's devDependencies makes turbo refuse the graph outright: `Cyclic dependency detected: @objectstack/metadata-protocol#build, @objectstack/objectql#build` (turbo 2.10.7, `turbo run test --filter=@objectstack/metadata-protocol --dry`, measured by adding the edge and reverting it). The fake's delete is exercised by one test (the #4867 delete path) and is a by-id delete routed through SysMetadataRepository.delete, but that is an argument about this file, not about the contract, so the entry stays DEBT rather than EXEMPT per this ledger's own rule.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on (@objectstack/metadata-core is the common dep; @objectstack/spec/contracts is the other candidate), then open the fake's delete with it — the devDependency route is closed by the cycle above, for this file and for the four sibling metadata-protocol entries alike" - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.history-counters.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 91. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.recorded-by.test.ts", - "verb": "delete", - "unguarded": 1, - "kind": "DEBT", - "why": "MEASURED (#4987): the devDependency route this entry used to prescribe DOES NOT EXIST — it is cyclic, not merely unreviewed. @objectstack/objectql depends on @objectstack/metadata-protocol in `dependencies` (`workspace:*`), so any reverse edge closes a cycle by construction. Re-measured on #4987's branch rather than cited: the edge was added to metadata-protocol's devDependencies and turbo 2.10.7 refused BOTH task graphs outright — `WARNING Circular package dependency detected: @objectstack/objectql, @objectstack/metadata-protocol` / `x Cyclic dependency detected: @objectstack/objectql#build, @objectstack/metadata-protocol#build`, exit 1 from `turbo run build --filter=@objectstack/metadata-protocol --dry` and from the same command with `test` — then the edge was reverted. This is exactly the criterion the `packages/spec/src/contracts/data-engine.test.ts` EXEMPT entry below already states ('it cannot be pinned even in principle ... the import would invert the dependency'); it was simply never applied to the metadata-protocol entries. The entry stays DEBT and not EXEMPT because what cannot exist is the devDependency ROUTE, while the entry itself is closable by sinking the predicate — see `closes`. Whether this file's own fake delete is currently exercised was NOT probed: #4987's file face is this ledger's text only. Per this ledger's own rule that changes nothing, since it would be an argument about this file rather than about the contract.", - "closes": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. Verified available on #4987's branch: @objectstack/objectql and @objectstack/metadata-protocol both already depend on @objectstack/metadata-core (`workspace:*`), whose own `dependencies` are just @objectstack/spec + zod and do NOT include objectql, so the sink adds no new edge; the producer `packages/objectql/src/engine-delete-dispatch.ts` has zero imports, so this is a move and not a refactor. @objectstack/spec/contracts is the other candidate, but only if the predicate belongs to the contract layer — do not pick it by default. The devDependency route is closed by the cycle recorded in `why`, for this file and for every other metadata-protocol entry in this ledger alike." - }, - { - "file": "packages/metadata-protocol/src/sys-metadata-repository.recorded-by.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 49. 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-protocol 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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." + "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", @@ -328,7 +120,7 @@ "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": "sink assertEngineDeleteDispatch into a package BOTH sides already depend on — tracked as #5619 — then open the fake's delete with it. The devDependency route is closed by the cycle recorded in `why`, exactly as for the metadata-protocol entries in this ledger." + "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", @@ -336,7 +128,7 @@ "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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." + "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", @@ -360,7 +152,7 @@ "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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." + "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", @@ -368,7 +160,7 @@ "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": "sink assertEngineUpdateDispatch into a package BOTH sides already depend on — the same blocker #5619 tracks for assertEngineDeleteDispatch, and one move serves both slices — then open the fake's update with it. The devDependency route is closed by the cycle recorded in `why`." + "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", @@ -831,7 +623,7 @@ "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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/plugin-webhooks --dry`, no circular-dependency warning), then the edge was reverted.", + "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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/plugin-webhooks --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1151,7 +943,7 @@ "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 33. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 33. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1167,7 +959,7 @@ "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 37. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 37. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1183,7 +975,7 @@ "verb": "delete", "unguarded": 2, "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 lines 23, 105. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 lines 23, 105. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1199,7 +991,7 @@ "verb": "delete", "unguarded": 6, "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 lines 37, 173, 226, 375, 410, 442. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 lines 37, 173, 226, 375, 410, 442. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1215,7 +1007,7 @@ "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 18. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 18. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1231,7 +1023,7 @@ "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 24. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 24. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1247,7 +1039,7 @@ "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 33. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 33. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1263,7 +1055,7 @@ "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 39. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 39. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, { @@ -1279,7 +1071,7 @@ "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 70. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family in this ledger — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", + "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 70. 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 package does not depend on @objectstack/objectql yet, and — unlike the metadata-protocol family this ledger carried until #5619 removed it — the devDependency route is AVAILABLE here rather than cyclic. Measured on this branch, not cited: the edge was added to this package's devDependencies and turbo 2.10.7 accepted the graph (`turbo run build --filter=@objectstack/service-messaging --dry`, no circular-dependency warning), then the edge was reverted.", "closes": "add @objectstack/objectql to this package's devDependencies (verified acyclic on this branch — see `why`), then open the fake's delete with assertEngineDeleteDispatch(options)" }, {