From e4ecbf2490acbd4ee93e97270f1741ab355f431b Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 22:59:16 +0000 Subject: [PATCH 1/8] fix(spec,runtime): `ActionEngineFacade.find` takes the engine's query envelope; the bare-filter shape is withdrawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ctx.engine.find(object, query)` now declares `EngineQueryOptions` by identity — the same options bag `IDataEngine.find` takes — and the runtime's `find` arm passes it through instead of building `{ where: query }` itself. The facade's parameter had been the `where` half alone, which made the engine's own envelope the wrong spelling at the call site: it reached the engine as `{ where: { where: … } }`, matched no row and resolved to `[]` with no error. Closing that at the type level the other way would have had to reserve the field name `where` across every customer's data model. Migration is lossless and mechanical — `find(o, f)` → `find(o, { where: f })` — and registered as an ADR-0087 semantic entry. A bare filter is now a compile error on both paths a caller can reach it by: an object literal fails the excess-property check, and a `FilterCondition` variable fails TS2559. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- ...ction-engine-facade-find-query-envelope.md | 73 ++++++++ content/docs/ui/actions.mdx | 36 ++-- .../app-todo/src/actions/task.handlers.ts | 10 +- ...action-engine-facade-find-envelope.test.ts | 139 ++++++++++++++++ packages/runtime/src/action-execution.ts | 26 ++- ...ction-engine-facade-find-query-envelope.ts | 43 +++++ packages/spec/src/ui/action-params.test.ts | 156 +++++++++++------- packages/spec/src/ui/action-params.zod.ts | 101 ++++++++---- 8 files changed, 474 insertions(+), 110 deletions(-) create mode 100644 .changeset/15124-action-engine-facade-find-query-envelope.md create mode 100644 packages/runtime/src/action-engine-facade-find-envelope.test.ts create mode 100644 packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts diff --git a/.changeset/15124-action-engine-facade-find-query-envelope.md b/.changeset/15124-action-engine-facade-find-query-envelope.md new file mode 100644 index 00000000000..388b5cf9bb2 --- /dev/null +++ b/.changeset/15124-action-engine-facade-find-query-envelope.md @@ -0,0 +1,73 @@ +--- +'@objectstack/spec': minor +'@objectstack/runtime': minor +--- + +**BREAKING for action handlers** — `ActionEngineFacade.find` takes the engine's query ENVELOPE; the bare-filter parameter shape is withdrawn (#15124) + +Clause-②: yes (narrowing) + +`ctx.engine.find(object, query)` now takes `EngineQueryOptions` — the same +options bag `IDataEngine.find` and ObjectQL's own `engine.find` take, named by +identity rather than restated. **One platform, one query shape.** + +### Migration — FROM → TO + +| You wrote | Write instead | +| --- | --- | +| `ctx.engine.find('task', { status: 'open' })` | `ctx.engine.find('task', { where: { status: 'open' } })` | +| `ctx.engine.find('task', { amount: { $gt: 100 } })` | `ctx.engine.find('task', { where: { amount: { $gt: 100 } } })` | +| `ctx.engine.find('task', {})` | unchanged — an empty envelope is still the unfiltered read | + +The rewrite is lossless and mechanical: the filter moves under `where`, verbatim. +`tsc --noEmit` over your handlers finds every unmigrated call — see below. + +### Why the shape was withdrawn rather than the bar closed + +Until now this parameter was the `where` HALF of a query while every other +`find` on the platform took the whole envelope, and the runtime wrapped what it +was given. That made the most natural spelling the wrong one, silently: an +author who passed the engine's own envelope reached the engine as +`{ where: { where: … } }` — a filter on a field named `where` — which matches no +row and resolves to `[]` with **no error at all**. A handler that made the +mistake ran to completion over zero rows for as long as it shipped, and its own +hand-written test double, written to the same belief, passed every assertion. +Because an empty `{}` skipped the wrap, one unfiltered read kept working under +either belief, so a dead handler looked partially alive. + +Refusing `where` at the top level instead — intersecting the old parameter with +`{ where?: never }` — was rejected: it asserts a vocabulary fact the spec +declares nowhere, reserving the field name `where` across every customer's data +model to buy one parameter's compile-time check. Aligning the parameter removes +the ambiguity at its root and reserves nothing. + +### What the new declaration refuses, measured + +A bare filter no longer type-checks on **either** path a caller can reach it by: + +- an object literal (`{ status: 'completed' }`) fails the excess-property check — + a field name is not an envelope key; +- a filter held in a `FilterCondition` variable fails **TS2559** — every envelope + key is optional, so a bag of field names has no property in common with it. + +So the failure is a compile error at the call site, never a runtime surprise. +The envelope's own keys are typed too: `where: 'a = b'`, `fields: 'id,subject'` +and `limit: '50'` are each refused. + +### What this opens + +`fields`, `orderBy`, `limit`, `offset` and `expand` are reachable from an action +handler for the first time — under the old parameter there was nowhere to carry +them. A caller-supplied `context` is **ignored**: this facade is trusted and +context-less by design, and the runtime stamps its own elevated +`ExecutionContext` last. Do not write one — it reads as authorization and is +none. + +### Checking a migrated handler + +Do not settle for "it still resolves". A handler that had been passing the +envelope was returning `[]` on **every** call, so a suite written against the +mistake passes and the row count is the only witness. Re-run each migrated +handler against seeded data and assert it returns the rows its filter selects. + + diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index 3f2021af153..e2d0d352eb1 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -170,19 +170,29 @@ already `completed` — is not stamped, so nothing overwrites the handler's valu and nothing strips it either, and the action silently replaces the real completion timestamp with "now". That is why the snippet sends `status` alone. - -**`ctx.engine.find(object, filter)` takes a filter, not a query.** The second -argument is the `where` half only — `{ status: 'completed' }`, operators -(`{ amount: { $gt: 100 } }`), `$and` / `$or` / `$not` — and the runtime wraps -it in `where` itself. Passing an ObjectQL envelope -(`{ where: { status: 'completed' } }`) raises no error: it becomes -`{ where: { where: … } }`, matches no row, and returns `[]`. An empty filter -(`{}`) is passed through unwrapped, so the one unfiltered read works under -either reading and a handler can look partially alive. The parameter is typed -`FilterCondition` (`ActionEngineFacade` in `@objectstack/spec/ui`), which -refuses a primitive or a mistyped `$and` / `$or` / `$not` but still admits -`where` as a key — the sentence above is the contract, and a hand-written test -double must honour it too. + +**`ctx.engine.find(object, query)` takes the engine's query envelope.** The +second argument is the same options bag `engine.find` takes everywhere else — +the filter goes under `where`, and `fields`, `orderBy`, `limit`, `offset` and +`expand` mean what they mean on the engine. One platform, one query shape. + +```typescript +await ctx.engine.find('todo_task', { where: { status: 'completed' } }); +await ctx.engine.find('todo_task', { where: { amount: { $gt: 100 } }, fields: ['id', 'subject'], limit: 50 }); +await ctx.engine.find('todo_task', {}); // the unfiltered read +``` + +The parameter is typed `EngineQueryOptions` (`ActionEngineFacade` in +`@objectstack/spec/ui`), so a bare filter is a **compile error** at the call +site — `{ status: 'completed' }` has nowhere to land, and so does a filter held +in a `FilterCondition` variable. A hand-written test double must honour the +envelope too. + +**Upgrading?** This parameter used to be the `where` half on its own, and the +runtime wrapped it. `find(o, f)` becomes `find(o, { where: f })`; an unfiltered +`find(o, {})` is unchanged. The old shape had the trap the other way round: +writing the engine's own envelope produced `{ where: { where: … } }`, which +matched no row and returned `[]` with no error at all. diff --git a/examples/app-todo/src/actions/task.handlers.ts b/examples/app-todo/src/actions/task.handlers.ts index e98bcf7e7f5..3792158b350 100644 --- a/examples/app-todo/src/actions/task.handlers.ts +++ b/examples/app-todo/src/actions/task.handlers.ts @@ -38,6 +38,11 @@ import type { ActionHandlerContext } from '@objectstack/spec/ui'; // written against the published type at all. The declaration now says // `string | string[]` (#15117), so the copy is gone and this example // type-checks against exactly the types a real app gets. +// +// And the copy's `query` bag turned out to be the shape the platform kept: +// #15124 withdrew the bare-filter parameter and `ctx.engine.find` now takes the +// ENGINE's query envelope — `find(object, { where: … })`, the same options bag +// `engine.find` takes anywhere else. One platform, one query shape. /** * Mark a single task as complete. @@ -106,7 +111,9 @@ export async function massCompleteTasks(ctx: ActionHandlerContext): Promise { const { engine } = ctx; - const completed = await engine.find('todo_task', { status: 'completed' }); + // [#15124] The filter goes under `where` — the second argument is the + // engine's query envelope, not the `where` half on its own. + const completed = await engine.find('todo_task', { where: { status: 'completed' } }); const ids = completed.map((r) => r.id as string); if (ids.length > 0) { await engine.delete('todo_task', ids); @@ -135,6 +142,7 @@ export async function setReminder(ctx: ActionHandlerContext): Promise { /** Export tasks to CSV format */ export async function exportTasksToCSV(ctx: ActionHandlerContext): Promise { const { engine } = ctx; + // An EMPTY envelope is still the unfiltered read — unchanged by #15124. const tasks = await engine.find('todo_task', {}); const header = 'subject,status,priority,category,due_date'; const rows = tasks.map((t) => diff --git a/packages/runtime/src/action-engine-facade-find-envelope.test.ts b/packages/runtime/src/action-engine-facade-find-envelope.test.ts new file mode 100644 index 00000000000..9821768de34 --- /dev/null +++ b/packages/runtime/src/action-engine-facade-find-envelope.test.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * [#15124] `ActionEngineFacade.find` passes the engine's QUERY ENVELOPE + * through — the double-wrap is gone. + * + * ## What this pins, and why the declaration's own pin is not enough + * + * The spec half (`packages/spec/src/ui/action-params.test.ts`) pins what the + * DECLARATION admits and refuses, in the tsc channel. It cannot pin what the + * runtime does with the value, and the defect this card closes lived exactly + * there: the arm built the envelope itself, so an author who wrote the + * engine's own envelope reached `ql.find` as `{ where: { where: … } }` — a + * filter on a field named `where`, which matches no row and resolves to `[]` + * with no error at all. A type change alone would have left that arm free to + * keep wrapping, and the two halves would have disagreed in silence: exactly + * the shape #14175 found and could not close. + * + * So the assertions here are about the ARGUMENT the engine actually received, + * not about the rows that came back. A pin that only checked "rows came back" + * is what the original defect passed: the reporting app's own hand-written + * double read `query.where` and agreed with the mistake all the way down. + * + * ## The three clauses + * + * 1. **Pass-through, verbatim.** Every envelope key an author writes reaches + * `ql.find` under its own name — `where` as `where`, and `fields`, + * `orderBy`, `limit` beside it. Under the old arm the whole bag landed + * nested under `where` and the projection/paging keys were unreachable from + * a handler at all. + * 2. **No wrap, and no second `where`.** The negative half of clause 1, stated + * separately because it is the one an accidental re-wrap would break while + * clause 1 stayed green. + * 3. **`context` is the facade's.** The envelope carries `context` because + * every engine option bag does, but this facade is trusted and context-less + * by design (#3914, ADR-0096) — the elevated context it builds wins over a + * caller-supplied one. That is a security-shaped property of a spread + * ORDER, which is one edit away from silently inverting. + * + * @see packages/runtime/src/action-execution.ts — `buildActionEngineFacade`. + * @see packages/spec/src/ui/action-params.zod.ts — the member doc of record. + */ + +import { describe, it, expect } from 'vitest'; +import { assertEngineDeleteDispatch } from '@objectstack/metadata-core'; +import { buildActionEngineFacade } from './action-execution.js'; + +const deps: any = { resolveService: () => undefined, getObjectQL: async () => undefined }; + +/** + * An engine double that RECORDS the options bag its `find` was handed. Its + * `delete` is bound to the real engine's dispatch contract through the + * producer's own predicate rather than a mirrored `if` + * (`scripts/check-engine-double-contract.mjs`). + */ +function makeEngine(rows: Array> = []) { + const found: Array<{ object: string; options: Record | undefined }> = []; + const ql: any = { + found, + async insert(_object: string, data: Record) { + return { id: (data as Record)?.id ?? 'rec_new' }; + }, + async find(object: string, options?: Record) { + found.push({ object, options }); + return rows; + }, + async count(_object: string, _options?: Record) { + return rows.length; + }, + async update(_object: string, _data: Record, _options?: Record) { + return { ok: true }; + }, + async delete(object: string, options?: Record) { + assertEngineDeleteDispatch(options); + return { ok: true, object }; + }, + }; + return ql; +} + +describe('#15124 — ActionEngineFacade.find passes the engine query envelope through', () => { + it('hands every envelope key to the engine under its own name', async () => { + const ql = makeEngine([{ id: 'tsk_1' }]); + const engine = buildActionEngineFacade(deps, ql, { userId: 'u1' }); + + await engine.find('todo_task', { + where: { status: 'completed' }, + fields: ['id', 'subject'], + orderBy: [{ field: 'due_date', order: 'asc' }], + limit: 50, + }); + + const [call] = ql.found; + expect(call.object).toBe('todo_task'); + expect(call.options.where).toEqual({ status: 'completed' }); + expect(call.options.fields).toEqual(['id', 'subject']); + expect(call.options.orderBy).toEqual([{ field: 'due_date', order: 'asc' }]); + expect(call.options.limit).toBe(50); + }); + + it('does NOT wrap — the filter the author wrote under `where` stays one level deep', async () => { + const ql = makeEngine(); + const engine = buildActionEngineFacade(deps, ql, { userId: 'u1' }); + + await engine.find('todo_task', { where: { status: 'completed' } }); + + const where = ql.found[0].options.where as Record; + // The defect this card closes, stated as an assertion: the old arm + // produced `{ where: { where: { status: … } } }`, which matched no row. + expect(where).not.toHaveProperty('where'); + expect(where).toEqual({ status: 'completed' }); + }); + + it('the unfiltered read stays unfiltered — `{}` carries no `where` at all', async () => { + const ql = makeEngine(); + const engine = buildActionEngineFacade(deps, ql, { userId: 'u1' }); + + await engine.find('todo_task', {}); + + expect(ql.found[0].options).not.toHaveProperty('where'); + }); + + it('stamps the facade\'s OWN elevated context, and a caller-supplied `context` does not displace it', async () => { + const ql = makeEngine(); + const engine = buildActionEngineFacade(deps, ql, { userId: 'u1', tenantId: 'org_acme' }); + + await engine.find('todo_task', { + where: { status: 'open' }, + context: { userId: 'someone_else', isSystem: false }, + } as never); + + const context = ql.found[0].options.context as Record; + // The facade is trusted and context-less by design: what a caller put + // in the envelope reads as authorization and is none. + expect(context).toBeDefined(); + expect(context.userId).not.toBe('someone_else'); + expect(context.isSystem).toBe(true); + }); +}); diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index c9b6c637e77..77a961f4c79 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -1505,9 +1505,29 @@ export function buildActionEngineFacade(_deps: ActionExecutionDeps, ql: any, ec? await ql.delete(object, { where: { id }, context }); } }, - async find(object: string, query: Record): Promise>> { - const where = query && Object.keys(query).length ? { where: query } : {}; - const rows = await ql.find(object, { ...where, context } as any); + // [#15124] The ENVELOPE goes through — this arm no longer builds one. + // + // It used to take the `where` half alone and wrap it + // (`{ where: query }`, with an empty bag passed through unwrapped), so + // the facade's parameter shape differed from the engine's for no reason + // a caller could see. The cost was silent: an author who wrote the + // engine's own envelope got `{ where: { where: … } }`, which matches no + // row and resolves to `[]` with no error. The director seat withdrew + // that parameter shape rather than reserving the field name `where` + // across every customer's data model to refuse it — one platform, one + // query shape. The spec member (`ActionEngineFacade.find`, + // `packages/spec/src/ui/action-params.zod.ts`) now declares + // `EngineQueryOptions` by identity, so the handler writes what the + // engine reads and this arm only adds the identity. + // + // `context` is spread LAST on purpose: the facade is trusted and + // context-less by design (#3914, ADR-0096), so the elevated context it + // built wins over any `context` a caller put in the envelope. The + // envelope admits the key because every engine option bag does; it is + // not an authorization the caller gets to choose. Pinned in + // `action-engine-facade-find-envelope.test.ts`. + async find(object: string, query?: Record): Promise>> { + const rows = await ql.find(object, { ...(query ?? {}), context } as any); return Array.isArray(rows) ? rows : ((rows as any)?.value ?? []); }, }; diff --git a/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts b/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts new file mode 100644 index 00000000000..c27f719cdd7 --- /dev/null +++ b/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts @@ -0,0 +1,43 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import type { SemanticMigration } from '../../types.js'; + +// The action facade's `find` took the `where` HALF of a query while every other +// `find` on the platform took the whole envelope. The rewrite is mechanical and +// lossless, but it lives in an action HANDLER's source — a TypeScript function +// body, not a keyed metadata document — so `objectstack migrate meta` cannot +// reach it and it is a semantic entry rather than a D2 conversion. +export const entry: SemanticMigration = { + id: 'action-engine-facade-find-query-envelope', + surface: 'Action handler body — `ctx.engine.find(object, filter)` ' + + '(`ActionEngineFacade.find`, `@objectstack/spec/ui`)', + replacement: '`ctx.engine.find(object, { where: filter })` — the engine\'s own query envelope ' + + '(`EngineQueryOptions`), the same options bag `IDataEngine.find` takes. The filter moves under ' + + '`where` verbatim: `find(\'task\', { status: \'open\' })` → ' + + '`find(\'task\', { where: { status: \'open\' } })`. An unfiltered `find(object, {})` is unchanged, ' + + 'and the rest of the envelope — `fields`, `orderBy`, `limit`, `offset`, `expand` — becomes ' + + 'reachable from a handler for the first time. A caller-supplied `context` is ignored: the ' + + 'facade is trusted and stamps its own elevated one.', + reason: + 'The rewrite itself is lossless and mechanical, but it is not automatable here: an action handler ' + + 'is authored TypeScript, and the chain rewrites stored metadata by key, so no `os migrate meta` ' + + 'step can reach a call expression inside a function body. The change is a WITHDRAWAL of the ' + + 'parameter shape #14175 chose, ruled by the director seat (decision batch #123 item 3, ' + + '2026-09-12, 「同意」) on the long-term axis 「one platform, one query shape」. The facade had been ' + + 'given a shape different from the engine\'s — the `where` half alone — which made the most ' + + 'natural spelling the wrong one: an author who passed the engine\'s envelope got ' + + '`{ where: { where: … } }`, matching no row and resolving to `[]` with no error, while an ' + + 'unfiltered `{}` kept working under either belief so a dead handler looked partially alive. The ' + + 'alternative — refusing `where` at the top level with an intersection — was rejected because it ' + + 'asserts a vocabulary fact the spec declares nowhere, reserving the field name `where` across ' + + 'every customer\'s data model to buy one parameter\'s compile-time check.', + acceptanceCriteria: + 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope, and the package ' + + 'type-checks: a bare filter is now a compile error at the call site — an object literal fails ' + + 'the excess-property check and a `FilterCondition` variable fails TS2559 — so `tsc --noEmit` ' + + 'over the handlers finds every unmigrated call, with no runtime run needed. Then confirm the ' + + 'reads that were already SILENTLY EMPTY: any handler that had been passing the envelope was ' + + 'resolving to `[]` on every call, so a suite written against the mistake passed and the row ' + + 'count is the only witness — re-run each migrated handler against seeded data and assert it now ' + + 'returns the rows its filter selects, rather than asserting it still resolves.', +}; diff --git a/packages/spec/src/ui/action-params.test.ts b/packages/spec/src/ui/action-params.test.ts index 8181e4c704b..402b40ce9c3 100644 --- a/packages/spec/src/ui/action-params.test.ts +++ b/packages/spec/src/ui/action-params.test.ts @@ -10,6 +10,7 @@ import { type ResolvedActionParam, } from './action-params.zod'; import type { FilterCondition } from '../data/filter.zod'; +import type { EngineQueryOptions } from '../data/data-engine.zod'; import { MIGRATIONS_BY_MAJOR } from '../migrations/registry'; const codes = (issues: ReturnType) => issues.map((i) => i.code).sort(); @@ -396,76 +397,113 @@ describe('#5779 — ActionSession `positions` canonical + `roles` deprecated ali }); // --------------------------------------------------------------------------- -// #14175 — `ActionEngineFacade.find` takes a FILTER, never an ObjectQL envelope +// #15124 — `ActionEngineFacade.find` takes the engine's QUERY ENVELOPE +// (#14175's bare-filter parameter shape is withdrawn) // --------------------------------------------------------------------------- type Eq< A, B > = (< T >() => T extends A ? 1 : 2) extends (< T >() => T extends B ? 1 : 2) ? true : false; type Assert< T extends true > = T; // The declared slot, read off the interface — not a retyped copy of it, so a -// re-widening back to an open record, or a rename of the type behind it, fails -// HERE rather than in the first consumer to notice. -type FindFilter = Parameters[1]; +// re-widening back to an open record, a re-narrowing back to the bare filter, +// or a rename of the type behind it, fails HERE rather than in the first +// consumer to notice. +type FindQuery = Parameters[1]; // The type-level pin (the tsc channel, `tsc -p tsconfig.test.json`). `Eq` is -// the strict mutual-assignability test, so `Record` — the type -// this slot carried before, and the one it must not drift back to — does not -// satisfy it (measured: the same `Assert` against `Record` is -// a TS2344). Exported, as the sibling pins are, so `noUnusedLocals` does not -// read a type that exists only to be checked as one that is never used. -export type FindFilterIsFilterCondition = Assert< Eq< FindFilter, FilterCondition > >; - -describe('#14175 — ActionEngineFacade.find takes a FILTER (the `where` half), never an ObjectQL envelope', () => { - it('types the second parameter as the published `FilterCondition` (the tsc channel)', () => { - // The value-level half of `FindFilterIsFilterCondition` above: a literal +// the strict mutual-assignability test, so neither `Record` +// nor `FilterCondition` — the type this slot carried between #14175 and +// #15124, and the one it must not drift back to — satisfies it. Reading the +// engine's published type BY IDENTITY is the whole point of the ruling ("one +// platform, one query shape"): a structural copy of the envelope would pass a +// weaker pin and then drift the moment the engine's own options grow a key. +// Exported, as the sibling pins are, so `noUnusedLocals` does not read a type +// that exists only to be checked as one that is never used. +export type FindQueryIsEngineQueryOptions = Assert< Eq< FindQuery, EngineQueryOptions > >; + +describe('#15124 — ActionEngineFacade.find takes the engine query envelope, never a bare filter', () => { + it('types the second parameter as the published `EngineQueryOptions` (the tsc channel)', () => { + // The value-level half of `FindQueryIsEngineQueryOptions` above: a literal // annotated with the slot type, so the runtime run exercises the same // declaration the type pin reads. - const filter: FindFilter = { position_code: 'qa_lead', active: true }; - expect(Object.keys(filter)).toEqual(['position_code', 'active']); - }); - - it('positive control — every shape a handler legitimately passes compiles, the empty filter included', () => { - const implicitEquality: FindFilter = { status: 'completed' }; - const explicitOperator: FindFilter = { position_code: { $in: ['qa_lead', 'qa_manager'] }, active: true }; - const logical: FindFilter = { - $and: [{ active: true }, { $or: [{ tier: 'a' }, { tier: 'b' }] }], - $not: { archived: true }, + const query: FindQuery = { where: { position_code: 'qa_lead', active: true } }; + expect(Object.keys(query)).toEqual(['where']); + }); + + it('positive control — the envelope spellings a handler passes compile, the unfiltered read included', () => { + const implicitEquality: FindQuery = { where: { status: 'completed' } }; + const explicitOperator: FindQuery = { where: { position_code: { $in: ['qa_lead', 'qa_manager'] }, active: true } }; + const logical: FindQuery = { + where: { + $and: [{ active: true }, { $or: [{ tier: 'a' }, { tier: 'b' }] }], + $not: { archived: true }, + }, }; - // The runtime passes THIS one through unwrapped — the unfiltered read, and - // the one call that kept working in the reporting app under either belief. - const unfiltered: FindFilter = {}; - - expect([implicitEquality, explicitOperator, logical, unfiltered].every((f) => typeof f === 'object')).toBe(true); - }); - - it('refuses at compile time what `FilterCondition` refuses — a primitive and a mistyped logical operator', () => { - // Each `@ts-expect-error` is itself checked: if the type ever ADMITS one of - // these, the directive goes unused and `tsc -p tsconfig.test.json` reds. - // @ts-expect-error — a filter is an object; a bare string is not a `where` half. - const primitive: FindFilter = 'position_code = qa_lead'; - // @ts-expect-error — `$and` is `FilterCondition[]`; a string is refused. - const andNotArray: FindFilter = { $and: 'active' }; - // @ts-expect-error — `$or` is `FilterCondition[]`; a bare object is refused. - const orNotArray: FindFilter = { $or: { active: true } }; - // @ts-expect-error — `$not` is a `FilterCondition`; a string is refused. - const notNotFilter: FindFilter = { $not: 'archived' }; - - expect([primitive, andNotArray, orNotArray, notNotFilter]).toHaveLength(4); - }); - - it('MEASURED GAP — the exact envelope mistake still compiles; the doc comment, not the type, is the contract', () => { - // `FilterCondition`'s string index signature is what lets a field NAME be a - // key, and `where` is a string — so the shape that returned `[]` in silence - // in the reporting app (`{ where: { position_code: 'qa_lead' } }`) is - // admitted by the type, one level down too. This pin RECORDS that - // measurement rather than hiding it: a later narrowing that refuses `where` - // at the top level turns it red on purpose, so the member's "does NOT - // refuse `{ where: … }`" sentence is updated with the type instead of - // drifting from it. - const envelope: FindFilter = { where: { position_code: 'qa_lead' } }; - const nested: FindFilter = { where: { where: { position_code: 'qa_lead' } } }; - - expect('where' in envelope && 'where' in nested).toBe(true); + // The rest of the envelope is reachable from a handler for the first time: + // under the bare-filter shape a handler could not project, sort or page at + // all, because the parameter had nowhere to carry those keys. + const projected: FindQuery = { where: { status: 'completed' }, fields: ['id', 'subject'], limit: 50 }; + const sorted: FindQuery = { orderBy: [{ field: 'due_date', order: 'asc' }], offset: 20 }; + // The unfiltered read — `{}` was the one call that worked under EITHER + // reading before this card, and it still means "every row". + const unfiltered: FindQuery = {}; + + expect([implicitEquality, explicitOperator, logical, projected, sorted, unfiltered] + .every((q) => typeof q === 'object')).toBe(true); + }); + + it('REFUSAL PIN — the bare filter #14175 declared no longer type-checks (the trap is inverted, not narrowed)', () => { + // This is the pin #14175 recorded as a MEASURED GAP, flipped. The envelope + // that returned `[]` in silence in the reporting app is now the RIGHT + // spelling (the positive controls above), and the bare filter that used to + // be right is the one tsc refuses. Each `@ts-expect-error` is itself + // checked: if the slot ever re-admits one of these, the directive goes + // unused and `tsc -p tsconfig.test.json` reds. + // + // The refusal is the object-literal excess-property check, which is what + // makes it LOUD at the call site an author actually writes: a field name is + // not an envelope key, so `{ status: … }` has nowhere to land. + // @ts-expect-error — `status` is a field name, not an envelope key; write `{ where: { status } }`. + const bareImplicitEquality: FindQuery = { status: 'completed' }; + // @ts-expect-error — the same for an explicit operator: it belongs under `where`. + const bareExplicitOperator: FindQuery = { position_code: { $in: ['qa_lead'] } }; + // @ts-expect-error — `$and` is a FILTER operator; at envelope level it is an unknown key. + const bareLogical: FindQuery = { $and: [{ active: true }] }; + // @ts-expect-error — an envelope is an object; a bare string is not one. + const primitive: FindQuery = 'position_code = qa_lead'; + + expect([bareImplicitEquality, bareExplicitOperator, bareLogical, primitive]).toHaveLength(4); + }); + + it('refuses a mistyped envelope key — the keys are the engine\'s, and they are typed', () => { + // @ts-expect-error — `where` is a `FilterCondition`; a bare string is refused. + const whereNotFilter: FindQuery = { where: 'status = completed' }; + // @ts-expect-error — `fields` is an array of field nodes; a comma string is refused. + const fieldsNotArray: FindQuery = { fields: 'id,subject' }; + // @ts-expect-error — `limit` is a number. + const limitNotNumber: FindQuery = { limit: '50' }; + + expect([whereNotFilter, fieldsNotArray, limitNotNumber]).toHaveLength(3); + }); + + it('the refusal survives the VARIABLE path too — not just the object-literal check', () => { + // The obvious worry about narrowing an all-optional target is that only + // FRESH object literals get the excess-property check, so a filter reaching + // the call through a variable would slide in structurally and fail at + // runtime instead. MEASURED: it does not. `EngineQueryOptions` is a weak + // type (every key optional), and a `FilterCondition` holding field names + // has no property in common with it, so tsc answers TS2559 — "has no + // properties in common with" — on the assignment itself. `FilterCondition`'s + // string index signature does not rescue it. + // + // Pinned because it is the half a reader assumes is open: the member doc + // says the old spelling fails at COMPILE time, and this is the leg of that + // claim the literal pin above does not cover. + const held: FilterCondition = { status: 'completed' }; + // @ts-expect-error — TS2559: a bare filter has no property in common with the envelope. + const viaVariable: FindQuery = held; + + expect(Object.keys(viaVariable)).toEqual(['status']); }); }); diff --git a/packages/spec/src/ui/action-params.zod.ts b/packages/spec/src/ui/action-params.zod.ts index fccd803f357..a7c30826974 100644 --- a/packages/spec/src/ui/action-params.zod.ts +++ b/packages/spec/src/ui/action-params.zod.ts @@ -29,7 +29,7 @@ import { z } from 'zod'; import { valueSchemaFor } from '../data/field-value.zod'; -import type { FilterCondition } from '../data/filter.zod'; +import type { EngineQueryOptions } from '../data/data-engine.zod'; import type { FieldErrorCode } from '../api/errors.zod'; import { lazySchema } from '../shared/lazy-schema'; @@ -232,11 +232,12 @@ export function validateActionParams( * at invoke time (`ai.exposed` + the ADR-0066 D4 capability gate), not here. * * Two members carry an argument contract the signature alone does not settle, - * and both state it on the member: `find` takes a bare FILTER — the `where` - * half of a query — and never an ObjectQL query envelope (#14175); `delete` - * accepts a single id OR an array of them, both as declared contract, served - * one row at a time (#15117). Read those doc comments before writing a handler - * or a test double against either. + * and both state it on the member: `find` takes the engine's own query + * ENVELOPE — {@link EngineQueryOptions}, by identity, the same type + * `IDataEngine.find` takes — and the bare-filter parameter shape #14175 chose + * is withdrawn (#15124); `delete` accepts a single id OR an array of them, + * both as declared contract, served one row at a time (#15117). Read those doc + * comments before writing a handler or a test double against either. */ export interface ActionEngineFacade { insert(object: string, data: Record): Promise<{ id: string }>; @@ -275,39 +276,71 @@ export interface ActionEngineFacade { */ delete(object: string, idOrIds: string | string[]): Promise; /** - * Read the rows of `object` that match `filter`. + * Read the rows of `object` that `query` selects. * - * `filter` is a FILTER — the `where` HALF of an ObjectQL query, the same - * {@link FilterCondition} that `QueryAST.where` carries: implicit equality - * `{ field: value }`, explicit operators `{ field: { $in: [...] } }`, - * `$and` / `$or` / `$not`. It is NOT the query ENVELOPE - * (`{ where, fields, orderBy, limit }`) that `DataEngine.find` and ObjectQL's - * own `engine.find` take — the shape this parameter's former name, `query`, - * invited. The runtime builds the envelope itself: `buildActionEngineFacade`'s - * `find` arm (`packages/runtime/src/action-execution.ts`, `:1183` on - * `369da918`) wraps a non-empty filter as `{ where: filter }` and passes an - * EMPTY filter (`{}`) through unwrapped — the unfiltered read. + * ## One platform, one query shape * - * Two consequences, both silent (#14175): + * `query` is the ENGINE's query envelope — {@link EngineQueryOptions}, the + * very type `IDataEngine.find` and ObjectQL's own `engine.find` take, named + * here by identity rather than restated. The filter goes under `where`, and + * the rest of the envelope (`fields`, `orderBy`, `limit`, `offset`, + * `expand`, `search`, …) means exactly what it means on the engine: * - * - An envelope passed here becomes `{ where: { where: … } }`. No object has - * a field named `where`, so the read matches nothing and resolves to `[]` - * with no error. A handler that made this mistake ran to completion over - * zero rows for as long as it shipped, and its own hand-written test - * double — written to the same belief, reading `query.where` — passed - * every assertion. - * - Because `{}` skips the wrap, an unfiltered call works under EITHER - * reading, so a handler mixing one unfiltered read with envelope-shaped - * ones looks partially alive rather than uniformly dead. + * ```ts + * await ctx.engine.find('todo_task', { where: { status: 'completed' } }); + * await ctx.engine.find('todo_task', { where: { status: 'open' }, fields: ['id', 'subject'], limit: 50 }); + * await ctx.engine.find('todo_task', {}); // the unfiltered read + * ``` * - * What the type buys, exactly: `FilterCondition` refuses a primitive and a - * mistyped logical operator (`$and` / `$or` not arrays, `$not` not a - * filter). It does NOT refuse `{ where: … }` — its string index signature is - * what lets any field name stand as a key, and `where` is a string — so the - * envelope mistake still compiles, and this doc comment, not the type, is - * the contract of record. Both halves are pinned in `action-params.test.ts`. + * ## What changed, and why it is a WITHDRAWAL rather than a narrowing + * + * Until #15124 this slot took a bare `FilterCondition` — the `where` + * half alone — and the runtime wrapped it. That parameter shape differed + * from the engine's for no reason a caller could see, and the cost was + * silent: an author who wrote the engine's own envelope got + * `{ where: { where: … } }`, which matches no row (no object has a field + * named `where`) and resolves to `[]` with no error. A handler that made + * that mistake ran to completion over zero rows for as long as it shipped, + * and its hand-written test double — written to the same belief — passed + * every assertion. `{}` skipped the wrap, so one unfiltered read kept + * working under either belief and a dead handler looked partially alive. + * + * #14175 declared the bare filter and pinned the gap; #15124 withdraws the + * shape instead. Closing the gap the other way would have had to assert a + * vocabulary fact the spec declares nowhere — that no object may carry a + * field named `where` — and that is a word taken from every customer's data + * model to buy one parameter's compile-time check. Aligning the parameter + * removes the ambiguity at its root: the most natural spelling is now the + * correct one, and nothing is reserved. + * + * Migration is lossless and mechanical: `find(o, f)` → `find(o, { where: f })` + * (ADR-0087 semantic migration `action-engine-facade-find-query-envelope`). + * An unfiltered `find(o, {})` is unchanged. + * + * ## What the type refuses, measured + * + * A bare filter no longer type-checks, on BOTH paths a caller can reach it + * by: an object literal (`{ status: 'completed' }`) fails the excess-property + * check, because a field name is not an envelope key; and a filter held in a + * variable typed `FilterCondition` fails TS2559 — `EngineQueryOptions` is a + * weak type, every key optional, and a filter of field names has no property + * in common with it. The refusal is a compile error at the call site, never a + * runtime surprise. The envelope's own keys are typed, so `where: 'a = b'`, + * `fields: 'id,subject'` and `limit: '50'` are refused too. + * + * ## `context` is the caller's to pass and NOT the caller's to choose + * + * The envelope carries `context` because every engine option bag does. This + * facade is TRUSTED and context-less by design (#2849, ADR-0096): the + * runtime stamps its own elevated `ExecutionContext` last, so a + * caller-supplied `context` is overridden rather than honoured. Do not write + * one — it reads as authorization and is none. + * + * Every clause above is pinned in `action-params.test.ts`, and the + * pass-through is pinned against the runtime in + * `packages/runtime/src/action-engine-facade-find-envelope.test.ts`. */ - find(object: string, filter: FilterCondition): Promise>>; + find(object: string, query: EngineQueryOptions): Promise>>; } /** From b9a09a242b3b17952a25fa68dc35946565aa9e61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 19 Sep 2026 23:13:59 +0000 Subject: [PATCH 2/8] chore(spec): regenerate the artifacts the facade signature change moves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `gen:migration-registry` folds in the new ADR-0087 semantic entry; `gen:skill-refs` and `gen:api-surface-declarations` follow the module graph, which moved when `ui/action-params.zod.ts` began importing `data/data-engine.zod`. The seven declaration files beyond `ui.txt` carry ORDER churn only — d.ts emit order follows the chunking, and the chunking follows that import. Measured against a pristine base worktree at the branch point: the same `build && gen:api-surface-declarations` there rewrites nothing at all, so every byte here is downstream of this diff rather than pre-existing drift. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .../api-surface-declarations/automation.txt | 18 +- .../spec/api-surface-declarations/data.txt | 230 +++---- .../api-surface-declarations/integration.txt | 6 +- .../spec/api-surface-declarations/kernel.txt | 38 +- .../spec/api-surface-declarations/root.txt | 8 +- .../api-surface-declarations/security.txt | 46 +- .../spec/api-surface-declarations/system.txt | 588 +++++++++--------- packages/spec/api-surface-declarations/ui.txt | 162 +++-- packages/spec/src/migrations/registry.ts | 39 ++ skills/objectstack-data/references/_index.md | 3 + skills/objectstack-ui/references/_index.md | 3 + 11 files changed, 609 insertions(+), 532 deletions(-) diff --git a/packages/spec/api-surface-declarations/automation.txt b/packages/spec/api-surface-declarations/automation.txt index dcb0b9bfb53..31cf33027d7 100644 --- a/packages/spec/api-surface-declarations/automation.txt +++ b/packages/spec/api-surface-declarations/automation.txt @@ -254,17 +254,17 @@ declare const ApprovalNodeApproverSchema: z.ZodObject<{ user: "user"; role: "role"; queue: "queue"; + team: "team"; manager: "manager"; department: "department"; - team: "team"; org_membership_level: "org_membership_level"; }>; value: z.ZodOptional; resolveAs: z.ZodOptional>; group: z.ZodOptional; organization: z.ZodOptional; @@ -286,17 +286,17 @@ declare const ApprovalNodeConfigSchema: z.ZodObject<{ user: "user"; role: "role"; queue: "queue"; + team: "team"; manager: "manager"; department: "department"; - team: "team"; org_membership_level: "org_membership_level"; }>; value: z.ZodOptional; resolveAs: z.ZodOptional>; group: z.ZodOptional; organization: z.ZodOptional; @@ -324,17 +324,17 @@ declare const ApprovalNodeConfigSchema: z.ZodObject<{ user: "user"; role: "role"; queue: "queue"; + team: "team"; manager: "manager"; department: "department"; - team: "team"; org_membership_level: "org_membership_level"; }>; value: z.ZodOptional; resolveAs: z.ZodOptional>; group: z.ZodOptional; organization: z.ZodOptional; @@ -346,8 +346,8 @@ declare const ApprovalNodeConfigSchema: z.ZodObject<{ position: "position"; text: "text"; user: "user"; - department: "department"; team: "team"; + department: "department"; }>>; multiple: z.ZodOptional; required: z.ZodOptional; @@ -378,9 +378,9 @@ declare const ApproverType: z.ZodEnum<{ user: "user"; role: "role"; queue: "queue"; + team: "team"; manager: "manager"; department: "department"; - team: "team"; org_membership_level: "org_membership_level"; }>; @@ -723,8 +723,8 @@ declare const DecisionOutputDefSchema: z.ZodObject<{ position: "position"; text: "text"; user: "user"; - department: "department"; team: "team"; + department: "department"; }>>; multiple: z.ZodOptional; required: z.ZodOptional; diff --git a/packages/spec/api-surface-declarations/data.txt b/packages/spec/api-surface-declarations/data.txt index 737fc93857b..37b5637becd 100644 --- a/packages/spec/api-surface-declarations/data.txt +++ b/packages/spec/api-surface-declarations/data.txt @@ -442,8 +442,8 @@ declare const ApiOperationSchema: z.ZodEnum<{ create: "create"; restore: "restore"; purge: "purge"; - import: "import"; export: "export"; + import: "import"; }>; // ── ApiPrimitive (type) ── @@ -521,9 +521,9 @@ declare const BaseEngineOptionsSchema: z.ZodObject<{ principalKind: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; audience: z.ZodOptional>; }, z.core.$strip>>>; performedBy: z.ZodOptional>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; @@ -659,8 +659,8 @@ declare const DataSyncConfigSchema: z.ZodObject<{ append_only: "append_only"; }>>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; @@ -792,8 +792,8 @@ declare const DeclarativeConnectorEntrySchema: z.ZodObject<{ append_only: "append_only"; }>>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; diff --git a/packages/spec/api-surface-declarations/kernel.txt b/packages/spec/api-surface-declarations/kernel.txt index 3eec7f91bcf..68c037bc09c 100644 --- a/packages/spec/api-surface-declarations/kernel.txt +++ b/packages/spec/api-surface-declarations/kernel.txt @@ -1414,9 +1414,9 @@ declare const ExecutionContextSchema: z.ZodObject<{ principalKind: z.ZodOptional>; audience: z.ZodOptional>; }, z.core.$strip>>; performedBy: z.ZodOptional; declare const PermissionActionSchema: z.ZodEnum<{ delete: "delete"; update: "update"; - admin: "admin"; - execute: "execute"; - create: "create"; read: "read"; - import: "import"; + create: "create"; export: "export"; + execute: "execute"; + admin: "admin"; + import: "import"; manage: "manage"; configure: "configure"; share: "share"; @@ -5462,12 +5462,12 @@ declare const PluginPermissionSchema: z.ZodObject<{ actions: z.ZodArray>; sortOrder: z.ZodOptional>>; page: z.ZodOptional>; limit: z.ZodOptional>; @@ -6043,12 +6043,12 @@ declare const PluginSecurityManifestSchema: z.ZodObject<{ actions: z.ZodArray; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -22460,8 +22460,8 @@ declare const ObjectStackDefinitionSchema: z.ZodObject<{ append_only: "append_only"; }>>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; @@ -32941,7 +32941,7 @@ declare const ObjectStackSchema: z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -44313,8 +44313,8 @@ declare const ObjectStackSchema: z.ZodObject<{ append_only: "append_only"; }>>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; diff --git a/packages/spec/api-surface-declarations/security.txt b/packages/spec/api-surface-declarations/security.txt index 65897f150af..1351e0c27a8 100644 --- a/packages/spec/api-surface-declarations/security.txt +++ b/packages/spec/api-surface-declarations/security.txt @@ -236,8 +236,8 @@ declare const EffectiveObjectPermissionSchema: z.ZodType create: "create"; restore: "restore"; purge: "purge"; - import: "import"; export: "export"; + import: "import"; }>>>; }; }; @@ -257,10 +257,10 @@ declare const ExplainDecisionSchema: z.ZodObject<{ update: "update"; read: "read"; create: "create"; + transfer: "transfer"; restore: "restore"; purge: "purge"; export: "export"; - transfer: "transfer"; }>; principal: z.ZodObject<{ userId: z.ZodNullable; @@ -269,9 +269,9 @@ declare const ExplainDecisionSchema: z.ZodObject<{ principalKind: z.ZodOptional>; onBehalfOf: z.ZodOptional; kernelTier: z.ZodOptional; @@ -365,15 +365,15 @@ declare const ExplainDecisionSchema: z.ZodObject<{ visible: z.ZodBoolean; decidedBy: z.ZodOptional>; }, z.core.$strip>>; records: z.ZodOptional>; }, z.core.$strip>>>; }, z.core.$strip>; @@ -404,15 +404,15 @@ type ExplainLayerParsed = z.infer; declare const ExplainLayerSchema: z.ZodObject<{ layer: z.ZodEnum<{ sharing: "sharing"; - fls: "fls"; - rls: "rls"; owd_baseline: "owd_baseline"; tenant_isolation: "tenant_isolation"; principal: "principal"; required_permissions: "required_permissions"; object_crud: "object_crud"; + fls: "fls"; depth: "depth"; vama_bypass: "vama_bypass"; + rls: "rls"; }>; kernelTier: z.ZodOptional; @@ -485,11 +485,11 @@ type ExplainMatchedRule = z.input; declare const ExplainMatchedRuleSchema: z.ZodObject<{ kind: z.ZodEnum<{ sharing_rule: "sharing_rule"; - ownership: "ownership"; - team: "team"; tenant_filter: "tenant_filter"; owd_baseline: "owd_baseline"; + ownership: "ownership"; record_share: "record_share"; + team: "team"; territory: "territory"; rls_policy: "rls_policy"; }>; @@ -517,10 +517,10 @@ declare const ExplainOperationSchema: z.ZodEnum<{ update: "update"; read: "read"; create: "create"; + transfer: "transfer"; restore: "restore"; purge: "purge"; export: "export"; - transfer: "transfer"; }>; // ── ExplainRecordAttribution (type) ── @@ -541,11 +541,11 @@ declare const ExplainRecordAttributionSchema: z.ZodObject<{ rules: z.ZodDefault; @@ -577,10 +577,10 @@ declare const ExplainRequestSchema: z.ZodObject<{ update: "update"; read: "read"; create: "create"; + transfer: "transfer"; restore: "restore"; purge: "purge"; export: "export"; - transfer: "transfer"; }>; recordId: z.ZodOptional; recordIds: z.ZodOptional>; @@ -1213,13 +1213,13 @@ declare const permissionForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { diff --git a/packages/spec/api-surface-declarations/system.txt b/packages/spec/api-surface-declarations/system.txt index 46425c691a2..fdc7017a4d9 100644 --- a/packages/spec/api-surface-declarations/system.txt +++ b/packages/spec/api-surface-declarations/system.txt @@ -2850,8 +2850,8 @@ declare const ChangeSetSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -3257,19 +3264,12 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -5207,7 +5207,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -5261,6 +5261,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -5399,14 +5403,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -6361,7 +6361,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -6415,6 +6415,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -6553,14 +6557,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -7516,7 +7516,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -7570,6 +7570,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -7708,14 +7712,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -8670,7 +8670,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -8724,6 +8724,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -8862,14 +8866,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -10043,8 +10043,8 @@ declare const ChangeSetSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -10450,19 +10457,12 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -11826,7 +11826,7 @@ declare const ChangeSetSchema: z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -13042,8 +13042,8 @@ declare const ChangeSetSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -13449,19 +13456,12 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -15399,7 +15399,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -15453,6 +15453,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -15591,14 +15595,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -16553,7 +16553,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -16607,6 +16607,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -16745,14 +16749,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -17708,7 +17708,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -17762,6 +17762,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -17900,14 +17904,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -18862,7 +18862,7 @@ declare const ChangeSetSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -18916,6 +18916,10 @@ declare const ChangeSetSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -19054,14 +19058,10 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -20235,8 +20235,8 @@ declare const ChangeSetSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -20642,19 +20649,12 @@ declare const ChangeSetSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -22018,7 +22018,7 @@ declare const ChangeSetSchema: z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -23197,8 +23197,8 @@ declare const CreateObjectOperation: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -23604,19 +23611,12 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -25554,7 +25554,7 @@ declare const CreateObjectOperation: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -25608,6 +25608,10 @@ declare const CreateObjectOperation: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -25746,14 +25750,10 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -26708,7 +26708,7 @@ declare const CreateObjectOperation: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -26762,6 +26762,10 @@ declare const CreateObjectOperation: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -26900,14 +26904,10 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -27863,7 +27863,7 @@ declare const CreateObjectOperation: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -27917,6 +27917,10 @@ declare const CreateObjectOperation: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -28055,14 +28059,10 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -29017,7 +29017,7 @@ declare const CreateObjectOperation: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -29071,6 +29071,10 @@ declare const CreateObjectOperation: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -29209,14 +29213,10 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -30390,8 +30390,8 @@ declare const CreateObjectOperation: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -30797,19 +30804,12 @@ declare const CreateObjectOperation: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -32173,7 +32173,7 @@ declare const CreateObjectOperation: z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -34136,8 +34136,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -34543,19 +34550,12 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -36493,7 +36493,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -36547,6 +36547,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -36685,14 +36689,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -37647,7 +37647,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -37701,6 +37701,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -37839,14 +37843,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -38802,7 +38802,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -38856,6 +38856,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -38994,14 +38998,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -39956,7 +39956,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -40010,6 +40010,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -40148,14 +40152,10 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -41329,8 +41329,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -41736,19 +41743,12 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -43112,7 +43112,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -43749,8 +43749,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -44156,19 +44163,12 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -44463,9 +44463,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ description: z.ZodOptional; defaultTab: z.ZodOptional; tabPosition: z.ZodOptional>; allowSkip: z.ZodOptional; @@ -44477,9 +44477,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ splitSize: z.ZodOptional; splitResizable: z.ZodOptional; drawerSide: z.ZodOptional>; drawerWidth: z.ZodOptional; @@ -44987,8 +44987,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -45394,19 +45401,12 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -45701,9 +45701,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ description: z.ZodOptional; defaultTab: z.ZodOptional; tabPosition: z.ZodOptional>; allowSkip: z.ZodOptional; @@ -45715,9 +45715,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ splitSize: z.ZodOptional; splitResizable: z.ZodOptional; drawerSide: z.ZodOptional>; drawerWidth: z.ZodOptional; @@ -46411,8 +46411,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -46496,7 +46496,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -46570,7 +46570,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -46660,8 +46660,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; filterBy: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -47085,7 +47085,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47159,7 +47159,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47305,8 +47305,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -47390,7 +47390,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47464,7 +47464,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47611,8 +47611,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -47696,7 +47696,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47770,7 +47770,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -47916,8 +47916,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -48001,7 +48001,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48075,7 +48075,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48222,8 +48222,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -48307,7 +48307,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48381,7 +48381,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48527,8 +48527,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -48612,7 +48612,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48686,7 +48686,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48833,8 +48833,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -48918,7 +48918,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -48992,7 +48992,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49138,8 +49138,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -49223,7 +49223,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49297,7 +49297,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49444,8 +49444,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -49529,7 +49529,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49603,7 +49603,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49749,8 +49749,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -49834,7 +49834,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -49908,7 +49908,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50055,8 +50055,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -50140,7 +50140,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50214,7 +50214,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50360,8 +50360,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -50445,7 +50445,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50519,7 +50519,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50666,8 +50666,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -50751,7 +50751,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50825,7 +50825,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -50971,8 +50971,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -51056,7 +51056,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -51130,7 +51130,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }[] | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; limit?: number | undefined; } | undefined; @@ -51358,9 +51358,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ stepSize: z.ZodOptional; showGridLines: z.ZodDefault; position: z.ZodOptional>; logarithmic: z.ZodDefault; @@ -51386,9 +51386,9 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ stepSize: z.ZodOptional; showGridLines: z.ZodDefault; position: z.ZodOptional>; logarithmic: z.ZodDefault; @@ -51526,8 +51526,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }>>; sortBy: z.ZodOptional; sortOrder: z.ZodOptional>; limit: z.ZodOptional; stageOrder: z.ZodOptional>>; @@ -51689,8 +51689,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ order: z.ZodOptional>; }, z.core.$strict>>>; drilldown: z.ZodDefault; @@ -53729,16 +53729,16 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ object: z.ZodString; active: z.ZodDefault; accessLevel: z.ZodDefault>; sharedWith: z.ZodObject<{ type: z.ZodEnum<{ user: "user"; field: "field"; position: "position"; - business_unit: "business_unit"; team: "team"; + business_unit: "business_unit"; unit_and_subordinates: "unit_and_subordinates"; }>; value: z.ZodString; @@ -54484,8 +54484,8 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ append_only: "append_only"; }>>>; direction: z.ZodDefault>>; realtimeSync: z.ZodDefault>; @@ -58404,8 +58404,8 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -58811,19 +58818,12 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -60761,7 +60761,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -60815,6 +60815,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -60953,14 +60957,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -61915,7 +61915,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -61969,6 +61969,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -62107,14 +62111,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -63070,7 +63070,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -63124,6 +63124,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -63262,14 +63266,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -64224,7 +64224,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ } | undefined; sort?: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; }[] | undefined; filter?: { field: string; @@ -64278,6 +64278,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ colorField?: string | undefined; allDayField?: string | undefined; } | undefined; + sharing?: { + type: "personal" | "collaborative"; + lockedBy?: string | undefined; + } | undefined; aria?: { ariaLabel?: string | (Record & { key?: never; @@ -64416,14 +64420,10 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions?: number[] | undefined; } | undefined; pageName?: undefined; - sharing?: { - type: "personal" | "collaborative"; - lockedBy?: string | undefined; - } | undefined; grouping?: { fields: { field: string; - order: "desc" | "asc"; + order: "asc" | "desc"; collapsed: boolean; }[]; } | undefined; @@ -65597,8 +65597,8 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strict>>>; filter: z.ZodOptional; allDayField: z.ZodOptional; }, z.core.$strict>>; + sharing: z.ZodOptional>; + lockedBy: z.ZodOptional; + }, z.core.$strict>>; aria: z.ZodOptional & { key?: never; @@ -66004,19 +66011,12 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ pageSizeOptions: z.ZodOptional>; }, z.core.$strict>>; pageName: z.ZodOptional; - sharing: z.ZodOptional>; - lockedBy: z.ZodOptional; - }, z.core.$strict>>; grouping: z.ZodOptional>; collapsed: z.ZodDefault; }, z.core.$strict>>; @@ -67380,7 +67380,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ reason: z.ZodString; docsUrl: z.ZodOptional; }, z.core.$strict>>; - }, z.core.$strict>>>(config: T & Record, never> & (T extends { + }, z.core.$strict>>>(config: T & Record, never> & (T extends { actions: infer As extends readonly unknown[]; } ? { actions: { [I in keyof As]: As[I] extends infer T_1 ? T_1 extends As[I] ? T_1 extends { @@ -70496,8 +70496,8 @@ type SupplierAssessmentStatus = z.input; // ── SupplierAssessmentStatusSchema (const) ── declare const SupplierAssessmentStatusSchema: z.ZodEnum<{ - failed: "failed"; expired: "expired"; + failed: "failed"; completed: "completed"; pending: "pending"; in_progress: "in_progress"; @@ -70531,8 +70531,8 @@ declare const SupplierSecurityAssessmentSchema: z.ZodObject<{ high: "high"; }>; status: z.ZodEnum<{ - failed: "failed"; expired: "expired"; + failed: "failed"; completed: "completed"; pending: "pending"; in_progress: "in_progress"; @@ -72577,13 +72577,13 @@ declare const emailTemplateForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index acd380e6af3..8305cc17fec 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -132,39 +132,71 @@ interface ActionEngineFacade { */ delete(object: string, idOrIds: string | string[]): Promise; /** - * Read the rows of `object` that match `filter`. + * Read the rows of `object` that `query` selects. * - * `filter` is a FILTER — the `where` HALF of an ObjectQL query, the same - * {@link FilterCondition} that `QueryAST.where` carries: implicit equality - * `{ field: value }`, explicit operators `{ field: { $in: [...] } }`, - * `$and` / `$or` / `$not`. It is NOT the query ENVELOPE - * (`{ where, fields, orderBy, limit }`) that `DataEngine.find` and ObjectQL's - * own `engine.find` take — the shape this parameter's former name, `query`, - * invited. The runtime builds the envelope itself: `buildActionEngineFacade`'s - * `find` arm (`packages/runtime/src/action-execution.ts`, `:1183` on - * `369da918`) wraps a non-empty filter as `{ where: filter }` and passes an - * EMPTY filter (`{}`) through unwrapped — the unfiltered read. + * ## One platform, one query shape * - * Two consequences, both silent (#14175): + * `query` is the ENGINE's query envelope — {@link EngineQueryOptions}, the + * very type `IDataEngine.find` and ObjectQL's own `engine.find` take, named + * here by identity rather than restated. The filter goes under `where`, and + * the rest of the envelope (`fields`, `orderBy`, `limit`, `offset`, + * `expand`, `search`, …) means exactly what it means on the engine: * - * - An envelope passed here becomes `{ where: { where: … } }`. No object has - * a field named `where`, so the read matches nothing and resolves to `[]` - * with no error. A handler that made this mistake ran to completion over - * zero rows for as long as it shipped, and its own hand-written test - * double — written to the same belief, reading `query.where` — passed - * every assertion. - * - Because `{}` skips the wrap, an unfiltered call works under EITHER - * reading, so a handler mixing one unfiltered read with envelope-shaped - * ones looks partially alive rather than uniformly dead. + * ```ts + * await ctx.engine.find('todo_task', { where: { status: 'completed' } }); + * await ctx.engine.find('todo_task', { where: { status: 'open' }, fields: ['id', 'subject'], limit: 50 }); + * await ctx.engine.find('todo_task', {}); // the unfiltered read + * ``` * - * What the type buys, exactly: `FilterCondition` refuses a primitive and a - * mistyped logical operator (`$and` / `$or` not arrays, `$not` not a - * filter). It does NOT refuse `{ where: … }` — its string index signature is - * what lets any field name stand as a key, and `where` is a string — so the - * envelope mistake still compiles, and this doc comment, not the type, is - * the contract of record. Both halves are pinned in `action-params.test.ts`. + * ## What changed, and why it is a WITHDRAWAL rather than a narrowing + * + * Until #15124 this slot took a bare `FilterCondition` — the `where` + * half alone — and the runtime wrapped it. That parameter shape differed + * from the engine's for no reason a caller could see, and the cost was + * silent: an author who wrote the engine's own envelope got + * `{ where: { where: … } }`, which matches no row (no object has a field + * named `where`) and resolves to `[]` with no error. A handler that made + * that mistake ran to completion over zero rows for as long as it shipped, + * and its hand-written test double — written to the same belief — passed + * every assertion. `{}` skipped the wrap, so one unfiltered read kept + * working under either belief and a dead handler looked partially alive. + * + * #14175 declared the bare filter and pinned the gap; #15124 withdraws the + * shape instead. Closing the gap the other way would have had to assert a + * vocabulary fact the spec declares nowhere — that no object may carry a + * field named `where` — and that is a word taken from every customer's data + * model to buy one parameter's compile-time check. Aligning the parameter + * removes the ambiguity at its root: the most natural spelling is now the + * correct one, and nothing is reserved. + * + * Migration is lossless and mechanical: `find(o, f)` → `find(o, { where: f })` + * (ADR-0087 semantic migration `action-engine-facade-find-query-envelope`). + * An unfiltered `find(o, {})` is unchanged. + * + * ## What the type refuses, measured + * + * A bare filter no longer type-checks, on BOTH paths a caller can reach it + * by: an object literal (`{ status: 'completed' }`) fails the excess-property + * check, because a field name is not an envelope key; and a filter held in a + * variable typed `FilterCondition` fails TS2559 — `EngineQueryOptions` is a + * weak type, every key optional, and a filter of field names has no property + * in common with it. The refusal is a compile error at the call site, never a + * runtime surprise. The envelope's own keys are typed, so `where: 'a = b'`, + * `fields: 'id,subject'` and `limit: '50'` are refused too. + * + * ## `context` is the caller's to pass and NOT the caller's to choose + * + * The envelope carries `context` because every engine option bag does. This + * facade is TRUSTED and context-less by design (#2849, ADR-0096): the + * runtime stamps its own elevated `ExecutionContext` last, so a + * caller-supplied `context` is overridden rather than honoured. Do not write + * one — it reads as authorization and is none. + * + * Every clause above is pinned in `action-params.test.ts`, and the + * pass-through is pinned against the runtime in + * `packages/runtime/src/action-engine-facade-find-envelope.test.ts`. */ - find(object: string, filter: FilterCondition): Promise>>; + find(object: string, query: EngineQueryOptions): Promise>>; } // ── ActionHandler (type) ── @@ -2878,8 +2910,8 @@ declare const ComponentPropsMap: { }>>; type: z.ZodOptional; position: z.ZodDefault>; alwaysShowStrip: z.ZodOptional; items: z.ZodArray; }, z.core.$strict>>]>>; limit: z.ZodDefault; @@ -3251,8 +3283,8 @@ declare const ComponentPropsMap: { email: "email"; system: "system"; note: "note"; - approval: "approval"; sharing: "sharing"; + approval: "approval"; event: "event"; comment: "comment"; field_change: "field_change"; @@ -3309,8 +3341,8 @@ declare const ComponentPropsMap: { email: "email"; system: "system"; note: "note"; - approval: "approval"; sharing: "sharing"; + approval: "approval"; event: "event"; comment: "comment"; field_change: "field_change"; @@ -3385,8 +3417,8 @@ declare const ComponentPropsMap: { email: "email"; system: "system"; note: "note"; - approval: "approval"; sharing: "sharing"; + approval: "approval"; event: "event"; comment: "comment"; field_change: "field_change"; @@ -4288,8 +4320,8 @@ declare const ComponentPropsMap: { sort: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -4471,8 +4503,8 @@ declare const ComponentPropsMap: { sort: z.ZodOptional; }, z.core.$strip>>>; defaultSort: z.ZodOptional; @@ -4717,8 +4749,8 @@ declare const ComponentPropsMap: { sort: z.ZodOptional; }, z.core.$strip>>>; data: z.ZodOptional>; @@ -4780,9 +4812,9 @@ declare const ComponentPropsMap: { }>>]>>; defaultTab: z.ZodOptional; tabPosition: z.ZodOptional>; allowSkip: z.ZodOptional; @@ -4794,9 +4826,9 @@ declare const ComponentPropsMap: { splitSize: z.ZodOptional; splitResizable: z.ZodOptional; drawerSide: z.ZodOptional>; drawerWidth: z.ZodOptional>; @@ -5015,8 +5047,8 @@ declare const ComponentPropsMap: { sort: z.ZodOptional; }, z.core.$strip>>>; map: z.ZodOptional; }, z.core.$strip>>>; gantt: z.ZodOptional; }, z.core.$strip>>>; limit: z.ZodOptional; @@ -10684,8 +10716,8 @@ declare const ObjectCalendarPropsSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; data: z.ZodOptional>; @@ -10752,9 +10784,9 @@ declare const ObjectFormPropsSchema: z.ZodObject<{ }>>]>>; defaultTab: z.ZodOptional; tabPosition: z.ZodOptional>; allowSkip: z.ZodOptional; @@ -10766,9 +10798,9 @@ declare const ObjectFormPropsSchema: z.ZodObject<{ splitSize: z.ZodOptional; splitResizable: z.ZodOptional; drawerSide: z.ZodOptional>; drawerWidth: z.ZodOptional>; @@ -10937,8 +10969,8 @@ declare const ObjectGanttPropsSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; gantt: z.ZodOptional; }, z.core.$strip>>>; defaultSort: z.ZodOptional; @@ -12035,8 +12067,8 @@ declare const ObjectMapPropsSchema: z.ZodObject<{ sort: z.ZodOptional; }, z.core.$strip>>>; map: z.ZodOptional>; type: z.ZodOptional; position: z.ZodDefault>; alwaysShowStrip: z.ZodOptional; items: z.ZodArray; }, z.core.$strict>>]>>; limit: z.ZodDefault; @@ -27058,13 +27090,13 @@ declare const actionForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -27258,13 +27290,13 @@ declare const appForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -27515,13 +27547,13 @@ declare const dashboardForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -27715,13 +27747,13 @@ declare const datasetForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -27994,13 +28026,13 @@ declare const pageForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -28200,13 +28232,13 @@ declare const reportForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { @@ -28420,13 +28452,13 @@ declare const viewForm: { title?: string | undefined; description?: string | undefined; defaultTab?: string | undefined; - tabPosition?: "left" | "right" | "top" | "bottom" | undefined; + tabPosition?: "top" | "left" | "right" | "bottom" | undefined; allowSkip?: boolean | undefined; showStepIndicator?: boolean | undefined; splitDirection?: "vertical" | "horizontal" | undefined; splitSize?: number | undefined; splitResizable?: boolean | undefined; - drawerSide?: "left" | "right" | "top" | "bottom" | undefined; + drawerSide?: "top" | "left" | "right" | "bottom" | undefined; drawerWidth?: string | undefined; modalSize?: "default" | "full" | "sm" | "lg" | "xl" | undefined; data?: { diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index c0ee8e31d01..60a4d58c27e 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5582,6 +5582,45 @@ const step18: MigrationStep = { + 'dispatches matches the declaration (N for per-record, one for aggregate) — a mismatch that ' + 'used to be silent is what this key exists to surface.', }, + // The action facade's `find` took the `where` HALF of a query while every other + // `find` on the platform took the whole envelope. The rewrite is mechanical and + // lossless, but it lives in an action HANDLER's source — a TypeScript function + // body, not a keyed metadata document — so `objectstack migrate meta` cannot + // reach it and it is a semantic entry rather than a D2 conversion. + { + id: 'action-engine-facade-find-query-envelope', + surface: 'Action handler body — `ctx.engine.find(object, filter)` ' + + '(`ActionEngineFacade.find`, `@objectstack/spec/ui`)', + replacement: '`ctx.engine.find(object, { where: filter })` — the engine\'s own query envelope ' + + '(`EngineQueryOptions`), the same options bag `IDataEngine.find` takes. The filter moves under ' + + '`where` verbatim: `find(\'task\', { status: \'open\' })` → ' + + '`find(\'task\', { where: { status: \'open\' } })`. An unfiltered `find(object, {})` is unchanged, ' + + 'and the rest of the envelope — `fields`, `orderBy`, `limit`, `offset`, `expand` — becomes ' + + 'reachable from a handler for the first time. A caller-supplied `context` is ignored: the ' + + 'facade is trusted and stamps its own elevated one.', + reason: + 'The rewrite itself is lossless and mechanical, but it is not automatable here: an action handler ' + + 'is authored TypeScript, and the chain rewrites stored metadata by key, so no `os migrate meta` ' + + 'step can reach a call expression inside a function body. The change is a WITHDRAWAL of the ' + + 'parameter shape #14175 chose, ruled by the director seat (decision batch #123 item 3, ' + + '2026-09-12, 「同意」) on the long-term axis 「one platform, one query shape」. The facade had been ' + + 'given a shape different from the engine\'s — the `where` half alone — which made the most ' + + 'natural spelling the wrong one: an author who passed the engine\'s envelope got ' + + '`{ where: { where: … } }`, matching no row and resolving to `[]` with no error, while an ' + + 'unfiltered `{}` kept working under either belief so a dead handler looked partially alive. The ' + + 'alternative — refusing `where` at the top level with an intersection — was rejected because it ' + + 'asserts a vocabulary fact the spec declares nowhere, reserving the field name `where` across ' + + 'every customer\'s data model to buy one parameter\'s compile-time check.', + acceptanceCriteria: + 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope, and the package ' + + 'type-checks: a bare filter is now a compile error at the call site — an object literal fails ' + + 'the excess-property check and a `FilterCondition` variable fails TS2559 — so `tsc --noEmit` ' + + 'over the handlers finds every unmigrated call, with no runtime run needed. Then confirm the ' + + 'reads that were already SILENTLY EMPTY: any handler that had been passing the envelope was ' + + 'resolving to `[]` on every call, so a suite written against the mistake passed and the row ' + + 'count is the only witness — re-run each migrated handler against seeded data and assert it now ' + + 'returns the rows its filter selects, rather than asserting it still resolves.', + }, { id: 'address-location-value-unknown-keys-refused', surface: 'stored `address` and `location` field VALUES (`AddressSchema` / `AddressValueSchema`, ' diff --git a/skills/objectstack-data/references/_index.md b/skills/objectstack-data/references/_index.md index bc75258863f..6601f4589de 100644 --- a/skills/objectstack-data/references/_index.md +++ b/skills/objectstack-data/references/_index.md @@ -21,6 +21,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol - `node_modules/@objectstack/spec/src/automation/flow-function.zod.ts` — The contract for a **named handler function a `script` node invokes** — +- `node_modules/@objectstack/spec/src/data/data-engine.zod.ts` — Data Engine Protocol - `node_modules/@objectstack/spec/src/data/driver-sql.zod.ts` — Exports: SQLDialectSchema, DataTypeMappingSchema, SSLConfigSchema, SQLDriverConfigSchema, SQLiteDataTypeMappingDefaults - `node_modules/@objectstack/spec/src/data/driver.zod.ts` — Exports: DriverOptionsSchema, DriverCapabilitiesSchema, DriverInterfaceSchema, PoolConfigSchema, DriverConfigSchema - `node_modules/@objectstack/spec/src/data/driver/common.zod.ts` — Shared building blocks for the per-driver `datasource.config` shapes. @@ -35,7 +36,9 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema - `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries. +- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object, - `node_modules/@objectstack/spec/src/security/rls.zod.ts` — Row-Level Security (RLS) Protocol - `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index e91ebf9be5e..9be8ae689a8 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -23,6 +23,7 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies - `node_modules/@objectstack/spec/src/api/errors.zod.ts` — Standardized Error Codes Protocol +- `node_modules/@objectstack/spec/src/data/data-engine.zod.ts` — Data Engine Protocol - `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/data/feed.zod.ts` — Activity-timeline UI config enums, and the `sys_activity.type` built-in set. - `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). @@ -30,7 +31,9 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Exports: HookBodyCapability, ExpressionBodySchema, ScriptBodySchema, HookBodySchema - `node_modules/@objectstack/spec/src/data/query.zod.ts` — QueryAST — Abstract Syntax Tree for data queries. +- `node_modules/@objectstack/spec/src/kernel/execution-context.zod.ts` — Exports: ExecutionContextSchema - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) +- `node_modules/@objectstack/spec/src/security/explain.zod.ts` — [ADR-0090 D6] Access-explanation contract — `explain(principal, object, - `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas From c5f0855f4464fcb218acd6316378980fb966f907 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 00:42:54 +0000 Subject: [PATCH 3/8] fix(spec,runtime): keep the day-one anchor residual live, and pin the new engine double Three gate-driven corrections, none of them a contract change: - `action-params.zod.ts` keeps its `:1183` / `369da918` citation, as data about where the wrap USED to live. Dropping it silently repaired a row in `check-spec-docblock-symbol-anchors`'s day-one residual, whose repair that gate's own header assigns to a `domain:spec` repair card (#16960), not to whoever edits the file next. - the new runtime double drops the `update()` it never exercised, matching its pinned sibling, so `check:engine-double-contract` has no unbound verb to pin. - `engine-double-contract.pinned.json` learns the new file (gate `--write`, additive: 810 rows, 1 added, 0 lost). `ui.txt` follows the doc comment. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .../runtime/src/action-engine-facade-find-envelope.test.ts | 3 --- packages/spec/api-surface-declarations/ui.txt | 6 ++++-- packages/spec/src/ui/action-params.zod.ts | 6 ++++-- scripts/engine-double-contract.pinned.json | 5 +++++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/action-engine-facade-find-envelope.test.ts b/packages/runtime/src/action-engine-facade-find-envelope.test.ts index 9821768de34..cd8333c75ee 100644 --- a/packages/runtime/src/action-engine-facade-find-envelope.test.ts +++ b/packages/runtime/src/action-engine-facade-find-envelope.test.ts @@ -67,9 +67,6 @@ function makeEngine(rows: Array> = []) { async count(_object: string, _options?: Record) { return rows.length; }, - async update(_object: string, _data: Record, _options?: Record) { - return { ok: true }; - }, async delete(object: string, options?: Record) { assertEngineDeleteDispatch(options); return { ok: true, object }; diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index 8305cc17fec..a1b1ffc60b3 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -151,8 +151,10 @@ interface ActionEngineFacade { * ## What changed, and why it is a WITHDRAWAL rather than a narrowing * * Until #15124 this slot took a bare `FilterCondition` — the `where` - * half alone — and the runtime wrapped it. That parameter shape differed - * from the engine's for no reason a caller could see, and the cost was + * half alone — and the runtime's `find` arm wrapped it + * (`packages/runtime/src/action-execution.ts`, `:1183` on `369da918`; that + * wrap is gone as of this card). That parameter shape differed from the + * engine's for no reason a caller could see, and the cost was * silent: an author who wrote the engine's own envelope got * `{ where: { where: … } }`, which matches no row (no object has a field * named `where`) and resolves to `[]` with no error. A handler that made diff --git a/packages/spec/src/ui/action-params.zod.ts b/packages/spec/src/ui/action-params.zod.ts index a7c30826974..23466e4954b 100644 --- a/packages/spec/src/ui/action-params.zod.ts +++ b/packages/spec/src/ui/action-params.zod.ts @@ -295,8 +295,10 @@ export interface ActionEngineFacade { * ## What changed, and why it is a WITHDRAWAL rather than a narrowing * * Until #15124 this slot took a bare `FilterCondition` — the `where` - * half alone — and the runtime wrapped it. That parameter shape differed - * from the engine's for no reason a caller could see, and the cost was + * half alone — and the runtime's `find` arm wrapped it + * (`packages/runtime/src/action-execution.ts`, `:1183` on `369da918`; that + * wrap is gone as of this card). That parameter shape differed from the + * engine's for no reason a caller could see, and the cost was * silent: an author who wrote the engine's own envelope got * `{ where: { where: … } }`, which matches no row (no object has a field * named `where`) and resolves to `[]` with no error. A handler that made diff --git a/scripts/engine-double-contract.pinned.json b/scripts/engine-double-contract.pinned.json index dc0147a4bcb..bde3fbc37b1 100644 --- a/scripts/engine-double-contract.pinned.json +++ b/scripts/engine-double-contract.pinned.json @@ -3446,6 +3446,11 @@ "verb": "delete", "pinned": 1 }, + { + "file": "packages/runtime/src/action-engine-facade-find-envelope.test.ts", + "verb": "delete", + "pinned": 1 + }, { "file": "packages/runtime/src/action-engine-facade-nullish-id.test.ts", "verb": "delete", From 77f73a3d03f749822e82e4d781f093cc1683f909 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 01:33:02 +0000 Subject: [PATCH 4/8] fix(runtime): refuse the withdrawn bare filter in the facade's `find` arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contract-review FAIL, finding ①: passing the envelope through opened a SILENT path on the untyped channel. `ObjectQL.find`'s unknown-option refusal (#4371) exempts a null VALUE — correct for an option bag, where a null is a withdrawal; wrong for a filter, where `{ deleted_at: null }` is the "rows with no X" idiom. Measured on a real engine over three seeded rows: the key was dropped unexecuted and the read returned ALL THREE, the excluded row included, with no error. Before this card the wrap kept every filter key away from that exemption. The arm now judges its own parameter first and refuses any key the envelope does not carry — null-valued included — naming the stray key and prescribing `where`. The key set is read off `EngineQueryOptionsSchema`, the same declaration the parameter's type names, so the compile-time and runtime refusals are one fact. Also: `action-body-identity.test.ts` was still calling the facade with the withdrawn shape at two sites and riding green (the facade returns `any` and the double did not validate); its "the caller's predicate must survive" case asserted `toBeDefined()` on the recorded entry, which is true whatever the arm did with the filter. Both migrated, and that case now asserts the predicate. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- ...ction-engine-facade-find-query-envelope.md | 22 ++- content/docs/ui/actions.mdx | 15 +- .../runtime/src/action-body-identity.test.ts | 25 ++- ...action-engine-facade-find-envelope.test.ts | 148 +++++++++++++++++- packages/runtime/src/action-execution.ts | 87 ++++++++++ ...ction-engine-facade-find-query-envelope.ts | 20 ++- packages/spec/src/ui/action-params.zod.ts | 33 +++- 7 files changed, 318 insertions(+), 32 deletions(-) diff --git a/.changeset/15124-action-engine-facade-find-query-envelope.md b/.changeset/15124-action-engine-facade-find-query-envelope.md index 388b5cf9bb2..d3d7a2c8527 100644 --- a/.changeset/15124-action-engine-facade-find-query-envelope.md +++ b/.changeset/15124-action-engine-facade-find-query-envelope.md @@ -43,17 +43,35 @@ the ambiguity at its root and reserves nothing. ### What the new declaration refuses, measured -A bare filter no longer type-checks on **either** path a caller can reach it by: +If your handler is typed with the published `ActionHandlerContext`, a bare filter +no longer type-checks on **either** path you can reach it by: - an object literal (`{ status: 'completed' }`) fails the excess-property check — a field name is not an envelope key; - a filter held in a `FilterCondition` variable fails **TS2559** — every envelope key is optional, so a bag of field names has no property in common with it. -So the failure is a compile error at the call site, never a runtime surprise. The envelope's own keys are typed too: `where: 'a = b'`, `fields: 'id,subject'` and `limit: '50'` are each refused. +**If your handler is NOT typed with it** — a handler in an `objectstack.config.js` +/ `.mjs`, one annotated with your own copy of the context type, or a `(ctx: any)` +handler — nothing above reaches you, so the facade refuses the withdrawn shape at +**runtime** instead, before the engine, with the same prescription: + +``` +find('task') was given a key 'status' the query envelope does not carry. +ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare +filter — move the filter under `where`: find(object, { where: { … } }) (#15124). +``` + +⚠️ **That refusal matters most for a filter whose value is `null`.** The engine's +own unknown-option check exempts a `null` value, because on an option bag a +`null` is a withdrawal. On a filter it is the "rows with no X" idiom, so +`{ deleted_at: null }` would have been dropped unexecuted and the read would have +widened to **every row** — including the ones you were excluding — with no error +at all. It is refused instead. + ### What this opens `fields`, `orderBy`, `limit`, `offset` and `expand` are reachable from an action diff --git a/content/docs/ui/actions.mdx b/content/docs/ui/actions.mdx index e2d0d352eb1..56545f994c8 100644 --- a/content/docs/ui/actions.mdx +++ b/content/docs/ui/actions.mdx @@ -183,10 +183,17 @@ await ctx.engine.find('todo_task', {}); // the unfiltered read ``` The parameter is typed `EngineQueryOptions` (`ActionEngineFacade` in -`@objectstack/spec/ui`), so a bare filter is a **compile error** at the call -site — `{ status: 'completed' }` has nowhere to land, and so does a filter held -in a `FilterCondition` variable. A hand-written test double must honour the -envelope too. +`@objectstack/spec/ui`), so if you annotate `ctx` with the published +`ActionHandlerContext` a bare filter is a **compile error** at the call site — +`{ status: 'completed' }` has nowhere to land, and neither does a filter held in +a `FilterCondition` variable. A hand-written test double must honour the envelope +too. + +If you do **not** annotate it — a handler in an `objectstack.config.js` / +`.mjs`, your own copy of the context type, or `(ctx: any)` — the facade refuses +the bare filter at **runtime** instead, naming the stray key and prescribing the +same fix. That runtime refusal is what stops `{ deleted_at: null }` from being +dropped unexecuted and quietly widening the read to every row. **Upgrading?** This parameter used to be the `where` half on its own, and the runtime wrapped it. `find(o, f)` becomes `find(o, { where: f })`; an unfiltered diff --git a/packages/runtime/src/action-body-identity.test.ts b/packages/runtime/src/action-body-identity.test.ts index f1d18223a03..5c03cc400a8 100644 --- a/packages/runtime/src/action-body-identity.test.ts +++ b/packages/runtime/src/action-body-identity.test.ts @@ -36,9 +36,12 @@ import { QuickJSScriptRunner } from './sandbox/quickjs-runner.js'; * `ctx.api` binding is exercised end-to-end. */ function makeSharingEngine(extra: Record = {}) { - const writes: Array<{ op: string; object: string; context: any }> = []; - const gate = (op: string, object: string, context: any) => { - writes.push({ op, object, context }); + // [#15124] `where` is recorded as well as `context`, so the "the caller's + // predicate must survive" case below can assert the PREDICATE instead of + // asserting that an entry exists. + const writes: Array<{ op: string; object: string; context: any; where?: unknown }> = []; + const gate = (op: string, object: string, context: any, where?: unknown) => { + writes.push({ op, object, context, where }); if (!context?.isSystem && !context?.userId) { throw new Error(`FORBIDDEN: insufficient privileges to ${op} ${object}`); } @@ -58,7 +61,7 @@ function makeSharingEngine(extra: Record = {}) { return { ok: true }; }, async find(object: string, options?: any) { - gate('find', object, options?.context); + gate('find', object, options?.context, options?.where); // [#16370] The by-id pre-load has to be ANSWERED: an action door now // refuses a row-scoped invocation whose caller-scope subject load did // not deliver the row, so a rig that answered every read with `[]` @@ -121,7 +124,9 @@ describe('#3914 — ctx.engine (buildActionEngineFacade)', () => { await engine.insert('crm_case', { subject: 'x' }); await engine.update('crm_case', 'case_1', { status: 'closed' }); await engine.delete('crm_case', 'case_1'); - await engine.find('crm_case', { status: 'open' }); + // [#15124] The envelope, not a bare filter — the withdrawn shape is now + // refused by the arm itself, so this call would throw if left as it was. + await engine.find('crm_case', { where: { status: 'open' } }); expect(ql.writes.map((w: any) => w.op)).toEqual(['insert', 'update', 'delete', 'find']); for (const w of ql.writes) { @@ -138,10 +143,14 @@ describe('#3914 — ctx.engine (buildActionEngineFacade)', () => { it('still passes the caller filter on find (context is additive, not a replacement)', async () => { const ql = makeSharingEngine(); const engine = buildActionEngineFacade(deps, ql, { userId: 'u1' }); - await engine.find('crm_case', { status: 'open' }); + await engine.find('crm_case', { where: { status: 'open' } }); expect(ql.writes[0].context).toMatchObject({ isSystem: true, userId: 'u1' }); - // the caller's predicate must survive alongside the injected context - expect((ql.writes as any)[0]).toBeDefined(); + // [#15124] The caller's predicate must survive alongside the injected + // context — asserted on the PREDICATE. This line used to read + // `expect(ql.writes[0]).toBeDefined()`, which is true of any recorded + // call whatever the arm did with the filter, so the one thing the case + // is named for was the one thing it did not check. + expect(ql.writes[0].where).toEqual({ status: 'open' }); }); }); diff --git a/packages/runtime/src/action-engine-facade-find-envelope.test.ts b/packages/runtime/src/action-engine-facade-find-envelope.test.ts index cd8333c75ee..986bc598d48 100644 --- a/packages/runtime/src/action-engine-facade-find-envelope.test.ts +++ b/packages/runtime/src/action-engine-facade-find-envelope.test.ts @@ -41,9 +41,11 @@ * @see packages/spec/src/ui/action-params.zod.ts — the member doc of record. */ -import { describe, it, expect } from 'vitest'; +import { describe, it, expect, beforeAll } from 'vitest'; import { assertEngineDeleteDispatch } from '@objectstack/metadata-core'; -import { buildActionEngineFacade } from './action-execution.js'; +import { ObjectQL } from '@objectstack/objectql'; +import { InMemoryDriver } from '@objectstack/driver-memory'; +import { buildActionEngineFacade, ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION } from './action-execution.js'; const deps: any = { resolveService: () => undefined, getObjectQL: async () => undefined }; @@ -134,3 +136,145 @@ describe('#15124 — ActionEngineFacade.find passes the engine query envelope th expect(context.isSystem).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// The UNTYPED channel, through a REAL engine +// --------------------------------------------------------------------------- + +/** + * Everything above pins the ARGUMENT, against a double. That is the right + * instrument for "the envelope goes through", and the wrong one for the + * question this block asks, which is what a caller still on the WITHDRAWN + * shape actually experiences — because the answer is produced by the engine, + * and a double is free to be kinder than one. + * + * ## Why there is an untyped channel at all + * + * `buildActionEngineFacade` returns `any`, so the published declaration binds + * a caller only where the caller opted into it. Three populations do not: + * a handler in a JS config (`objectstack.config.js` / `.mjs` are accepted + * spellings — `packages/cli/src/utils/config.ts`), a handler annotated with a + * LOCAL copy of the context type (the pattern #15117 measured in this repo's + * own example), and a `(ctx: any)` handler. ⛔ Metadata `type: 'script'` bodies + * are NOT in this set: the sandbox `ScriptContext` exposes `api`, never + * `engine`. + * + * ## What the engine does with a bare filter, and why one half was silent + * + * `ObjectQL.find` refuses option keys it does not execute (#4371) — but its + * refusal deliberately EXEMPTS a `null` value, because on an option bag a + * `null` is a withdrawal carrying no intent a drop could lose. On a FILTER + * that rule is exactly wrong: `{ deleted_at: null }` is the "rows with no X" + * idiom, and dropping it silently returns EVERY row — including the ones the + * author was excluding — to a caller whose next line is often a delete. + * + * Before #15124 the facade wrapped its argument, so no filter key ever reached + * that exemption. Removing the wrap without a refusal here would have opened + * the silent path, which is why the arm now judges its own parameter against + * the envelope's key set. The four rows below are that boundary, measured end + * to end rather than argued. + */ + +const PROBE_OBJECT = { + name: 'probe_task', + label: 'Probe Task', + fields: { + id: { type: 'text', label: 'Id' }, + status: { type: 'text', label: 'Status' }, + deleted_at: { type: 'datetime', label: 'Deleted at' }, + }, +} as any; + +/** A real `ObjectQL` over a real in-memory driver, seeded with three rows. */ +async function makeRealEngine() { + const engine = new ObjectQL(); + engine.registerDriver(new InMemoryDriver({}) as any, true); + await engine.init(); + engine.registry.registerObject(PROBE_OBJECT, 'test'); + await engine.syncSchemas?.(); + + const ctx = { isSystem: true } as any; + // t3 carries a non-null `deleted_at`, so a filter that is HONOURED returns + // two rows and a filter that is DROPPED returns three. Without it the + // silent-drop row would be indistinguishable from a correct answer. + await engine.insert('probe_task', { id: 't1', status: 'open', deleted_at: null }, { context: ctx }); + await engine.insert('probe_task', { id: 't2', status: 'completed', deleted_at: null }, { context: ctx }); + await engine.insert('probe_task', { id: 't3', status: 'open', deleted_at: '2026-01-01T00:00:00.000Z' }, { context: ctx }); + + return engine; +} + +/** Drive a call and hand back what it rejected with, or `undefined`. */ +async function rejection(run: Promise): Promise { + return run.then(() => undefined, (e: unknown) => e); +} + +describe('#15124 — the withdrawn shape on the UNTYPED channel, through a real engine', () => { + let engine: any; + beforeAll(async () => { engine = await makeRealEngine(); }); + + it('control — the envelope selects, through the real engine', async () => { + const facade = buildActionEngineFacade(deps, engine, { userId: 'u1' }); + + const rows = await facade.find('probe_task', { where: { status: 'completed' } }); + + expect(rows.map((r: any) => r.id)).toEqual(['t2']); + }); + + it('control — the empty envelope is still the unfiltered read', async () => { + const facade = buildActionEngineFacade(deps, engine, { userId: 'u1' }); + + const rows = await facade.find('probe_task', {}); + + expect(rows.map((r: any) => r.id).sort()).toEqual(['t1', 't2', 't3']); + }); + + it('REFUSES the withdrawn bare filter, and the refusal carries the `where` prescription', async () => { + const facade = buildActionEngineFacade(deps, engine, { userId: 'u1' }); + + const err = await rejection(facade.find('probe_task', { status: 'completed' })); + + expect(err).toBeInstanceOf(Error); + expect((err as Error).message).toContain(ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION); + // The offending key is NAMED — a prescription with no subject sends the + // reader back to a diff to find out which key it meant. + expect((err as Error).message).toContain("'status'"); + }); + + it('REFUSES a NULL-VALUED bare filter too — the row the engine\'s null exemption used to swallow', async () => { + const facade = buildActionEngineFacade(deps, engine, { userId: 'u1' }); + + const err = await rejection(facade.find('probe_task', { deleted_at: null })); + + // ⭐ THIS is the row the review failed the first cut on. Without the + // arm's own refusal the engine's `value == null` exemption lets the key + // through unexecuted and the call RESOLVES WITH ALL THREE ROWS — `t3`, + // the one the author was excluding, included. A `toThrow()` with no + // message assertion would not have caught it either: the shape that + // must never come back is ROWS. + expect(err).toBeInstanceOf(Error); + expect((err as Error).message).toContain(ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION); + expect((err as Error).message).toContain("'deleted_at'"); + expect(Array.isArray(err)).toBe(false); + }); + + it('the refusal is loud INSTEAD of reading, not as well as — nothing was queried', async () => { + const facade = buildActionEngineFacade(deps, engine, { userId: 'u1' }); + let reached = 0; + const counting = new Proxy(engine, { + get(target, prop, recv) { + if (prop === 'find') return async (...args: unknown[]) => { reached += 1; return (target as any).find(...args); }; + return Reflect.get(target, prop, recv); + }, + }); + const countingFacade = buildActionEngineFacade(deps, counting, { userId: 'u1' }); + + await rejection(countingFacade.find('probe_task', { deleted_at: null })); + + expect(reached).toBe(0); + // ...and the control proves the counter can move at all. + await countingFacade.find('probe_task', { where: { status: 'completed' } }); + expect(reached).toBe(1); + expect(facade).toBeDefined(); + }); +}); diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index 77a961f4c79..8dc2d5ed264 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -16,6 +16,9 @@ */ import { validateActionParams, type ActionSession, type ResolvedActionParam } from '@objectstack/spec/ui'; +// [#15124] The facade's `find` judges its own parameter against the SAME +// declaration its type names, so the refusal and the type cannot drift apart. +import { EngineQueryOptionsSchema } from '@objectstack/spec/data'; import type { ExecutionContext } from '@objectstack/spec/kernel'; import type { IObjectQLEngine, ServiceSlotContract, ServiceSlotContracts } from '@objectstack/spec/contracts'; // [#15942 / #16293] The confirmation member's SPELLING is the contract, so it @@ -1465,6 +1468,85 @@ export function buildActionApi(_deps: ActionExecutionDeps, ql: any, ec: any): an } } +/** + * The sentence a caller still on the WITHDRAWN bare-filter shape must read + * (#15124). Exported because the wording IS the contract here: this refusal is + * a plain `Error` carrying no ADR-0112 `code`/`status` — the same shape its + * sibling {@link ENGINE_DELETE_REJECT_MESSAGE} has — so a bare `toThrow()` + * would stay green against any unnamed `Error` at all, and the pin has to + * compare text. + */ +export const ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION = + 'ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare filter — ' + + 'move the filter under `where`: find(object, { where: { … } }) (#15124).'; + +/** + * The envelope's own key set, read off the DECLARATION rather than restated. + * + * Resolved on first use, never at module load: `EngineQueryOptionsSchema` is a + * `lazySchema` proxy whose whole purpose is to defer building its closures, and + * touching `.shape` here would build them for every process that imports this + * module whether or not an action ever runs. + */ +let actionEngineFindEnvelopeKeys: ReadonlySet | undefined; +function findEnvelopeKeys(): ReadonlySet { + return (actionEngineFindEnvelopeKeys ??= new Set( + Object.keys((EngineQueryOptionsSchema as unknown as { shape: Record }).shape), + )); +} + +/** + * Refuse the shape #15124 withdrew, loudly, BEFORE the engine sees it. + * + * ## Why the engine's own refusal is not enough + * + * `ObjectQL.find` already rejects option keys it does not execute (#4371) — + * but that check deliberately exempts a `null` VALUE, because on an option bag + * a `null` is a withdrawal carrying no intent a drop could lose. On a FILTER + * the same rule is exactly wrong: `{ deleted_at: null }` is the "rows with no + * X" idiom, so the key is dropped, the read widens to every row, and the call + * resolves. Measured on a real engine over three seeded rows, the excluded row + * came back with the others. + * + * Until this card the facade WRAPPED its argument, so no filter key ever + * reached that exemption; passing the envelope through without this guard is + * what would have opened the path. A handler's next line after such a read is + * routinely a delete, so this is the silent-data-loss class, not a DX nit. + * + * ## Why the key set comes from the schema + * + * The declared parameter type is `EngineQueryOptions`. Reading the same + * schema's shape here makes the compile-time refusal and the runtime refusal + * one fact with one source: a key the type rejects is a key this rejects, and a + * key the engine grows in the spec is legal in both on the same day. + * + * ⚠️ MEASURED DELTA, deliberate: the engine's own `find` set additionally + * carries six driver pass-through keys (`transaction`, `tenantId`, + * `tenantIds`, `timezone`, `bypassTenantAudit`, `preserveAudit`) that + * `EngineQueryOptions` does not declare. They stay refused here. Three of them + * are tenancy escape hatches, this facade is trusted and context-less by + * design, and no typed caller can write any of them — so agreeing with the + * TYPE is both the narrower and the fail-closed reading. Retired keys + * (`cursor`, `distinct`) are in the shape and pass through on purpose: the + * engine answers them with their tombstone, which is the better message. + */ +function assertActionEngineFindEnvelope(object: string, query: Record | undefined): void { + if (!query) return; + const legal = findEnvelopeKeys(); + let stray: string[] | undefined; + for (const key of Object.keys(query)) { + if (legal.has(key)) continue; + (stray ??= []).push(key); + } + if (!stray) return; + throw new Error( + `find('${object}') was given ${stray.length > 1 ? 'keys' : 'a key'} ` + + `${stray.map((k) => `'${k}'`).join(', ')} the query envelope does not carry. ` + + `${ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION} ` + + `Envelope keys: ${[...legal].sort().join(', ')}.`, + ); +} + /** * Build the action-body `ctx.engine` — the slim CRUD surface handler suites * use. Every call carries {@link buildActionExecutionContext} so `ctx.engine` @@ -1527,6 +1609,11 @@ export function buildActionEngineFacade(_deps: ActionExecutionDeps, ql: any, ec? // not an authorization the caller gets to choose. Pinned in // `action-engine-facade-find-envelope.test.ts`. async find(object: string, query?: Record): Promise>> { + // …and the withdrawn shape is refused HERE, before the engine, so + // the untyped channel gets the same answer the type gives + // (`assertActionEngineFindEnvelope` above says why the engine's own + // check cannot be the whole of it). + assertActionEngineFindEnvelope(object, query); const rows = await ql.find(object, { ...(query ?? {}), context } as any); return Array.isArray(rows) ? rows : ((rows as any)?.value ?? []); }, diff --git a/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts b/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts index c27f719cdd7..e3344f3b067 100644 --- a/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts +++ b/packages/spec/src/migrations/entries/semantic/18.action-engine-facade-find-query-envelope.ts @@ -32,12 +32,16 @@ export const entry: SemanticMigration = { + 'asserts a vocabulary fact the spec declares nowhere, reserving the field name `where` across ' + 'every customer\'s data model to buy one parameter\'s compile-time check.', acceptanceCriteria: - 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope, and the package ' - + 'type-checks: a bare filter is now a compile error at the call site — an object literal fails ' - + 'the excess-property check and a `FilterCondition` variable fails TS2559 — so `tsc --noEmit` ' - + 'over the handlers finds every unmigrated call, with no runtime run needed. Then confirm the ' - + 'reads that were already SILENTLY EMPTY: any handler that had been passing the envelope was ' - + 'resolving to `[]` on every call, so a suite written against the mistake passed and the row ' - + 'count is the only witness — re-run each migrated handler against seeded data and assert it now ' - + 'returns the rows its filter selects, rather than asserting it still resolves.', + 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope. Where the handler ' + + 'is annotated with the PUBLISHED `ActionHandlerContext`, `tsc --noEmit` finds every unmigrated ' + + 'call on its own — a bare filter is a compile error there, an object literal failing the ' + + 'excess-property check and a `FilterCondition` variable failing TS2559. ⚠️ Where it is NOT — a ' + + 'handler in an `objectstack.config.js` / `.mjs`, one annotated with a local copy of the context ' + + 'type, or a `(ctx: any)` handler — the type reaches nothing and a type-check alone proves ' + + 'nothing: those callers are refused at RUNTIME by the facade arm, with the same prescription, so ' + + 'the migration is complete for them only once each such handler has actually been RUN. Then ' + + 'confirm the reads that were already SILENTLY EMPTY: any handler that had been passing the ' + + 'envelope was resolving to `[]` on every call, so a suite written against the mistake passed and ' + + 'the row count is the only witness — re-run each migrated handler against seeded data and assert ' + + 'it now returns the rows its filter selects, rather than asserting it still resolves.', }; diff --git a/packages/spec/src/ui/action-params.zod.ts b/packages/spec/src/ui/action-params.zod.ts index 23466e4954b..356a7decf17 100644 --- a/packages/spec/src/ui/action-params.zod.ts +++ b/packages/spec/src/ui/action-params.zod.ts @@ -321,14 +321,31 @@ export interface ActionEngineFacade { * * ## What the type refuses, measured * - * A bare filter no longer type-checks, on BOTH paths a caller can reach it - * by: an object literal (`{ status: 'completed' }`) fails the excess-property - * check, because a field name is not an envelope key; and a filter held in a - * variable typed `FilterCondition` fails TS2559 — `EngineQueryOptions` is a - * weak type, every key optional, and a filter of field names has no property - * in common with it. The refusal is a compile error at the call site, never a - * runtime surprise. The envelope's own keys are typed, so `where: 'a = b'`, - * `fields: 'id,subject'` and `limit: '50'` are refused too. + * A bare filter no longer type-checks, on BOTH paths a TYPED caller can + * reach it by: an object literal (`{ status: 'completed' }`) fails the + * excess-property check, because a field name is not an envelope key; and a + * filter held in a variable typed `FilterCondition` fails TS2559 — + * `EngineQueryOptions` is a weak type, every key optional, and a filter of + * field names has no property in common with it. The envelope's own keys are + * typed, so `where: 'a = b'`, `fields: 'id,subject'` and `limit: '50'` are + * refused too. + * + * ## …and what refuses it for a caller the TYPE never reached + * + * `buildActionEngineFacade` returns `any`, so a handler in a JS config, one + * annotated with a local copy of this context, or a `(ctx: any)` handler is + * bound by nothing here. For those the runtime arm refuses the withdrawn + * shape itself, before the engine, carrying the same prescription + * (`ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION`, + * `packages/runtime/src/action-execution.ts`) and reading its key set off + * THIS schema so the two channels cannot drift apart. + * + * ⚠️ That arm is load-bearing rather than belt-and-braces, and the reason is + * a `null`: the engine's own unknown-option refusal (#4371) exempts a + * `null`-VALUED key, because on an option bag a `null` is a withdrawal. On a + * FILTER it is the "rows with no X" idiom — so `{ deleted_at: null }` passed + * straight through would be dropped unexecuted and the read would widen to + * EVERY row, silently, to a caller whose next line is often a delete. * * ## `context` is the caller's to pass and NOT the caller's to choose * From 26f18cdc0a314802e09d62fa9a19995a8785a222 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 01:43:48 +0000 Subject: [PATCH 5/8] chore(spec): regenerate the registry and declaration surface for the review fixes `gen:migration-registry` picks up the corrected acceptance criteria (a type check alone proves nothing for a handler the type never reached); `gen:api-surface-declarations` picks up the member doc, which now states the runtime half and why a null-valued key is the load-bearing case. `check:generated`: all 16 artifacts up to date. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- packages/spec/api-surface-declarations/ui.txt | 33 ++++++++++++++----- packages/spec/src/migrations/registry.ts | 20 ++++++----- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index a1b1ffc60b3..3d268e9bbd6 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -177,14 +177,31 @@ interface ActionEngineFacade { * * ## What the type refuses, measured * - * A bare filter no longer type-checks, on BOTH paths a caller can reach it - * by: an object literal (`{ status: 'completed' }`) fails the excess-property - * check, because a field name is not an envelope key; and a filter held in a - * variable typed `FilterCondition` fails TS2559 — `EngineQueryOptions` is a - * weak type, every key optional, and a filter of field names has no property - * in common with it. The refusal is a compile error at the call site, never a - * runtime surprise. The envelope's own keys are typed, so `where: 'a = b'`, - * `fields: 'id,subject'` and `limit: '50'` are refused too. + * A bare filter no longer type-checks, on BOTH paths a TYPED caller can + * reach it by: an object literal (`{ status: 'completed' }`) fails the + * excess-property check, because a field name is not an envelope key; and a + * filter held in a variable typed `FilterCondition` fails TS2559 — + * `EngineQueryOptions` is a weak type, every key optional, and a filter of + * field names has no property in common with it. The envelope's own keys are + * typed, so `where: 'a = b'`, `fields: 'id,subject'` and `limit: '50'` are + * refused too. + * + * ## …and what refuses it for a caller the TYPE never reached + * + * `buildActionEngineFacade` returns `any`, so a handler in a JS config, one + * annotated with a local copy of this context, or a `(ctx: any)` handler is + * bound by nothing here. For those the runtime arm refuses the withdrawn + * shape itself, before the engine, carrying the same prescription + * (`ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION`, + * `packages/runtime/src/action-execution.ts`) and reading its key set off + * THIS schema so the two channels cannot drift apart. + * + * ⚠️ That arm is load-bearing rather than belt-and-braces, and the reason is + * a `null`: the engine's own unknown-option refusal (#4371) exempts a + * `null`-VALUED key, because on an option bag a `null` is a withdrawal. On a + * FILTER it is the "rows with no X" idiom — so `{ deleted_at: null }` passed + * straight through would be dropped unexecuted and the read would widen to + * EVERY row, silently, to a caller whose next line is often a delete. * * ## `context` is the caller's to pass and NOT the caller's to choose * diff --git a/packages/spec/src/migrations/registry.ts b/packages/spec/src/migrations/registry.ts index 60a4d58c27e..2d7384c35c2 100644 --- a/packages/spec/src/migrations/registry.ts +++ b/packages/spec/src/migrations/registry.ts @@ -5612,14 +5612,18 @@ const step18: MigrationStep = { + 'asserts a vocabulary fact the spec declares nowhere, reserving the field name `where` across ' + 'every customer\'s data model to buy one parameter\'s compile-time check.', acceptanceCriteria: - 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope, and the package ' - + 'type-checks: a bare filter is now a compile error at the call site — an object literal fails ' - + 'the excess-property check and a `FilterCondition` variable fails TS2559 — so `tsc --noEmit` ' - + 'over the handlers finds every unmigrated call, with no runtime run needed. Then confirm the ' - + 'reads that were already SILENTLY EMPTY: any handler that had been passing the envelope was ' - + 'resolving to `[]` on every call, so a suite written against the mistake passed and the row ' - + 'count is the only witness — re-run each migrated handler against seeded data and assert it now ' - + 'returns the rows its filter selects, rather than asserting it still resolves.', + 'Every `ctx.engine.find(...)` in the app\'s action handlers passes an envelope. Where the handler ' + + 'is annotated with the PUBLISHED `ActionHandlerContext`, `tsc --noEmit` finds every unmigrated ' + + 'call on its own — a bare filter is a compile error there, an object literal failing the ' + + 'excess-property check and a `FilterCondition` variable failing TS2559. ⚠️ Where it is NOT — a ' + + 'handler in an `objectstack.config.js` / `.mjs`, one annotated with a local copy of the context ' + + 'type, or a `(ctx: any)` handler — the type reaches nothing and a type-check alone proves ' + + 'nothing: those callers are refused at RUNTIME by the facade arm, with the same prescription, so ' + + 'the migration is complete for them only once each such handler has actually been RUN. Then ' + + 'confirm the reads that were already SILENTLY EMPTY: any handler that had been passing the ' + + 'envelope was resolving to `[]` on every call, so a suite written against the mistake passed and ' + + 'the row count is the only witness — re-run each migrated handler against seeded data and assert ' + + 'it now returns the rows its filter selects, rather than asserting it still resolves.', }, { id: 'address-location-value-unknown-keys-refused', From 35aad65994ad1a377fa7aadfcf8f3d611ba4d59a Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 02:11:12 +0000 Subject: [PATCH 6/8] fix(runtime): keep the tracker id out of the refusal STRING, and pin on sqlite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gate-driven corrections to the refusal, both mechanical: - `check:doc-authoring` — a runtime message reaches authors and operators who have no tracker to resolve `#NNNN` with, so the card id moves to the `//` comment above the constant. The changeset's quoted message follows, and now quotes the envelope key list the refusal actually prints (measured: 12 keys). - `check:driver-memory-census` — the real-engine pin bound `@objectstack/driver-memory`, whose consumer set #5704 froze and #6664 ruled to a ledger; a new binding there is a maintainer ruling, not bookkeeping. It binds sqlite `:memory:` instead, which is what #5704 migrated the test backends to. Nothing about the pin needed that driver: what has to be real here is the ENGINE, because the null exemption is the engine's. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- ...24-action-engine-facade-find-query-envelope.md | 4 +++- .../action-engine-facade-find-envelope.test.ts | 15 ++++++++++++--- packages/runtime/src/action-execution.ts | 5 ++++- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/.changeset/15124-action-engine-facade-find-query-envelope.md b/.changeset/15124-action-engine-facade-find-query-envelope.md index d3d7a2c8527..ef93af3ad41 100644 --- a/.changeset/15124-action-engine-facade-find-query-envelope.md +++ b/.changeset/15124-action-engine-facade-find-query-envelope.md @@ -62,7 +62,9 @@ handler — nothing above reaches you, so the facade refuses the withdrawn shape ``` find('task') was given a key 'status' the query envelope does not carry. ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare -filter — move the filter under `where`: find(object, { where: { … } }) (#15124). +filter — move the filter under `where`: find(object, { where: { … } }). +Envelope keys: context, cursor, distinct, expand, fields, limit, offset, +orderBy, search, searchFields, top, where. ``` ⚠️ **That refusal matters most for a filter whose value is `null`.** The engine's diff --git a/packages/runtime/src/action-engine-facade-find-envelope.test.ts b/packages/runtime/src/action-engine-facade-find-envelope.test.ts index 986bc598d48..a6be5b7e434 100644 --- a/packages/runtime/src/action-engine-facade-find-envelope.test.ts +++ b/packages/runtime/src/action-engine-facade-find-envelope.test.ts @@ -44,7 +44,7 @@ import { describe, it, expect, beforeAll } from 'vitest'; import { assertEngineDeleteDispatch } from '@objectstack/metadata-core'; import { ObjectQL } from '@objectstack/objectql'; -import { InMemoryDriver } from '@objectstack/driver-memory'; +import { SqliteWasmDriver } from '@objectstack/driver-sqlite-wasm'; import { buildActionEngineFacade, ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION } from './action-execution.js'; const deps: any = { resolveService: () => undefined, getObjectQL: async () => undefined }; @@ -185,10 +185,19 @@ const PROBE_OBJECT = { }, } as any; -/** A real `ObjectQL` over a real in-memory driver, seeded with three rows. */ +/** + * A real `ObjectQL` over a real driver, seeded with three rows. + * + * sqlite `:memory:` rather than `@objectstack/driver-memory`: #5704 migrated + * this project's test backends to it and froze the memory driver's consumer + * set, which `check:driver-memory-census` holds to a ruled ledger. A new + * binding there would need a maintainer ruling, and nothing about this pin + * needs that driver — what has to be REAL here is the ENGINE, because the + * null-exemption this block is about is the engine's. + */ async function makeRealEngine() { const engine = new ObjectQL(); - engine.registerDriver(new InMemoryDriver({}) as any, true); + engine.registerDriver(new SqliteWasmDriver({ filename: ':memory:' }) as any, true); await engine.init(); engine.registry.registerObject(PROBE_OBJECT, 'test'); await engine.syncSchemas?.(); diff --git a/packages/runtime/src/action-execution.ts b/packages/runtime/src/action-execution.ts index 8dc2d5ed264..769ce082534 100644 --- a/packages/runtime/src/action-execution.ts +++ b/packages/runtime/src/action-execution.ts @@ -1476,9 +1476,12 @@ export function buildActionApi(_deps: ActionExecutionDeps, ql: any, ec: any): an * would stay green against any unnamed `Error` at all, and the pin has to * compare text. */ +// The card id stays in this comment and out of the string: a runtime message +// reaches authors and operators who have no tracker to resolve `#NNNN` with +// (#15124, and `check:doc-authoring` enforces it). export const ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION = 'ctx.engine.find(object, query) takes the engine QUERY ENVELOPE, not a bare filter — ' - + 'move the filter under `where`: find(object, { where: { … } }) (#15124).'; + + 'move the filter under `where`: find(object, { where: { … } }).'; /** * The envelope's own key set, read off the DECLARATION rather than restated. From ee8c41133e13de338c2157de79f8c78b71f99d7d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 03:43:56 +0000 Subject: [PATCH 7/8] chore(spec): regenerate `api-surface-declarations/ui.txt` on the merged tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseline drift, not a code change. `origin/main`'s #19219 moved `ComponentPropsMap` and `ObjectTimelinePropsSchema` after this branch forked, and `api-surface-declarations/**` is a `merge=os-regen` path — so the merge produced a `ui.txt` that was current for neither side, which is what turned `Check @objectstack/spec declaration text` red. Regenerated from a real build of the merged tree (34/34 declaration files emitted; ⛔ no `OS_SKIP_DTS`), via `scripts/pm/os-regen-merge.sh`. BOTH SIDES asserted present afterwards, by quoted-exact name against the INDEX blob rather than the worktree, with a dark control reading 0 files: this branch's `find(object: string, query: EngineQueryOptions)` and `ACTION_ENGINE_FIND_ENVELOPE_PRESCRIPTION`; #19219's `ObjectTimelineProps`, `ObjectTimelinePropsSchema` and `ComponentPropsMap`. Note the two `navigation` keys that branch also added live in `authorable-surface/ui.json` and in NO declaration file — a grep scoped to `api-surface-declarations/` reads 0 for them out of range, not out of loss, so the assertion ran over the whole tree and printed paths. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- packages/spec/api-surface-declarations/ui.txt | 288 +++++++++++++++++- 1 file changed, 286 insertions(+), 2 deletions(-) diff --git a/packages/spec/api-surface-declarations/ui.txt b/packages/spec/api-surface-declarations/ui.txt index 3d268e9bbd6..06a82c7d8b4 100644 --- a/packages/spec/api-surface-declarations/ui.txt +++ b/packages/spec/api-surface-declarations/ui.txt @@ -12,8 +12,8 @@ # excluded: documentation drift is `check:docs`'s axis, not this one. # # entry: ./ui -# exported names: 471 -# declarations: 485 +# exported names: 474 +# declarations: 488 # # GENERATED — ⛔ never hand-edited. Regenerate after a real build: # pnpm --filter @objectstack/spec build && pnpm --filter @objectstack/spec gen:api-surface-declarations @@ -4730,6 +4730,29 @@ declare const ComponentPropsMap: { quickAdd: z.ZodOptional; coverImageField: z.ZodOptional; conditionalFormatting: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; }, z.core.$strict>; readonly 'object-calendar': z.ZodObject<{ objectName: z.ZodOptional; @@ -4776,6 +4799,29 @@ declare const ComponentPropsMap: { staticData: z.ZodOptional>; locale: z.ZodOptional; loading: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; }, z.core.$strict>; readonly 'object-form': z.ZodObject<{ objectName: z.ZodOptional; @@ -5324,6 +5370,98 @@ declare const ComponentPropsMap: { }, z.core.$strict>>; navigation: z.ZodOptional; }, z.core.$strict>; + readonly 'object-timeline': z.ZodObject<{ + objectName: z.ZodOptional; + timeline: z.ZodOptional; + titleField: z.ZodString; + groupByField: z.ZodOptional; + colorField: z.ZodOptional; + scale: z.ZodDefault>; + }, z.core.$strict>>; + filter: z.ZodOptional>; + value: z.ZodOptional>]>>; + }, z.core.$strict>>>; + sort: z.ZodOptional; + }, z.core.$strip>>>; + limit: z.ZodOptional; + data: z.ZodOptional>; + items: z.ZodOptional>; + variant: z.ZodOptional>; + dateFormat: z.ZodOptional>; + rowLabel: z.ZodOptional; + minDate: z.ZodOptional; + maxDate: z.ZodOptional; + descriptionField: z.ZodOptional; + mapping: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; + }, z.core.$strict>; }; // ── DATE_RANGE_DEFAULT_RANGES (const) ── @@ -10743,6 +10881,29 @@ declare const ObjectCalendarPropsSchema: z.ZodObject<{ staticData: z.ZodOptional>; locale: z.ZodOptional; loading: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; }, z.core.$strict>; // ── ObjectFormProps (type) ── @@ -11272,6 +11433,29 @@ declare const ObjectKanbanPropsSchema: z.ZodObject<{ quickAdd: z.ZodOptional; coverImageField: z.ZodOptional; conditionalFormatting: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; }, z.core.$strict>; // ── ObjectListViewSchema (const) ── @@ -12336,6 +12520,106 @@ declare const ObjectNavItemSchema: z.ZodObject<{ requiresService: z.ZodOptional; }, z.core.$strict>; +// ── ObjectTimelineProps (type) ── +type ObjectTimelineProps = z.input; + +// ── ObjectTimelinePropsParsed (type) ── +type ObjectTimelinePropsParsed = z.infer; + +// ── ObjectTimelinePropsSchema (const) ── +declare const ObjectTimelinePropsSchema: z.ZodObject<{ + objectName: z.ZodOptional; + timeline: z.ZodOptional; + titleField: z.ZodString; + groupByField: z.ZodOptional; + colorField: z.ZodOptional; + scale: z.ZodDefault>; + }, z.core.$strict>>; + filter: z.ZodOptional>; + value: z.ZodOptional>]>>; + }, z.core.$strict>>>; + sort: z.ZodOptional; + }, z.core.$strip>>>; + limit: z.ZodOptional; + data: z.ZodOptional>; + items: z.ZodOptional>; + variant: z.ZodOptional>; + dateFormat: z.ZodOptional>; + rowLabel: z.ZodOptional; + minDate: z.ZodOptional; + maxDate: z.ZodOptional; + descriptionField: z.ZodOptional; + mapping: z.ZodOptional; + navigation: z.ZodOptional>; + view: z.ZodOptional; + preventNavigation: z.ZodDefault; + openNewTab: z.ZodDefault; + size: z.ZodDefault>; + width: z.ZodOptional>; + }, z.core.$strict>>; +}, z.core.$strict>; + // ── ObjectTreeProps (type) ── type ObjectTreeProps = z.input; From 1e391fe8b516d0721847d9cf7daa6d720c873e8f Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 06:00:37 +0000 Subject: [PATCH 8/8] chore(spec): regenerate the four drifted declaration files on the merged tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Baseline drift, not a code change — the fourth sync lap on this branch. Main's #19226 and #19235 moved `packages/spec/api-surface-declarations/{data,root, system,ui}.txt`, and that directory is a `merge=os-regen` path, so the merge produced four files current for neither side. Regenerated from a real build of the merged tree (34/34 declaration files emitted; ⛔ no `OS_SKIP_DTS`), via `scripts/pm/os-regen-merge.sh`, with `MERGE_HEAD` confirmed absent first — the build opens with `gen:schema`, and running that in MERGE state is the anchor-rollback trap. ⚠️ The `MM` grade was live here and was read on purpose. After regenerating, the index held main's side (803/535) while the worktree held the regeneration (323/4); a bare `git commit` would have landed the index. `git add -A` first, then `git diff --cached` re-read as the 323/4 it should be, and every one of the four index blobs hash-matches its worktree file. BOTH SIDES asserted by quoted-exact name over the WHOLE TREE with paths printed, then again against the index blobs, with a dark control reading 0 files: this branch's facade signature and prescription constant; #19226's `DEFAULT_VIEW_ROW_LIMIT`, `KanbanConfigParsed` and the three `ui/{Gallery,Kanban,Timeline}Config:limit` keys; #19235's `RecordRelatedListProps.columns[number]` and `z.array(ListColumnSchema)`; and #19219's `ObjectTimelinePropsSchema` carried forward. Note the three `limit` keys live ONLY in `authorable-surface/ui.json` and `authorable-defaults/ui.json` and the related-list row ONLY in `content/docs/references/ui/component.mdx` — a grep scoped to the declaration files reads 0 for them out of range, not loss. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .../spec/api-surface-declarations/data.txt | 9 + .../spec/api-surface-declarations/root.txt | 48 +++++ .../spec/api-surface-declarations/system.txt | 96 ++++++++++ packages/spec/api-surface-declarations/ui.txt | 174 +++++++++++++++++- 4 files changed, 323 insertions(+), 4 deletions(-) diff --git a/packages/spec/api-surface-declarations/data.txt b/packages/spec/api-surface-declarations/data.txt index 37b5637becd..045d3b05f65 100644 --- a/packages/spec/api-surface-declarations/data.txt +++ b/packages/spec/api-surface-declarations/data.txt @@ -17319,6 +17319,7 @@ declare const ObjectSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -3177,6 +3179,7 @@ declare const ChangeSetSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -10370,6 +10387,7 @@ declare const ChangeSetSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -13369,6 +13389,7 @@ declare const ChangeSetSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -20562,6 +20597,7 @@ declare const ChangeSetSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -23524,6 +23562,7 @@ declare const CreateObjectOperation: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -30717,6 +30770,7 @@ declare const CreateObjectOperation: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -34463,6 +34519,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -41656,6 +41727,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -44076,6 +44150,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -45314,6 +45391,7 @@ declare const EnvironmentArtifactSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -58731,6 +58811,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; gallery: z.ZodOptional; @@ -65924,6 +66019,7 @@ declare const MigrationOperationSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; - columns: z.ZodOptional>; + columns: z.ZodOptional, z.ZodArray & { + key?: never; + defaultValue?: never; + }, Record & { + key?: never; + defaultValue?: never; + }, z.core.$ZodTypeInternals & { + key?: never; + defaultValue?: never; + }, Record & { + key?: never; + defaultValue?: never; + }>>]>>; + width: z.ZodOptional; + align: z.ZodOptional>; + hidden: z.ZodOptional; + sortable: z.ZodOptional; + resizable: z.ZodOptional; + wrap: z.ZodOptional; + type: z.ZodOptional; + pinned: z.ZodOptional>; + summary: z.ZodOptional, z.ZodObject<{ + type: z.ZodEnum<{ + count: "count"; + none: "none"; + min: "min"; + max: "max"; + sum: "sum"; + avg: "avg"; + count_empty: "count_empty"; + count_filled: "count_filled"; + count_unique: "count_unique"; + percent_empty: "percent_empty"; + percent_filled: "percent_filled"; + }>; + field: z.ZodOptional; + }, z.core.$strict>]>>; + prefix: z.ZodOptional>; + }, z.core.$strict>>; + link: z.ZodOptional; + action: z.ZodOptional; + }, z.core.$strict>>]>>; sort: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; filter: z.ZodOptional) => Dashboard; @@ -8795,6 +8866,7 @@ declare const GalleryConfigSchema: z.ZodObject<{ }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>; // ── GanttConfig (type) ── @@ -9738,12 +9810,16 @@ declare const KNOWN_COMPONENT_TYPE_CANDIDATES: readonly string[]; // ── KanbanConfig (type) ── type KanbanConfig = z.input; +// ── KanbanConfigParsed (type) ── +type KanbanConfigParsed = z.infer; + // ── KanbanConfigSchema (const) ── declare const KanbanConfigSchema: z.ZodObject<{ groupByField: z.ZodString; summarizeField: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>; // ── LIST_VIEW_GROUP_COUNT_ALIAS (const) ── @@ -10204,6 +10280,7 @@ declare const ListViewSchema: z.ZodObject<{ summarizeField: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; timeline: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; filter: z.ZodOptional; - columns: z.ZodOptional>; + columns: z.ZodOptional, z.ZodArray & { + key?: never; + defaultValue?: never; + }, Record & { + key?: never; + defaultValue?: never; + }, z.core.$ZodTypeInternals & { + key?: never; + defaultValue?: never; + }, Record & { + key?: never; + defaultValue?: never; + }>>]>>; + width: z.ZodOptional; + align: z.ZodOptional>; + hidden: z.ZodOptional; + sortable: z.ZodOptional; + resizable: z.ZodOptional; + wrap: z.ZodOptional; + type: z.ZodOptional; + pinned: z.ZodOptional>; + summary: z.ZodOptional, z.ZodObject<{ + type: z.ZodEnum<{ + count: "count"; + none: "none"; + min: "min"; + max: "max"; + sum: "sum"; + avg: "avg"; + count_empty: "count_empty"; + count_filled: "count_filled"; + count_unique: "count_unique"; + percent_empty: "percent_empty"; + percent_filled: "percent_filled"; + }>; + field: z.ZodOptional; + }, z.core.$strict>]>>; + prefix: z.ZodOptional>; + }, z.core.$strict>>; + link: z.ZodOptional; + action: z.ZodOptional; + }, z.core.$strict>>]>>; sort: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>; // ── TreeConfig (type) ── @@ -20910,6 +21061,7 @@ declare const VIEW_METADATA_MEMBERS: { }>>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; timeline: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional>; titleField: z.ZodOptional; visibleFields: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; chart: z.ZodOptional; titleField: z.ZodOptional; columns: z.ZodArray; + limit: z.ZodDefault; }, z.core.$strict>>; gantt: z.ZodOptional>; + limit: z.ZodDefault; }, z.core.$strict>>; calendar: z.ZodOptional