From 8e0725338d6742991111da780034dafff7c38de6 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 15:53:34 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(spec):=20docs-gen=20=E6=8A=8A=20retired?= =?UTF-8?q?Key()=20=E5=A2=93=E7=A2=91=E6=B8=B2=E6=9F=93=E6=88=90=20never,?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E6=98=AF=20any=20(#5606)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `retiredKey()` 是 `z.never()`,`z.toJSONSchema` 把它发成 `{ "not": {} }` —— 没有 `type`、没有 `$ref`、没有 `enum`。`formatType()` 没有对应分支,于是全仓 约 28 处墓碑一路落到函数末尾的 `return prop.type || 'any'`,reference 页把一个 **已删除**的键印成了 **`any`**。 这是退役能得到的最差渲染。这些页面是升级作者(很常是 AI 作者,ADR-0033)的主要 输入,`heading?: any` 读起来不是「这个键被删了」,而是「这个槽存在,而且不校验」 —— 比它替换掉的 `heading?: string` **更**鼓励去写。写了之后 parse 会带着 `[REMOVED]` 处方硬拒,但那已经是在一份错元数据产出之后了。 两处改动,都落在 `scripts/lib/format-type.ts`: - `{ not: {} }` 现在渲染成 `never`。这既是准确的 TypeScript(该键的 `z.input` 类型本就是 `never`),也不像 `any` 那样需要旁边的散文来兜底。 - 内联 shape 摘要在计入 `INLINE_KEY_LIMIT` **之前**先剔除墓碑。摘要格只印前 4 个 声明键的 `k?: type`,根本没有描述列,所以嵌套的墓碑无处安放处方: `ui/theme.mdx` 宣传着 `{ base?: string; heading?: any; mono?: any }`,而这两条 处方在整页**任何地方都不出现**。退役键已不再是可写面,因此不再占用四个槽位之 一,也不再把作者**必须**写的键挤到 `…` 后面。已知的「把墓碑挪到 shape 底部」 规避办法覆盖不了这一类:#5248 把 `IndexSchema` 退役到只剩 3 个活键,在上限为 4 时第一个墓碑**在数学上**必然进入摘要。 逐键表行不受影响,描述列仍然完整携带 `[REMOVED]` 处方,只是类型格从 `any` 改成了 `never`。 反向验证(实测,两半分别做,方向都是常规的「还原缺陷 → 新钉子变红」): 注释掉 `isNeverNode` 前置返回 → 3 failed | 24 passed,三条红全部报 `expected 'any' to be 'never'`;还原该返回、把摘要改回不过滤的 `Object.keys(prop.properties)` → 4 failed | 23 passed,四条红报出「只做第一半」 会发布的中间态(`{ base?: string; heading?: never; mono?: never }`)。 ⚠️ `content/docs/references/**` 的整体重生成不在本 commit 内:该步需要先 `gen:schema` 物化 gitignore 掉的 `packages/spec/json-schema/` 树,而本座位的 权限系统拒绝执行 `gen:schema`。详见 PR 正文。 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- .changeset/docs-gen-retired-key-never.md | 40 +++++ packages/spec/scripts/format-type.test.ts | 197 ++++++++++++++++++++++ packages/spec/scripts/lib/format-type.ts | 63 ++++++- 3 files changed, 294 insertions(+), 6 deletions(-) create mode 100644 .changeset/docs-gen-retired-key-never.md diff --git a/.changeset/docs-gen-retired-key-never.md b/.changeset/docs-gen-retired-key-never.md new file mode 100644 index 0000000000..8d4d409103 --- /dev/null +++ b/.changeset/docs-gen-retired-key-never.md @@ -0,0 +1,40 @@ +--- +"@objectstack/spec": patch +--- + +fix(spec): the reference generator prints a `retiredKey()` tombstone as `never`, not `any` (#5606) + +`retiredKey()` is `z.never()`, which `z.toJSONSchema` emits as `{ "not": {} }` — +a node with no `type`, no `$ref` and no `enum`. `formatType()` had no branch for +it, so every one of the ~28 tombstones in the spec fell through to the +`prop.type || 'any'` tail and the generated reference pages typed a **removed** +key as **`any`**. + +That is the worst available rendering for a retirement. These pages are the +primary input for an upgrading author — very often an AI one (ADR-0033) — and +`heading?: any` does not read "this key was deleted", it reads "this slot exists +and nothing validates it": strictly *more* inviting than the `heading?: string` +it replaced. The author writes it, the parse rejects it with the `[REMOVED]` +prescription, and the prescription arrives only after a wrong metadata file +already exists. + +Two changes, both in `scripts/lib/format-type.ts`: + +- **`{ not: {} }` now renders as `never`.** Accurate TypeScript — the key's + `z.input` type *is* `never` — and, unlike `any`, self-evident with no prose + to lean on. +- **Tombstones are dropped from an inline shape summary before + `INLINE_KEY_LIMIT` counts.** A summary cell prints `k?: type` for the first + four declared keys and has no description column, so a nested tombstone had + nowhere to put its prescription at all: `ui/theme.mdx` advertised + `{ base?: string; heading?: any; mono?: any }` with both prescriptions + appearing NOWHERE on the page. Retired keys are no longer authorable surface, + so they no longer spend one of the four slots — nor push a key the author + must write behind the `…`. The known workaround of moving a tombstone to the + bottom of the shape cannot cover this: #5248 retired `IndexSchema` down to + three live keys, and with a limit of four the first tombstone is then + *mathematically* guaranteed into the summary. + +Per-key table rows are unaffected and keep carrying the full `[REMOVED]` +prescription in their description column; their type cell simply now says +`never` instead of `any`. diff --git a/packages/spec/scripts/format-type.test.ts b/packages/spec/scripts/format-type.test.ts index b2c38d9bf5..3cfe3eff5b 100644 --- a/packages/spec/scripts/format-type.test.ts +++ b/packages/spec/scripts/format-type.test.ts @@ -318,3 +318,200 @@ describe('formatType — the shapes that were already right stay right', () => { expect(rendered).toBe('[Field](/docs/references/data/field#field)'); }); }); + +/** + * The `retiredKey()` tombstone node, verbatim as `z.toJSONSchema` emits it. + * + * Probed against the real converter (`z.toJSONSchema(z.object({ heading: + * retiredKey('…') }), { target: 'draft-2020-12' })`) rather than guessed: the + * node is `{ description, not: {} }` in BOTH the `output` and the `io: 'input'` + * direction `build-schemas.ts` falls back to. No `type`, no `$ref`, no `enum` — + * which is precisely why it used to reach the `prop.type || 'any'` tail. + */ +const tombstone = (guidance: string) => ({ description: `[REMOVED] ${guidance}`, not: {} }); + +/** The real `Typography.fontFamily` node — one live key, two tombstones (#5021). */ +const THEME_FONT_FAMILY = { + type: 'object', + properties: { + base: { type: 'string', description: 'Base font family (default: system fonts)' }, + heading: tombstone('`theme.typography.fontFamily.heading` was removed in …'), + mono: tombstone('`theme.typography.fontFamily.mono` was removed in …'), + }, + additionalProperties: false, +}; + +/** + * The five `Theme`/`Typography` tombstones that DO own a table row (#5021). + * Their description column keeps the prescription; the type cell said `any`. + */ +const THEME_TOMBSTONES = { + type: 'object', + properties: { + animation: tombstone('`theme.animation` was removed in …'), + zIndex: tombstone('`theme.zIndex` was removed in …'), + fontSize: tombstone('`theme.typography.fontSize` was removed in …'), + fontWeight: tombstone('`theme.typography.fontWeight` was removed in …'), + lineHeight: tombstone('`theme.typography.lineHeight` was removed in …'), + }, + additionalProperties: false, +} as { type: string; properties: Record; additionalProperties: boolean }; + +/** The real `ObjectSchema.indexes` element — three live keys, two tombstones (#5248). */ +const INDEX_SCHEMA = { + type: 'array', + items: { + type: 'object', + properties: { + name: { type: 'string' }, + fields: { type: 'array', items: { type: 'string' } }, + unique: { anyOf: [{ type: 'boolean' }, { const: 'global' }, { const: 'organization' }] }, + type: tombstone('`indexes[].type` was removed in …'), + partial: tombstone('`indexes[].partial` was removed in …'), + }, + required: ['fields'], + additionalProperties: false, + }, +}; + +/** + * Pin for how the renderer prints a `retiredKey()` tombstone — #5606. + * + * `retiredKey()` (`src/shared/retired-key.ts`) is `z.never()`, which + * `z.toJSONSchema` emits as `{ "not": {} }`. That node carries no `type`, no + * `$ref` and no `enum`, so it fell through every branch of `formatType` to the + * `prop.type || 'any'` tail and the reference pages printed **`any`**. That is + * the worst available rendering for a removed key: to an author — very often an + * AI one (ADR-0033), for whom these pages are the primary input — `heading?: + * any` does not read "deleted", it reads "this slot exists and nothing + * validates it", i.e. MORE inviting than the `heading?: string` it replaced. + * + * A top-level tombstone at least got its `[REMOVED]` prescription in the + * description column of its own table row. A tombstone nested inside an inline + * shape summary got nothing: the summary prints `k?: type` and has no + * description column at all. `content/docs/references/ui/theme.mdx` carried + * `{ base?: string; heading?: any; mono?: any }` with the two prescriptions + * appearing NOWHERE on the page. + * + * Two halves, both pinned below: + * + * 1. `{ not: {} }` → `never`. Accurate TypeScript (the key's `z.input` type + * IS `never`) and self-evident in a summary cell. + * 2. Tombstones are filtered out of the inline summary BEFORE + * `INLINE_KEY_LIMIT` counts. #5248 is why this is not cosmetic: it retired + * `IndexSchema` down to three live keys, so with a limit of four the first + * tombstone is mathematically guaranteed into the summary — the #5050 + * workaround of "move the tombstone to the bottom of the shape" cannot + * work once live keys < the limit. + * + * MEASURED (reverse verification), run one half at a time. The direction is the + * ordinary one for both — restore the defect, the new pins go red — because + * these assert a POSITIVE rendering the fix produces, not the absence of a + * finding: + * + * - Commenting out the `isNeverNode` early return: **3 failed | 24 passed**. + * The three reds are this block's `never` assertions, each reporting + * `expected 'any' to be 'never'` (and `'any[]' to be 'never[]'`). The + * fourth case here stays green ON PURPOSE — it asserts a NON-`never` + * rendering, so it is the over-reach guard, not a dead pin. The whole + * second block also stays green, which is the honest signal that the two + * halves are independent: filtering a tombstone out of a summary does not + * care how it would have rendered. + * - Restoring the early return and reverting the summary to an unfiltered + * `Object.keys(prop.properties)`: **4 failed | 23 passed**, all four in the + * second block, reporting the halfway state the `never` branch ALONE would + * have shipped — `{ base?: string; heading?: never; mono?: never }`, + * `{ dead1?: never; a: string; dead2?: never; b?: string; … }`, + * `{ x?: never; y?: never }`. Safer than `any`, still spending the reader's + * four slots on keys nobody may write. + */ +describe('formatType — `retiredKey()` tombstones render as `never`, not `any` (#5606)', () => { + it('renders a bare tombstone node as `never`', () => { + expect(formatType(tombstone('`x` was removed in …'), ctx())).toBe('never'); + // The `description` is incidental — the node is `never` with or without it. + expect(formatType({ not: {} }, ctx())).toBe('never'); + }); + + it('renders the top-level table-row nodes as `never` (the theme.mdx specimens)', () => { + // `Theme.animation` / `Theme.zIndex` / `Typography.fontSize` … DO have their + // own row, so the `[REMOVED]` prescription survives in the description + // column — but `build-docs.ts` builds the type cell by calling `formatType` + // on exactly these nodes, and every one of them printed `any`. + for (const key of ['animation', 'zIndex', 'fontSize', 'fontWeight', 'lineHeight']) { + expect(formatType(THEME_TOMBSTONES.properties[key], ctx())).toBe('never'); + } + }); + + it('recurses consistently — `never` survives the array and union branches', () => { + // Not live specimens (no schema writes `z.array(z.never())` today); these + // pin that the tombstone check stays AHEAD of the structural branches, so a + // future nesting cannot reopen the `any` hole one level down. + expect(formatType({ type: 'array', items: tombstone('`x` …') }, ctx())).toBe('never[]'); + expect(formatType({ anyOf: [{ type: 'string' }, tombstone('`x` …')] }, ctx())) + .toBe('string | never'); + }); + + it('does NOT match a non-empty `not` — that is a negation constraint, not `never`', () => { + expect(formatType({ type: 'string', not: { const: 'reserved' } }, ctx())).toBe('string'); + expect(formatType({ not: { type: 'string' } }, ctx())).toBe('any'); + // `not: null` must not throw on the `Object.keys` probe. + expect(formatType({ type: 'number', not: null }, ctx())).toBe('number'); + }); +}); + +describe('formatType — tombstones leave the inline summary to the live keys (#5606)', () => { + it('drops the two tombstones from `Typography.fontFamily` (the theme.mdx specimen)', () => { + // Was: `{ base?: string; heading?: any; mono?: any }`, with the two + // `[REMOVED]` prescriptions appearing nowhere on the page. + expect(formatType(THEME_FONT_FAMILY, ctx())).toBe('{ base?: string }'); + }); + + it('shows all three live `IndexSchema` keys — the #5248 boundary the workaround cannot reach', () => { + // Was: `{ name?: string; fields: string[]; unique?: …; type?: any; … }[]`. + // Three live keys < INLINE_KEY_LIMIT, so the summary is now COMPLETE: no + // `…`, and the cell's promise "these are all the keys" is true. + const rendered = formatType(INDEX_SCHEMA, ctx()); + expect(rendered).toBe( + "{ name?: string; fields: string[]; unique?: boolean | 'global' | 'organization' }[]", + ); + expect(rendered).not.toContain('…'); + expect(rendered).not.toContain('type?'); + expect(rendered).not.toContain('partial?'); + }); + + it('spends the limit on live keys only — a tombstone never elides a live key', () => { + const rendered = formatType( + { + type: 'object', + properties: { + dead1: tombstone('`dead1` …'), + a: { type: 'string' }, + dead2: tombstone('`dead2` …'), + b: { type: 'string' }, + c: { type: 'string' }, + d: { type: 'string' }, + e: { type: 'string' }, + }, + required: ['a'], + additionalProperties: false, + }, + ctx(), + ); + // Four LIVE keys shown, `…` for the fifth live one. Before the filter, two + // of the four slots went to removed keys and `c`/`d`/`e` were all elided. + expect(rendered).toBe('{ a: string; b?: string; c?: string; d?: string; … }'); + }); + + it('renders a shape whose every key is a tombstone as no shape at all', () => { + const allDead = { + type: 'object', + properties: { x: tombstone('`x` …'), y: tombstone('`y` …') }, + additionalProperties: false, + }; + // Nothing authorable is left, which is the same fact `z.object({})` states. + expect(formatType(allDead, ctx())).toBe('{ }'); + // …and with a catchall, the record rendering wins rather than printing + // `{ } & Record<…>` — the pre-existing rule, unchanged. + expect(formatType({ ...allDead, additionalProperties: {} }, ctx())).toBe('Record'); + }); +}); diff --git a/packages/spec/scripts/lib/format-type.ts b/packages/spec/scripts/lib/format-type.ts index 792a7253cb..f27ca5658a 100644 --- a/packages/spec/scripts/lib/format-type.ts +++ b/packages/spec/scripts/lib/format-type.ts @@ -78,9 +78,41 @@ function hasTopLevelUnionOrIntersection(rendered: string): boolean { return false; } +/** + * Is this node the JSON Schema encoding of `z.never()` — i.e. a `retiredKey()` + * tombstone (`packages/spec/src/shared/retired-key.ts`)? + * + * `z.toJSONSchema` renders `z.never()` as `{ "not": {} }` — the negation of the + * always-true empty schema, so nothing validates against it. That node carries + * no `type`, no `$ref` and no `enum`, so before #5606 it fell all the way + * through `formatType` to the `prop.type || 'any'` tail and printed as **`any`** + * — the one rendering that reads as "free-form slot, nothing validates it", + * which is the exact inverse of what a tombstone means. A key retired from + * `heading?: string` to a tombstone came out of the generator as + * `heading?: any`, i.e. *more* inviting to write than before it was removed. + * + * A **non-empty** `not` (`{ not: { type: 'string' } }`) is an ordinary negation + * constraint, not `never`, and is deliberately not matched here. + */ +function isNeverNode(prop: any): boolean { + return ( + !!prop && + typeof prop.not === 'object' && + prop.not !== null && + Object.keys(prop.not).length === 0 + ); +} + export function formatType(prop: any, ctx?: TypeContext): string { if (!prop) return 'any'; + // A `retiredKey()` tombstone. `never` is both the accurate TypeScript (the + // key's `z.input` type IS `never`) and the only rendering that survives the + // inline shape summary below, where there is no description column to carry + // the `[REMOVED]` prescription. Checked FIRST: `{ not: {} }` accepts nothing + // whatever else the node says, so no later branch can be more specific. + if (isNeverNode(prop)) return 'never'; + if (prop.$ref) { // Self-reference: link to the current section rather than a bare `#`. if (prop.$ref === '#') { @@ -137,7 +169,21 @@ export function formatType(prop: any, ctx?: TypeContext): string { : null; // Inline object: show its shape one level deep instead of an opaque `Object`. - const keys = prop.properties ? Object.keys(prop.properties) : []; + // + // Tombstoned keys are dropped BEFORE `INLINE_KEY_LIMIT` is applied, not + // rendered as `never` and counted. They are not authorable surface any + // more, so spending one of the four slots on one — and pushing a key the + // author MUST write behind the `…` to afford it — sells a removed key in + // place of a live one. The elision cannot be worked around by ordering, + // either: #5248 retired `IndexSchema.type`/`.partial` down to three live + // keys, so with a limit of four the first tombstone is *mathematically* + // guaranteed into the summary however low in the shape it sits (#5606). + // Their own table row still carries the `[REMOVED]` prescription wherever + // the shape is a named schema; a summary cell has no description column to + // carry it at all. + const keys = prop.properties + ? Object.keys(prop.properties).filter(k => !isNeverNode(prop.properties[k])) + : []; if (keys.length > 0) { const shown = keys.slice(0, INLINE_KEY_LIMIT).map(k => { @@ -149,8 +195,11 @@ export function formatType(prop: any, ctx?: TypeContext): string { : formatType(child, ctx); return `${k}${optional}: ${childType}`; }); - // `…` elides further DECLARED keys; `& Record<…>` states that UNDECLARED - // ones are accepted. Different facts — a cell may need both. + // `…` elides further LIVE declared keys; `& Record<…>` states that + // UNDECLARED ones are accepted. Different facts — a cell may need both. + // Tombstones are in neither set: they are declared and rejected, so a + // summary that ends without `…` now means "these are all the keys you may + // write", which is a stronger and truer claim than it used to be. if (keys.length > shown.length) shown.push('…'); const shape = `{ ${shown.join('; ')} }`; // Declared shape first: the reader needs the keys they MUST write before @@ -158,9 +207,11 @@ export function formatType(prop: any, ctx?: TypeContext): string { return open ? `${shape} & ${open}` : shape; } - // Nothing declared. An empty `properties: {}` is not a shape — intersecting - // it would print `{ } & Record<…>`, so fall through to the record/opaque - // renderings exactly as before. + // No LIVE key declared — either `properties: {}` outright, or a shape whose + // every declared key is now a tombstone. Both state the same authorable + // fact, and neither is a shape: intersecting one would print + // `{ } & Record<…>`, so fall through to the record/opaque renderings + // exactly as before. if (open) return open; if (!prop.properties) return 'object'; return '{ }'; From 1c8c497cc932f51238c977d67115110a93542d1c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 00:35:47 +0000 Subject: [PATCH 2/2] docs(spec): regenerate content/docs/references/** with the never-tombstone renderer (#5606) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated by `pnpm --filter @objectstack/spec gen:schema && gen:docs`. Do not hand-edit — regenerate instead. 30 reference pages, 117 lines: - 101 per-key tombstone rows: type cell `any` -> `never`, [REMOVED] prescriptions unchanged. - 16 inline summary cells: tombstones dropped before INLINE_KEY_LIMIT, e.g. Theme.typography.fontFamily `{ base?: string; heading?: any; mono?: any }` -> `{ base?: string }`, ObjectSchema.indexes drops `type?: any` and its trailing ellipsis. The three sharded artifact dirs (authorable-surface/, json-schema.manifest/, api-surface/) are byte-identical after gen:schema — this change does not reach them. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --- content/docs/references/ai/agent.mdx | 4 +- content/docs/references/ai/skill.mdx | 2 +- content/docs/references/api/analytics.mdx | 4 +- .../docs/references/api/automation-api.mdx | 4 +- content/docs/references/api/batch.mdx | 10 +-- content/docs/references/api/contract.mdx | 8 +-- content/docs/references/api/errors.mdx | 2 +- content/docs/references/api/metadata.mdx | 2 +- content/docs/references/api/protocol.mdx | 6 +- content/docs/references/api/rest-server.mdx | 4 +- .../references/automation/control-flow.mdx | 2 +- content/docs/references/automation/flow.mdx | 8 +-- .../automation/schemaless-node-config.mdx | 10 +-- content/docs/references/data/data-engine.mdx | 4 +- content/docs/references/data/driver-nosql.mdx | 2 +- content/docs/references/data/driver-sql.mdx | 2 +- content/docs/references/data/driver.mdx | 64 +++++++++---------- content/docs/references/data/field.mdx | 2 +- content/docs/references/data/object.mdx | 8 +-- content/docs/references/data/query.mdx | 8 +-- .../docs/references/integration/connector.mdx | 4 +- content/docs/references/security/rls.mdx | 2 +- content/docs/references/system/book.mdx | 4 +- content/docs/references/system/job.mdx | 2 +- content/docs/references/ui/action.mdx | 6 +- content/docs/references/ui/app.mdx | 16 ++--- content/docs/references/ui/dashboard.mdx | 14 ++-- content/docs/references/ui/theme.mdx | 16 ++--- content/docs/references/ui/view.mdx | 12 ++-- content/docs/references/ui/widget.mdx | 2 +- 30 files changed, 117 insertions(+), 117 deletions(-) diff --git a/content/docs/references/ai/agent.mdx b/content/docs/references/ai/agent.mdx index f8f1930a0f..7feb552bfd 100644 --- a/content/docs/references/ai/agent.mdx +++ b/content/docs/references/ai/agent.mdx @@ -53,8 +53,8 @@ const result = AIModelConfigSchema.parse(data); | **lifecycle** | `{ id: string; description?: string; contextSchema?: Record; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). | | **surface** | `Enum<'ask' \| 'build'>` | ✅ | Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 | | **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) | -| **tools** | `any` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 (#3894) — use `skills`. An agent reaches exactly the tools its surface-compatible skills declare (ADR-0064), so move each reference into a skill: a platform tool by its registered name, or `action_` for one of your own AI-exposed Actions. Run `os migrate meta --from 16` to rewrite it automatically. | -| **knowledge** | `any` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — declaring knowledge sources/indexes on an agent never scoped retrieval: the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from the agent record. Delete the block. Restrict retrieval at the knowledge-service / source level (per-source permissions), and describe intended grounding in `instructions` so the model asks for the right sources. | +| **tools** | `never` | optional | [REMOVED] `agent.tools` was removed in @objectstack/spec 17 (#3894) — use `skills`. An agent reaches exactly the tools its surface-compatible skills declare (ADR-0064), so move each reference into a skill: a platform tool by its registered name, or `action_` for one of your own AI-exposed Actions. Run `os migrate meta --from 16` to rewrite it automatically. | +| **knowledge** | `never` | optional | [REMOVED] `agent.knowledge` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — declaring knowledge sources/indexes on an agent never scoped retrieval: the `search_knowledge` tool takes `sourceIds` from the LLM's tool-call arguments, not from the agent record. Delete the block. Restrict retrieval at the knowledge-service / source level (per-source permissions), and describe intended grounding in `instructions` so the model asks for the right sources. | | **active** | `boolean` | ✅ | | | **access** | `string[]` | optional | Who can chat with this agent | | **permissions** | `string[]` | optional | Required permission-set capabilities | diff --git a/content/docs/references/ai/skill.mdx b/content/docs/references/ai/skill.mdx index ec4ee772e2..ac013a9b1b 100644 --- a/content/docs/references/ai/skill.mdx +++ b/content/docs/references/ai/skill.mdx @@ -39,7 +39,7 @@ const result = SkillSchema.parse(data); | **surface** | `Enum<'ask' \| 'build' \| 'both'>` | ✅ | Agent surface this skill binds to ('ask' \| 'build' \| 'both') — ADR-0063 §3 | | **instructions** | `string` | optional | LLM instructions when skill is active | | **tools** | `string[]` | ✅ | Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) | -| **triggerPhrases** | `any` | optional | [REMOVED] `skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — phrases were never matched against the user's message; skill activation is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in `triggerConditions`; describe intent in `description`/`instructions` for the LLM. | +| **triggerPhrases** | `never` | optional | [REMOVED] `skill.triggerPhrases` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — phrases were never matched against the user's message; skill activation is `triggerConditions` (AND of context field/operator/value) intersected with the agent's `skills[]`, plus explicit /skill-name pinning. Delete the key. Put routing intent in `triggerConditions`; describe intent in `description`/`instructions` for the LLM. | | **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions | | **active** | `boolean` | ✅ | Whether the skill is enabled | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. | diff --git a/content/docs/references/api/analytics.mdx b/content/docs/references/api/analytics.mdx index c0684b35da..a1ad133aae 100644 --- a/content/docs/references/api/analytics.mdx +++ b/content/docs/references/api/analytics.mdx @@ -67,8 +67,8 @@ const result = AnalyticsEndpoint.parse(data); | **limit** | `number` | optional | | | **offset** | `number` | optional | | | **timezone** | `string` | optional | | -| **query** | `any` | optional | [REMOVED] `query` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). The `{ cube, query: {...} }` envelope was the dialect of the retired degraded analytics shim (#3891) — the real engine never understood it. Move the query.* fields to the body top level: `{ cube, measures, dimensions?, where?, timeDimensions?, order?, limit?, offset?, timezone? }`. | -| **format** | `any` | optional | [REMOVED] `format` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). It was never implemented — every response is the JSON envelope. Delete the key; for CSV/XLSX use the export surface instead. | +| **query** | `never` | optional | [REMOVED] `query` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). The `{ cube, query: {...} }` envelope was the dialect of the retired degraded analytics shim (#3891) — the real engine never understood it. Move the query.* fields to the body top level: `{ cube, measures, dimensions?, where?, timeDimensions?, order?, limit?, offset?, timezone? }`. | +| **format** | `never` | optional | [REMOVED] `format` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). It was never implemented — every response is the JSON envelope. Delete the key; for CSV/XLSX use the export surface instead. | --- diff --git a/content/docs/references/api/automation-api.mdx b/content/docs/references/api/automation-api.mdx index 8416442b8e..a307e3a43a 100644 --- a/content/docs/references/api/automation-api.mdx +++ b/content/docs/references/api/automation-api.mdx @@ -102,12 +102,12 @@ const result = AutomationApiErrorCode.parse(data); | **errorMessage** | `string` | optional | Toast shown when a screen flow fails (defaults to the raw error). | | **version** | `integer` | optional | Version number | | **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Deployment status | -| **template** | `any` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | | **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | | **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables | | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | | **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | ✅ | Flow connections | -| **active** | `any` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | | **runAs** | `Enum<'system' \| 'user'>` | optional | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | | **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | diff --git a/content/docs/references/api/batch.mdx b/content/docs/references/api/batch.mdx index 90bdb89aa2..1e836ef252 100644 --- a/content/docs/references/api/batch.mdx +++ b/content/docs/references/api/batch.mdx @@ -47,7 +47,7 @@ const result = BatchConfigSchema.parse(data); | :--- | :--- | :--- | :--- | | **enabled** | `boolean` | ✅ | Enable batch operations | | **maxRecordsPerBatch** | `integer` | ✅ | Maximum records per batch | -| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Default batch options | +| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Default batch options | --- @@ -89,7 +89,7 @@ const result = BatchConfigSchema.parse(data); | **atomic** | `boolean` | ✅ | Opt-in all-or-nothing. When explicitly true the whole batch runs inside ONE engine transaction: the first failure rolls back every prior write, and the response reports zero successes — each row carries `errors[0].code` ROLLED_BACK (written, then undone), the causal row its own error, and rows never reached NOT_ATTEMPTED. A runtime that cannot roll back REFUSES the request (501 NOT_IMPLEMENTED) rather than silently degrading to best-effort — probe `capabilities.transactionalBatch` on /discovery first. Takes precedence over continueOnError. Default false: sequential best-effort. | | **returnRecords** | `boolean` | ✅ | If true, return full record data in response | | **continueOnError** | `boolean` | ✅ | If true (and atomic=false), continue processing remaining records after errors | -| **validateOnly** | `any` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | +| **validateOnly** | `never` | optional | [REMOVED] `options.validateOnly` was removed from BatchOptions in @objectstack/spec (#4052). It was never implemented: the batch surfaces persisted regardless, so a "dry-run" would have silently executed. There is no dry-run today — drop the key. If you need to preview a batch without writing, open an issue so it can be designed (no-commit cascade / constraint semantics) and reintroduced as a flag that actually holds. | --- @@ -115,7 +115,7 @@ const result = BatchConfigSchema.parse(data); | :--- | :--- | :--- | :--- | | **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` | ✅ | Type of batch operation | | **records** | `{ id?: string; data?: Record; externalId?: string }[]` | ✅ | Array of records to process (server caps the count — see batch.maxBatchSize) | -| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Batch operation options | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Batch operation options | --- @@ -199,7 +199,7 @@ A cross-object batch strip event: dropped fields plus the operation index | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **ids** | `string[]` | ✅ | Array of record IDs to delete (server caps the count — see batch.maxBatchSize) | -| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Delete options | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Delete options | --- @@ -223,7 +223,7 @@ A cross-object batch strip event: dropped fields plus the operation index | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **records** | `{ id: string; data: Record }[]` | ✅ | Array of records to update (server caps the count — see batch.maxBatchSize) | -| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Update options | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Update options | --- diff --git a/content/docs/references/api/contract.mdx b/content/docs/references/api/contract.mdx index a0ca5ff398..18995193ca 100644 --- a/content/docs/references/api/contract.mdx +++ b/content/docs/references/api/contract.mdx @@ -155,13 +155,13 @@ const result = ApiErrorSchema.parse(data); | **limit** | `number` | optional | Max records to return (LIMIT) | | **offset** | `number` | optional | Records to skip (OFFSET) | | **top** | `number` | optional | Alias for limit (OData compatibility) | -| **cursor** | `any` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | -| **joins** | `any` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). | | **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions | | **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | | **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | -| **windowFunctions** | `any` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | -| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | | **expand** | `Record; … }; … }>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | diff --git a/content/docs/references/api/errors.mdx b/content/docs/references/api/errors.mdx index 95ecc6abd3..3923e27ec9 100644 --- a/content/docs/references/api/errors.mdx +++ b/content/docs/references/api/errors.mdx @@ -54,7 +54,7 @@ const result = EnhancedApiErrorSchema.parse(data); | **retryAfter** | `number` | optional | Seconds to wait before retrying | | **details** | `any` | optional | Additional error context | | **fields** | `{ field: string; code: Enum<'required' \| 'invalid_type' \| 'invalid_shape' \| 'unknown_field' \| 'invalid_boolean' \| 'invalid_number' \| 'invalid_date' \| 'invalid_time' \| 'invalid_email' \| 'invalid_url' \| 'invalid_phone' \| 'invalid_json' \| 'invalid_format' \| 'min_length' \| 'max_length' \| 'min_value' \| 'max_value' \| 'min_items' \| 'max_items' \| 'invalid_option' \| 'invalid_value' \| 'reference_not_found' \| 'reference_ambiguous' \| 'rule_violation' \| 'json_schema_violation' \| 'invalid_initial_state' \| 'invalid_transition'>; message: string; label?: string; … }[]` | optional | One entry per offending value | -| **fieldErrors** | `any` | optional | [REMOVED] `EnhancedApiError.fieldErrors` was renamed to `fields` in @objectstack/spec 17 (ADR-0114 D4, #3977) — the array is unchanged, only the property name. Every producer already emitted `fields`; `fieldErrors` was declared and never emitted, so a reader keying on it was reading a field no server sent. | +| **fieldErrors** | `never` | optional | [REMOVED] `EnhancedApiError.fieldErrors` was renamed to `fields` in @objectstack/spec 17 (ADR-0114 D4, #3977) — the array is unchanged, only the property name. Every producer already emitted `fields`; `fieldErrors` was declared and never emitted, so a reader keying on it was reading a field no server sent. | | **timestamp** | `string` | optional | When the error occurred | | **requestId** | `string` | optional | Request ID for tracking | | **traceId** | `string` | optional | Distributed trace ID | diff --git a/content/docs/references/api/metadata.mdx b/content/docs/references/api/metadata.mdx index 6b8c9932f6..8265892607 100644 --- a/content/docs/references/api/metadata.mdx +++ b/content/docs/references/api/metadata.mdx @@ -68,7 +68,7 @@ const result = AppDefinitionResponseSchema.parse(data); | **success** | `boolean` | ✅ | Operation success status | | **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| 'INVALID_FORMAT' \| 'VALUE_TOO_LONG' \| 'VALUE_TOO_SHORT' \| 'VALUE_OUT_OF_RANGE' \| 'INVALID_REFERENCE' \| 'DUPLICATE_VALUE' \| 'INVALID_QUERY' \| 'INVALID_FILTER' \| 'INVALID_SORT' \| 'MAX_RECORDS_EXCEEDED' \| 'UNAUTHENTICATED' \| 'INVALID_CREDENTIALS' \| 'EXPIRED_TOKEN' \| 'INVALID_TOKEN' \| 'SESSION_EXPIRED' \| 'MFA_REQUIRED' \| 'EMAIL_NOT_VERIFIED' \| 'PERMISSION_DENIED' \| 'INSUFFICIENT_PRIVILEGES' \| 'FIELD_NOT_ACCESSIBLE' \| 'RECORD_NOT_ACCESSIBLE' \| 'LICENSE_REQUIRED' \| 'IP_RESTRICTED' \| 'TIME_RESTRICTED' \| 'RESOURCE_NOT_FOUND' \| 'OBJECT_NOT_FOUND' \| 'RECORD_NOT_FOUND' \| 'FIELD_NOT_FOUND' \| 'ENDPOINT_NOT_FOUND' \| 'RESOURCE_CONFLICT' \| 'CONCURRENT_MODIFICATION' \| 'DELETE_RESTRICTED' \| 'DUPLICATE_RECORD' \| 'LOCK_CONFLICT' \| 'METHOD_NOT_ALLOWED' \| 'PRECONDITION_REQUIRED' \| 'RATE_LIMIT_EXCEEDED' \| 'QUOTA_EXCEEDED' \| 'CONCURRENT_LIMIT_EXCEEDED' \| 'INTERNAL_ERROR' \| 'DATABASE_ERROR' \| 'TIMEOUT' \| 'SERVICE_UNAVAILABLE' \| 'NOT_IMPLEMENTED' \| 'EXTERNAL_SERVICE_ERROR' \| 'INTEGRATION_ERROR' \| 'WEBHOOK_DELIVERY_FAILED' \| 'BATCH_PARTIAL_FAILURE' \| 'BATCH_COMPLETE_FAILURE' \| 'TRANSACTION_FAILED' \| 'ACCOUNT_LOCKED' \| 'ALREADY_REVERTED' \| 'AMBIGUOUS_MATCH' \| 'ANALYTICS_QUERY_FAILED' \| 'APPROVAL_ACTIONS_FAILED' \| 'APPROVAL_RECALL_FAILED' \| 'APPROVAL_REQUEST_GET_FAILED' \| 'APPROVAL_REQUEST_LIST_FAILED' \| 'ASYNC_NOT_SUPPORTED' \| 'ATTACHMENT_DELETE_DENIED' \| 'ATTACHMENT_DOWNLOAD_DENIED' \| 'ATTACHMENT_PARENT_ACCESS' \| 'AUDIENCE_NOT_ALLOWED' \| 'AUTH_CONFIG_ERROR' \| 'AUTH_REQUIRED' \| 'AUTOMATION_UNSCOPED_RUN_DATA_ACCESS' \| 'BATCH_ABORTED' \| 'BATCH_NOT_ATOMIC' \| 'BATCH_TOO_LARGE' \| 'BATCH_UNRESOLVED_REF' \| 'BLANK_MATCH_KEY' \| 'CLONE_DISABLED' \| 'CLOUD_FETCH_FAILED' \| 'CLOUD_UNCONFIGURED' \| 'COMMIT_NOT_FOUND' \| 'CONCURRENT_UPDATE' \| 'CONFLICTING_MAPPING' \| 'CONNECTOR_UPSTREAM_UNAVAILABLE' \| 'CREATE_FAILED' \| 'CUBE_NOT_FOUND' \| 'DATASET_INVALID' \| 'DATASOURCE_ADMIN_ERROR' \| 'DELEGABLE_SCOPE_FAILED' \| 'DELIVERY_NOT_ELIGIBLE' \| 'DESTRUCTIVE_CHANGE' \| 'DEVICE_CODE_FAILED' \| 'DOMAIN_VERIFICATION_DISABLED' \| 'DOMAIN_VERIFICATION_FAILED' \| 'DRIVER_UNAVAILABLE' \| 'DUPLICATE_REQUEST' \| 'EMAIL_SEND_FAILED' \| 'EMAIL_SERVICE_REQUIRED' \| 'ENQUEUE_FAILED' \| 'ENVIRONMENT_BIND_FAILED' \| 'ENVIRONMENT_NOT_FOUND' \| 'ENV_ACCESS_DENIED' \| 'ERR_BULK_RESULT_MISMATCH' \| 'ERR_DATASOURCE_UNAVAILABLE' \| 'ERR_DRIVER_CONNECT' \| 'ERR_FILE_CONSTRAINT' \| 'ERR_FILE_REFERENCE_COPY' \| 'ERR_READONLY_FIELD_REJECTED' \| 'ERR_SUMMARY_RECOMPUTE' \| 'EXECUTION_ERROR' \| 'EXPIRED_OR_REVOKED' \| 'EXPIRY_IN_PAST' \| 'EXPIRY_TOO_LONG' \| 'EXPLAIN_FAILED' \| 'EXPORT_NOT_PERMITTED' \| 'EXTERNAL_DATASOURCE_ERROR' \| 'EXTERNAL_IMPORT_ERROR' \| 'EXTERNAL_SCHEMA_MISMATCH' \| 'EXTERNAL_SCHEMA_MODE_VIOLATION' \| 'EXTERNAL_WRITE_FORBIDDEN' \| 'FEEDS_DISABLED' \| 'FILES_DISABLED' \| 'FILE_DOWNLOAD_DENIED' \| 'FILE_NOT_FOUND' \| 'FILTER_TOKEN_UNKNOWN' \| 'FILTER_TOKEN_UNRESOLVED' \| 'FORBIDDEN' \| 'FORM_NOT_FOUND' \| 'FORM_RESOLVE_FAILED' \| 'IMPORT_JOB_CREATE_FAILED' \| 'IMPORT_ROW_FAILED' \| 'INTERNAL' \| 'INVALID_EMAIL' \| 'INVALID_EXPIRY' \| 'INVALID_METADATA' \| 'INVALID_OR_EXPIRED' \| 'INVALID_PHONE' \| 'INVALID_REQUEST' \| 'INVALID_RESUME_TOKEN' \| 'INVALID_SIGNAL' \| 'INVALID_SIGNATURE' \| 'INVALID_STATE' \| 'INVITE_EMAIL_FAILED' \| 'INVITE_REQUIRES_EMAIL' \| 'INVITE_SMS_FAILED' \| 'IP_NOT_ALLOWED' \| 'ITEM_LOCKED' \| 'LAST_LOCAL_CREDENTIAL' \| 'LOOKUP_NOT_PUBLIC' \| 'LOOKUP_TARGET_MISSING' \| 'MANIFEST_CONFLICT' \| 'MAPPING_FORMAT_MISMATCH' \| 'MAPPING_FORMAT_UNSUPPORTED' \| 'MAPPING_NOT_FOUND' \| 'MAPPING_TARGET_MISMATCH' \| 'MARKETPLACE_PROXY_FAILED' \| 'MARKETPLACE_STORAGE_FAILED' \| 'MARKETPLACE_UNAVAILABLE' \| 'METADATA_BRANCH' \| 'METADATA_CONFLICT' \| 'METADATA_NOT_FOUND' \| 'METADATA_SCHEMA_INVALID' \| 'MONGODB_MULTI_TENANT_UNSUPPORTED' \| 'NAMESPACE_PREFIX' \| 'NEEDS_PASSWORD' \| 'NODE_FAILURE' \| 'NOTHING_TO_PURGE' \| 'NOT_ATTEMPTED' \| 'NOT_CREATABLE' \| 'NOT_FOUND' \| 'NOT_OVERRIDABLE' \| 'NOT_UNDOABLE' \| 'NO_DRAFT' \| 'NO_EXECUTOR' \| 'NO_IDENTITY' \| 'NO_MATCH' \| 'NO_PENDING_VERIFICATION' \| 'OAUTH_REGISTER_FAILED' \| 'OBJECT_API_DISABLED' \| 'OBJECT_API_METHOD_NOT_ALLOWED' \| 'OPENAPI_UNAVAILABLE' \| 'OS_PROTOCOL_INCOMPATIBLE' \| 'OVERLAY_PERSISTENCE_FAILED' \| 'PACKAGE_DELETE_FAILED' \| 'PACKAGE_DELETE_PARTIAL' \| 'PACKAGE_MANIFEST_INVALID' \| 'PACKAGE_PUBLISH_FAILED' \| 'PASSWORD_ALREADY_SET' \| 'PASSWORD_EXPIRED' \| 'PASSWORD_POLICY_VIOLATION' \| 'PASSWORD_REUSE' \| 'PAYLOAD_TOO_LARGE' \| 'PERMISSION_NOT_ALLOWED' \| 'PHONE_NOT_ENABLED' \| 'PLUGIN_INSTALL_FAILED' \| 'PLUGIN_MANIFEST_INVALID' \| 'PLUGIN_REGISTER_FAILED' \| 'PROJECT_MEMBERSHIP_REQUIRED' \| 'PROJECT_NOT_FOUND' \| 'PROJECT_PROVISIONING' \| 'PROJECT_PROVISIONING_FAILED' \| 'RAW_SQL_UNSUPPORTED' \| 'READ_SCOPE_COMPILE_FAILED' \| 'RECORD_GONE' \| 'RECORD_LOCKED' \| 'REPORTS_LIST_FAILED' \| 'REPORT_DELETE_FAILED' \| 'REPORT_GET_FAILED' \| 'REPORT_NOT_FOUND' \| 'REPORT_RUN_FAILED' \| 'REPORT_SAVE_FAILED' \| 'REPORT_SCHEDULE_FAILED' \| 'REQUEST_NOT_FOUND' \| 'RESEED_NO_ROWS' \| 'RESEED_SKIPPED' \| 'RESUME_FAILED' \| 'RESUME_IN_PROGRESS' \| 'RESUME_TARGET_LOST' \| 'ROLLED_BACK' \| 'ROUTE_NOT_FOUND' \| 'RULE_DEFINE_FAILED' \| 'RULE_DELETE_FAILED' \| 'RULE_EVALUATE_FAILED' \| 'RULE_GET_FAILED' \| 'RULE_LIST_FAILED' \| 'RULE_NOT_FOUND' \| 'RUN_NOT_FOUND' \| 'SAML_REGISTER_FAILED' \| 'SCHEDULES_LIST_FAILED' \| 'SCHEDULE_DELETE_FAILED' \| 'SETTINGS_ACTION_FAILED' \| 'SETTINGS_FORBIDDEN' \| 'SETTINGS_LOCKED' \| 'SETTINGS_UNKNOWN_KEY' \| 'SETTINGS_UNKNOWN_NAMESPACE' \| 'SETTINGS_VALIDATION' \| 'SHARES_LIST_FAILED' \| 'SHARE_GRANT_FAILED' \| 'SHARE_REVOKE_FAILED' \| 'SHARING_NOT_ENABLED' \| 'SIGN_IN_REQUIRED' \| 'SSO_REGISTER_FAILED' \| 'SSO_REGISTER_FORBIDDEN' \| 'STORE_UNAVAILABLE' \| 'SUGGESTION_CONFIRM_FAILED' \| 'SUGGESTION_DISMISS_FAILED' \| 'SUGGESTION_LIST_FAILED' \| 'SUGGESTION_NOT_FOUND' \| 'SUGGESTION_STATE' \| 'SUMMARY_RECOMPUTE_FAILED' \| 'UNAUTHORIZED' \| 'UNIQUE_VIOLATION' \| 'UNKNOWN_KEY' \| 'UNKNOWN_NAMESPACE' \| 'UNSUPPORTED' \| 'UNSUPPORTED_QUERY_PARAM' \| 'UNSUPPORTED_TRANSFORM' \| 'UPLOAD_SESSION_NOT_FOUND' \| 'USER_ALREADY_EXISTS' \| 'VALIDATION_FAILED' \| 'VERSION_NOT_FOUND' \| 'VERSION_NOT_RESTORABLE' \| 'WRITABLE_PACKAGE_REQUIRED' \| 'WRONG_PASSWORD'>; message: string; category?: string; httpStatus?: integer; … }` | optional | Error details if success is false | | **meta** | `{ timestamp: string; duration?: number; requestId?: string; traceId?: string }` | optional | Response metadata | -| **data** | `{ name: string; label: string; version?: any; description?: string; … }` | ✅ | Full App Configuration | +| **data** | `{ name: string; label: string; description?: string; icon?: string; … }` | ✅ | Full App Configuration | --- diff --git a/content/docs/references/api/protocol.mdx b/content/docs/references/api/protocol.mdx index 2b59cfed4d..bf42c05743 100644 --- a/content/docs/references/api/protocol.mdx +++ b/content/docs/references/api/protocol.mdx @@ -448,7 +448,7 @@ const result = AiAgentCapabilitiesSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **ids** | `string[]` | ✅ | Array of record IDs to delete (server caps the count — see batch.maxBatchSize) | -| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Delete options | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Delete options | | **object** | `string` | ✅ | Object name | @@ -1024,7 +1024,7 @@ Get package response | **skip** | `number` | optional | Records to skip (offset). | | **expand** | `string` | optional | Comma-separated list of lookup/master_detail field names to expand. Resolved to populate array and passed to the engine for batch $in expansion. | | **search** | `string` | optional | Full-text search query. | -| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | | **count** | `boolean` | optional | Include total count in response. | @@ -1550,7 +1550,7 @@ Uninstall package response | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | | **records** | `{ id: string; data: Record }[]` | ✅ | Array of records to update (server caps the count — see batch.maxBatchSize) | -| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Update options | +| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Update options | | **object** | `string` | ✅ | Object name | diff --git a/content/docs/references/api/rest-server.mdx b/content/docs/references/api/rest-server.mdx index e285023664..20d52779cb 100644 --- a/content/docs/references/api/rest-server.mdx +++ b/content/docs/references/api/rest-server.mdx @@ -164,7 +164,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **enableOpenApi** | `boolean` | ✅ | Enable OpenAPI 3.1 spec & docs viewer endpoints | | **enableProjectScoping** | `boolean` | ✅ | Enable project-scoped routing for data/meta/AI APIs | | **projectResolution** | `Enum<'required' \| 'optional' \| 'auto'>` | ✅ | Project ID resolution strategy | -| **requireAuth** | `any` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17 (#3963). Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. | +| **requireAuth** | `never` | optional | [REMOVED] `api.requireAuth` was removed in @objectstack/spec 17 (#3963). Anonymous access to object data is now always denied — auth is a kernel concern, not a deployment posture. Delete the key. To publish something publicly, declare it: a public form view (`sharing.allowAnonymous`), a share link, or `book.audience: 'public'` — each derives its own narrow authorization instead of opening the whole data plane. | | **documentation** | `{ enabled: boolean; title: string; description?: string; version?: string; … }` | optional | OpenAPI/Swagger documentation config | | **responseFormat** | `{ envelope: boolean; includeMetadata: boolean; includePagination: boolean }` | optional | Response format options | @@ -182,7 +182,7 @@ const result = BatchEndpointsConfigSchema.parse(data); | **metadata** | `{ prefix: string; enableCache: boolean; cacheTtl: integer; endpoints?: object }` | optional | Metadata endpoints configuration | | **batch** | `{ maxBatchSize: integer; enableBatchEndpoint: boolean; operations?: object; defaultAtomic: boolean }` | optional | Batch endpoints configuration | | **routes** | `{ includeObjects?: string[]; excludeObjects?: string[]; nameTransform: Enum<'none' \| 'plural' \| 'kebab-case' \| 'camelCase'>; overrides?: Record }> }` | optional | Route generation configuration | -| **openApi31** | `any` | optional | [REMOVED] `RestServerConfig.openApi31` was removed in @objectstack/spec 17 (#4579, ADR-0049) — no runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. | +| **openApi31** | `never` | optional | [REMOVED] `RestServerConfig.openApi31` was removed in @objectstack/spec 17 (#4579, ADR-0049) — no runtime ever read it: the REST server forwards only `api`/`crud`/`metadata`/`batch`/`routes`, and the served /openapi.json is the pre-generated contract enriched with the live server URL and the registered objects, so webhook/callback definitions declared here never appeared in it. Delete the key. Config-driven OpenAPI 3.1 webhooks/callbacks documentation is a new capability and must arrive via the enforce route of ADR-0049 (a new ADR), not by re-declaring the key; for a real outbound webhook use `Webhook` from `@objectstack/spec/automation`. | --- diff --git a/content/docs/references/automation/control-flow.mdx b/content/docs/references/automation/control-flow.mdx index ee7173642d..ed44f396c8 100644 --- a/content/docs/references/automation/control-flow.mdx +++ b/content/docs/references/automation/control-flow.mdx @@ -189,7 +189,7 @@ const result = FlowRegionSchema.parse(data); | **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | | **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | | **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | --- diff --git a/content/docs/references/automation/flow.mdx b/content/docs/references/automation/flow.mdx index 23890026d4..47adce029f 100644 --- a/content/docs/references/automation/flow.mdx +++ b/content/docs/references/automation/flow.mdx @@ -52,12 +52,12 @@ const result = FlowSchema.parse(data); | **errorMessage** | `string` | optional | Toast shown when a screen flow fails (defaults to the raw error). | | **version** | `integer` | optional | Version number | | **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Deployment status | -| **template** | `any` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | +| **template** | `never` | optional | [REMOVED] `flow.template` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no designer or engine path ever read it, so flagging a flow as a template/subflow did nothing. Delete the key. Shared logic is invoked via a subflow NODE referencing the flow by name. | | **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` | ✅ | Flow type | | **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables | | **nodes** | `{ id: string; type: string; label: string; config?: Record; … }[]` | ✅ | Flow nodes | | **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | ✅ | Flow connections | -| **active** | `any` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | +| **active** | `never` | optional | [REMOVED] `flow.active` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never had an effect: the engine arms flows from `status`, and `active: false` did NOT stop a flow (worse, the default read as disabled while the engine treated unset as enabled). Delete the key. Use `status: 'obsolete'` (or 'invalid') to unbind and disable a flow, `status: 'active'` to arm it. | | **runAs** | `Enum<'system' \| 'user'>` | optional | Execution identity for the run: system = elevated (bypasses RLS), user = the triggering user (RLS-respecting). A run with no trigger user has no identity to scope to, so under user its data operations are REFUSED — declare system to make the elevation explicit. This covers schedule/time-relative/api triggers AND any record-change flow fired by a write that carried no user. | | **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. | @@ -103,8 +103,8 @@ const result = FlowSchema.parse(data); | **position** | `{ x: number; y: number }` | optional | | | **timeoutMs** | `integer` | optional | Maximum execution time for this node in milliseconds | | **inputSchema** | `Record; required: boolean; description?: string }>` | optional | Input parameter schema for this node | -| **outputSchema** | `any` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. | -| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string; timeoutMs?: any; … }` | optional | Configuration for wait node event resumption | +| **outputSchema** | `never` | optional | [REMOVED] `flow.nodes[].outputSchema` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it was never validated: the engine does not check node outputs against it, so it documented a contract nothing enforced. Delete the key. Downstream nodes read prior outputs via expressions (`{{nodeId.field}}`) regardless of any declaration. | +| **waitEventConfig** | `{ eventType: Enum<'timer' \| 'signal' \| 'webhook' \| 'manual' \| 'condition'>; timerDuration?: string; signalName?: string }` | optional | Configuration for wait node event resumption | | **boundaryConfig** | `{ attachedToNodeId: string; eventType: Enum<'error' \| 'timer' \| 'signal' \| 'cancel'>; interrupting: boolean; errorCode?: string; … }` | optional | Configuration for boundary events attached to host nodes | diff --git a/content/docs/references/automation/schemaless-node-config.mdx b/content/docs/references/automation/schemaless-node-config.mdx index cd7b12846d..357e866ad7 100644 --- a/content/docs/references/automation/schemaless-node-config.mdx +++ b/content/docs/references/automation/schemaless-node-config.mdx @@ -211,11 +211,11 @@ const result = DecisionConditionSchema.parse(data); | **function** | `string` | ✅ | Registered function to call (defineStack(`{ functions }`)). Contractually pure — it returns a value a later declarative node persists | | **inputs** | `Record` | optional | Inputs passed to the function (values interpolate `{token}` templates) | | **outputVariable** | `string` | optional | Flow variable the function's return value is bound to | -| **actionType** | `any` | optional | [REMOVED] `script.config.actionType` was removed in @objectstack/spec 17 (#4343) — none of its values did what it said. The two built-ins were logger-backed stubs that recorded the intent and delivered nothing under any configuration, and every other value was a second spelling of `config.function`. Replace it per branch: for `email` use a `notify` node (it delivers through the messaging service — the in-app inbox by default, real email once `@objectstack/plugin-email` is installed); for `slack` use a `connector_action` node with the Slack connector, or an `http` node posting to a webhook; for anything else, move the name into `config.function`. Run `os migrate meta --from 16` to rewrite it automatically. | -| **template** | `any` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 (#4343) — it fed only the logger-backed `email`/`slack` stubs, which never rendered or sent a message, so no template id was ever resolved. Delete the key. A `notify` node carries its own `title`/`message`, and stored templates live in the messaging service (`sys_notification_template`), not on the node. Run `os migrate meta --from 16` to rewrite it automatically. | -| **recipients** | `any` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 (#4343) — the addresses were logged, never messaged: the `email`/`slack` branches it fed delivered nothing. Use a `notify` node, whose `recipients` (user ids, field refs or addresses) reach the messaging service for real. Run `os migrate meta --from 16` to rewrite it automatically. | -| **variables** | `any` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 (#4343) — it injected values into a template no side effect ever rendered. Delete the key. A `notify` node carries structured data in `payload`; a registered function takes it in `config.inputs`. Run `os migrate meta --from 16` to rewrite it automatically. | -| **script** | `any` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 (#4343) — the built-in runtime has no server-side JS sandbox, so an inline body was recognized and never executed: the node warned and completed as a no-op. Move the logic into a registered function (`defineStack({ functions })`) and name it in `config.function`. Run `os migrate meta --from 16` to rewrite it automatically. | +| **actionType** | `never` | optional | [REMOVED] `script.config.actionType` was removed in @objectstack/spec 17 (#4343) — none of its values did what it said. The two built-ins were logger-backed stubs that recorded the intent and delivered nothing under any configuration, and every other value was a second spelling of `config.function`. Replace it per branch: for `email` use a `notify` node (it delivers through the messaging service — the in-app inbox by default, real email once `@objectstack/plugin-email` is installed); for `slack` use a `connector_action` node with the Slack connector, or an `http` node posting to a webhook; for anything else, move the name into `config.function`. Run `os migrate meta --from 16` to rewrite it automatically. | +| **template** | `never` | optional | [REMOVED] `script.config.template` was removed in @objectstack/spec 17 (#4343) — it fed only the logger-backed `email`/`slack` stubs, which never rendered or sent a message, so no template id was ever resolved. Delete the key. A `notify` node carries its own `title`/`message`, and stored templates live in the messaging service (`sys_notification_template`), not on the node. Run `os migrate meta --from 16` to rewrite it automatically. | +| **recipients** | `never` | optional | [REMOVED] `script.config.recipients` was removed in @objectstack/spec 17 (#4343) — the addresses were logged, never messaged: the `email`/`slack` branches it fed delivered nothing. Use a `notify` node, whose `recipients` (user ids, field refs or addresses) reach the messaging service for real. Run `os migrate meta --from 16` to rewrite it automatically. | +| **variables** | `never` | optional | [REMOVED] `script.config.variables` was removed in @objectstack/spec 17 (#4343) — it injected values into a template no side effect ever rendered. Delete the key. A `notify` node carries structured data in `payload`; a registered function takes it in `config.inputs`. Run `os migrate meta --from 16` to rewrite it automatically. | +| **script** | `never` | optional | [REMOVED] `script.config.script` was removed in @objectstack/spec 17 (#4343) — the built-in runtime has no server-side JS sandbox, so an inline body was recognized and never executed: the node warned and completed as a no-op. Move the logic into a registered function (`defineStack({ functions })`) and name it in `config.function`. Run `os migrate meta --from 16` to rewrite it automatically. | --- diff --git a/content/docs/references/data/data-engine.mdx b/content/docs/references/data/data-engine.mdx index 1f6c300fa6..aac0fde689 100644 --- a/content/docs/references/data/data-engine.mdx +++ b/content/docs/references/data/data-engine.mdx @@ -521,11 +521,11 @@ QueryAST-aligned query options for IDataEngine.find() operations | **limit** | `number` | optional | | | **offset** | `number` | optional | | | **top** | `number` | optional | | -| **cursor** | `any` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | | **search** | `{ query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }` | optional | | | **searchFields** | `string[]` | optional | | | **expand** | `Record; … }; … }>` | optional | | -| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | --- diff --git a/content/docs/references/data/driver-nosql.mdx b/content/docs/references/data/driver-nosql.mdx index 57a886ffe9..b1d788c6d1 100644 --- a/content/docs/references/data/driver-nosql.mdx +++ b/content/docs/references/data/driver-nosql.mdx @@ -123,7 +123,7 @@ const result = AggregationPipelineSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `'nosql'` | ✅ | Driver type must be "nosql" | -| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | | **databaseType** | `Enum<'mongodb' \| 'couchdb' \| 'dynamodb' \| 'cassandra' \| 'redis' \| 'elasticsearch' \| 'neo4j' \| 'orientdb'>` | ✅ | Specific NoSQL database type | diff --git a/content/docs/references/data/driver-sql.mdx b/content/docs/references/data/driver-sql.mdx index 3fb2bf90d3..65547ac09c 100644 --- a/content/docs/references/data/driver-sql.mdx +++ b/content/docs/references/data/driver-sql.mdx @@ -65,7 +65,7 @@ const result = DataTypeMappingSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `'sql'` | ✅ | Driver type must be "sql" | -| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | | **dialect** | `Enum<'postgresql' \| 'mysql' \| 'sqlite' \| 'mssql' \| 'oracle' \| 'mariadb'>` | ✅ | SQL database dialect | diff --git a/content/docs/references/data/driver.mdx b/content/docs/references/data/driver.mdx index 1314091a62..9b88c26b23 100644 --- a/content/docs/references/data/driver.mdx +++ b/content/docs/references/data/driver.mdx @@ -34,37 +34,37 @@ const result = DriverCapabilitiesSchema.parse(data); | **queryDateGranularity** | `Record` | optional | Per-granularity native date bucketing (day/week/month/quarter/year). Missing keys fall back to in-memory bucketing. | | **autonumber** | `boolean` | optional | Driver natively generates persistent autonumber/sequence values | | **batchSchemaSync** | `boolean` | optional | Supports batched schema sync to reduce schema DDL round-trips (absence = false) | -| **create** | `any` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | -| **read** | `any` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | -| **update** | `any` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | -| **delete** | `any` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | -| **bulkCreate** | `any` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | -| **bulkUpdate** | `any` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | -| **bulkDelete** | `any` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | -| **transactions** | `any` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | -| **savepoints** | `any` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | -| **isolationLevels** | `any` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | -| **queryFilters** | `any` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | -| **querySorting** | `any` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | -| **queryPagination** | `any` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | -| **queryAggregations** | `any` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | -| **queryWindowFunctions** | `any` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | -| **querySubqueries** | `any` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | -| **queryCTE** | `any` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | -| **joins** | `any` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | -| **fullTextSearch** | `any` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | -| **jsonQuery** | `any` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | -| **geospatialQuery** | `any` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | -| **streaming** | `any` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | -| **jsonFields** | `any` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | -| **arrayFields** | `any` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | -| **vectorSearch** | `any` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | -| **schemaSync** | `any` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | -| **migrations** | `any` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | -| **indexes** | `any` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | -| **connectionPooling** | `any` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | -| **preparedStatements** | `any` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | -| **queryCache** | `any` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | +| **create** | `never` | optional | [REMOVED] `DriverCapabilities.create` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `create`/`find`/`findOne`/`update`/`delete` are REQUIRED `IDataDriver` methods and the engine calls them unconditionally. Delete the key. | +| **read** | `never` | optional | [REMOVED] `DriverCapabilities.read` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: reads go through the REQUIRED `find`/`findOne`/`count` methods, called unconditionally. Delete the key. | +| **update** | `never` | optional | [REMOVED] `DriverCapabilities.update` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `update`/`upsert` are REQUIRED `IDataDriver` methods, called unconditionally. Delete the key. | +| **delete** | `never` | optional | [REMOVED] `DriverCapabilities.delete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. CRUD is not optional for a driver: `delete` is a REQUIRED `IDataDriver` method, called unconditionally. Delete the key. | +| **bulkCreate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkCreate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods (`bulkCreate`/`bulkUpdate`/`bulkDelete`) are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkUpdate** | `never` | optional | [REMOVED] `DriverCapabilities.bulkUpdate` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **bulkDelete** | `never` | optional | [REMOVED] `DriverCapabilities.bulkDelete` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. The bulk methods are REQUIRED `IDataDriver` methods and the engine calls them directly; wire-level batch capability is advertised by REST discovery from the live composition (#3298), never from this record. Delete the key. | +| **transactions** | `never` | optional | [REMOVED] `DriverCapabilities.transactions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Transaction use is gated on METHOD PRESENCE — `driver.beginTransaction` (`engine.transaction()`, ADR-0034 ambient transactions): a driver without the method gets the non-transactional fallback, whatever this bit claimed. Discovery's `transactionalBatch` capability is likewise derived from `engine.transaction` plus the mounted batch route, never from this bit. Delete the key. | +| **savepoints** | `never` | optional | [REMOVED] `DriverCapabilities.savepoints` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No savepoint code path exists in the engine — a capability bit for a feature the platform does not call is a false affordance, not documentation. Delete the key. | +| **isolationLevels** | `never` | optional | [REMOVED] `DriverCapabilities.isolationLevels` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Isolation is requested per transaction via `beginTransaction({ isolationLevel })`; no planner ever consulted this list to decide anything. Delete the key. | +| **queryFilters** | `never` | optional | [REMOVED] `DriverCapabilities.queryFilters` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST (`where`/`orderBy`/`limit`/`offset`) and MUST execute all of it — the "ObjectQL will filter in memory" fallback this bit's description promised was never built. Delete the key. | +| **querySorting** | `never` | optional | [REMOVED] `DriverCapabilities.querySorting` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will sort in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryPagination** | `never` | optional | [REMOVED] `DriverCapabilities.queryPagination` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `find()` receives the full QueryAST and MUST execute all of it — the "ObjectQL will paginate in memory" fallback this bit's description promised was never built. Delete the key. | +| **queryAggregations** | `never` | optional | [REMOVED] `DriverCapabilities.queryAggregations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Aggregate pushdown is decided by `typeof driver.aggregate === 'function'` plus `queryDateGranularity` (engine aggregate dispatch) — never by this bit. Delete the key. | +| **queryWindowFunctions** | `never` | optional | [REMOVED] `DriverCapabilities.queryWindowFunctions` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans window functions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **querySubqueries** | `never` | optional | [REMOVED] `DriverCapabilities.querySubqueries` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans subqueries through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **queryCTE** | `never` | optional | [REMOVED] `DriverCapabilities.queryCTE` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. ObjectQL never plans Common Table Expressions through a driver, so there was nothing for the bit to switch on. Delete the key. | +| **joins** | `never` | optional | [REMOVED] `DriverCapabilities.joins` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Related data is resolved by the engine (lookup expansion over `find()`), not by driver-side JOIN planning — no code consulted the bit. Delete the key. | +| **fullTextSearch** | `never` | optional | [REMOVED] `DriverCapabilities.fullTextSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. `$search` is compiled by the engine into an `$or` of `$contains` predicates over the searchable fields (ADR-0061) and removed from the AST before the driver sees it — no driver-side full-text path exists. Delete the key. | +| **jsonQuery** | `never` | optional | [REMOVED] `DriverCapabilities.jsonQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No engine path ever branched on driver-side JSON querying. Delete the key. | +| **geospatialQuery** | `never` | optional | [REMOVED] `DriverCapabilities.geospatialQuery` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No geospatial query path exists in the platform — declaring the bit advertised a capability nothing delivers. Delete the key. | +| **streaming** | `never` | optional | [REMOVED] `DriverCapabilities.streaming` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, and `findStream`, the only read this bit could describe, was itself removed in 17.0.0 (#4484): nothing ever called it, and two of its three implementations materialised the entire result set before yielding. The bit carried the same defect one level up (`SqlDriver` implemented `findStream` yet declared `streaming: false`; `InMemoryDriver` declared `true` over a full-table read) — which is what zero readers makes inevitable. Page large reads through `find()` with `limit`/`offset`. Delete the key. | +| **jsonFields** | `never` | optional | [REMOVED] `DriverCapabilities.jsonFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself (e.g. `SqlDriver`'s per-object JSON/date column tracking); no engine path consulted the bit. Delete the key. | +| **arrayFields** | `never` | optional | [REMOVED] `DriverCapabilities.arrayFields` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Field-type handling is negotiated per object at `syncSchema` time by the driver itself; no engine path consulted the bit. Delete the key. | +| **vectorSearch** | `never` | optional | [REMOVED] `DriverCapabilities.vectorSearch` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No vector read path routes through `IDataDriver`. When one exists it should arrive WITH its caller and its capability bit together (the honest order under enforce-or-remove), not as a dangling boolean. Delete the key. | +| **schemaSync** | `never` | optional | [REMOVED] `DriverCapabilities.schemaSync` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Schema sync is gated on METHOD PRESENCE — `typeof driver.syncSchema === 'function'` (engine and ObjectQL plugin init). Delete the key. | +| **migrations** | `never` | optional | [REMOVED] `DriverCapabilities.migrations` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No migration engine ever consulted it. Delete the key. | +| **indexes** | `never` | optional | [REMOVED] `DriverCapabilities.indexes` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Declared indexes are materialised by the driver itself during schema sync (`SqlDriver.syncDeclaredIndexes`); no engine path consulted the bit. Delete the key. | +| **connectionPooling** | `never` | optional | [REMOVED] `DriverCapabilities.connectionPooling` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Pooling is configured via `poolConfig` and owned by the driver; `getPoolStats` is duck-typed where monitoring wants it. Nothing consulted the bit. Delete the key. | +| **preparedStatements** | `never` | optional | [REMOVED] `DriverCapabilities.preparedStatements` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. Parameterised execution is an implementation detail of the driver (`execute(command, parameters)`); nothing consulted the bit. Delete the key. | +| **queryCache** | `never` | optional | [REMOVED] `DriverCapabilities.queryCache` was removed in @objectstack/spec 17.0.0 (#4634, ADR-0049 enforce-or-remove) — no code in any repository ever read it, so its value never changed which code path ran. No query-cache layer keyed off it exists; `DriverOptions.skipCache` is a per-call hint to the driver, not a switch on this bit. Delete the key. | --- @@ -77,7 +77,7 @@ const result = DriverCapabilitiesSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | Driver instance name | | **type** | `Enum<'sql' \| 'nosql' \| 'cache' \| 'search' \| 'graph' \| 'timeseries'>` | ✅ | Driver type category | -| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean; create?: any; … }` | ✅ | Driver capability flags | +| **capabilities** | `{ queryDateGranularity?: Record; autonumber?: boolean; batchSchemaSync?: boolean }` | ✅ | Driver capability flags | | **connectionString** | `string` | optional | Database connection string (driver-specific format) | | **poolConfig** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool configuration | diff --git a/content/docs/references/data/field.mdx b/content/docs/references/data/field.mdx index 74f76ecaab..67317405d1 100644 --- a/content/docs/references/data/field.mdx +++ b/content/docs/references/data/field.mdx @@ -119,7 +119,7 @@ const result = AddressSchema.parse(data); | **visibleWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is shown only when TRUE (else hidden). e.g. P`record.type == 'invoice'` | | **readonlyWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is read-only when TRUE. e.g. P`record.status == 'paid'` | | **requiredWhen** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Predicate (CEL) — field is required when TRUE. The only slot; the `conditionalRequired` alias was removed in protocol 17 (#3855). | -| **conditionalRequired** | `any` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | +| **conditionalRequired** | `never` | optional | [REMOVED] `conditionalRequired` was removed in @objectstack/spec 17 (#3855) — use `requiredWhen`. Rename the key; the value (a CEL predicate) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | | **widget** | `string` | optional | Form widget override — names a registered field component (resolved as `field:`) to render this field instead of the `type` default. Degrades to the `type` renderer when unregistered. e.g. "object-ref", "filter-condition", "recipient-picker". | | **hidden** | `boolean` | optional | Hidden from default UI | | **readonly** | `boolean` | optional | Read-only — never editable in forms, AND server-enforced on BOTH write paths: a non-system write to this field is silently dropped from the payload on UPDATE (#2948/#3003) and on INSERT (#3043; a create can no longer directly seed e.g. `approval_status: "approved"`), symmetric with `readonlyWhen`. A stripped INSERT field still falls back to its `defaultValue`. Exempt from the strip: `isSystem` writes (seed replay, migration), and an opt-in "historical" import (`preserveAudit`, #3493) — which admits a whitelist (the audit/timestamp family plus author-declared business `readonly` fields). A normal (non-system) import is NOT system-context and still strips. | diff --git a/content/docs/references/data/object.mdx b/content/docs/references/data/object.mdx index cc913a50b0..aa8ab116b9 100644 --- a/content/docs/references/data/object.mdx +++ b/content/docs/references/data/object.mdx @@ -68,8 +68,8 @@ const result = ApiMethod.parse(data); | **name** | `string` | optional | Index name (auto-generated if not provided) | | **fields** | `string[]` | ✅ | Fields included in the index | | **unique** | `boolean \| 'global' \| 'organization'` | ✅ | Whether the index enforces uniqueness, and at which scope (ADR-0120). 'global' = materialized over exactly `fields`, no organization column injected — one holder across the whole installation; 'organization' = the driver prepends the NULL-safe organization key part (COALESCE(organization_id, '__global__')) at registration — one holder per organization; bare true = deprecated positional spelling of 'global' (warned in 17.x by lint unique/unscoped-declared-index, rejected at protocol 18, #5082) — state the scope. 'tenant'/'org' are rejected — the word is 'organization' | -| **type** | `any` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to rewrite it automatically. | -| **partial** | `any` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite it automatically. | +| **type** | `never` | optional | [REMOVED] `indexes[].type` was removed in @objectstack/spec 17.0.0 (#5248, ADR-0049) — no driver ever read it. `SqlDriver.syncDeclaredIndexes` creates every declared index through knex's `table.index()` / `table.unique()`, which cannot express an access method, so the value changed no DDL; its `.default('btree')` merely made an inert knob show up in every parse output. Delete the key. The index method is the driver/dialect's decision (Postgres defaults to B-tree; `gin`/`gist`/`fulltext` are dialect-specific and are chosen by a database-layer migration when a workload actually needs one). Run `os migrate meta --from 16` to rewrite it automatically. | +| **partial** | `never` | optional | [REMOVED] `indexes[].partial` was removed in @objectstack/spec 17.0.0 (#5248, #4943, ADR-0049) — no driver ever emitted the `WHERE` clause, so a declared partial index was materialized as a FULL index and the predicate silently did nothing. Delete the key. Partial indexes are built at the database layer, not the declaration surface: issue `CREATE [UNIQUE] INDEX … WHERE ` from a runtime migration (this is what `metadata-protocol`'s `ensureOverlayIndex` already does for `sys_metadata`). Drift detection is unaffected — it reads partiality back from the database's own DDL, never from this key. Run `os migrate meta --from 16` to rewrite it automatically. | --- @@ -122,7 +122,7 @@ const result = ApiMethod.parse(data); | **datasource** | `string` | optional | Target Datasource ID. "default" is the primary DB. | | **external** | `{ remoteName?: string; remoteSchema?: string; writable?: boolean; columnMap?: Record; … }` | optional | Remote table binding for federated (external) objects. | | **fields** | `Record; description?: string; … }>` | ✅ | Field definitions map. Keys must be snake_case identifiers. | -| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization'; type?: any; … }[]` | optional | Database performance indexes | +| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Database performance indexes | | **fieldGroups** | `{ key: string; label: string; icon?: string; description?: string; … }[]` | optional | Ordered list of field groups (array order = display order). See ObjectFieldGroupSchema. | | **tenancy** | `{ enabled: boolean; tenantField?: string }` | optional | Multi-tenancy configuration for SaaS applications | | **access** | `{ default?: Enum<'public' \| 'private'> }` | optional | [ADR-0066 D2] Object exposure posture (public-by-default vs private secure-by-default). | @@ -196,7 +196,7 @@ const result = ApiMethod.parse(data); | **pluralLabel** | `string` | optional | Override plural label for the extended object | | **description** | `string` | optional | Override description for the extended object | | **validations** | `any[]` | optional | Additional validation rules to merge into the target object | -| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization'; type?: any; … }[]` | optional | Additional indexes to merge into the target object | +| **indexes** | `{ name?: string; fields: string[]; unique?: boolean \| 'global' \| 'organization' }[]` | optional | Additional indexes to merge into the target object | | **priority** | `integer` | optional | Merge priority (higher = applied later) | diff --git a/content/docs/references/data/query.mdx b/content/docs/references/data/query.mdx index 946427ed0e..4604f30937 100644 --- a/content/docs/references/data/query.mdx +++ b/content/docs/references/data/query.mdx @@ -134,13 +134,13 @@ Type: `string` | **limit** | `number` | optional | Max records to return (LIMIT) | | **offset** | `number` | optional | Records to skip (OFFSET) | | **top** | `number` | optional | Alias for limit (OData compatibility) | -| **cursor** | `any` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | -| **joins** | `any` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). | +| **cursor** | `never` | optional | [REMOVED] `query.cursor` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no driver ever implemented keyset pagination, so the cursor was accepted and ignored and every page came back identical (a caller looping "until hasMore is false" never terminates). Delete the key; `QueryBuilder.cursor()` was removed with it. Express the keyset as an ordinary `where` predicate on your sort key — `where: { created_at: { $gt: last.created_at } }` with the matching `orderBy` — which every driver executes with canonicalised comparands. A first-class cursor, if ever built, will be a response-minted opaque token, not this caller-built record. | +| **joins** | `never` | optional | [REMOVED] `query.joins` was removed in @objectstack/spec 17 (#4286, ADR-0049) — no engine or driver ever read it: a query carrying `joins` behaved exactly as if the key were absent, while its name squatted on the reserved REST parameter set. Delete the key. Related records are read through `expand` — `expand: { owner: { object: 'user', fields: ['name'] } }` — which the engine resolves via batch $in queries, and a single related column is a dotted `fields` path (`fields: ['owner.name']`). | | **aggregations** | `{ function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max' \| 'count_distinct' \| 'array_agg' \| 'string_agg'>; field?: string; alias: string; distinct?: boolean; … }[]` | optional | Aggregation functions | | **groupBy** | `(string \| { field: string; dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; alias?: string })[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) | | **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after aggregation | -| **windowFunctions** | `any` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | -| **distinct** | `any` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | +| **windowFunctions** | `never` | optional | [REMOVED] `query.windowFunctions` was removed in @objectstack/spec 17 (#4286, ADR-0049) — `find()` never applied it: no engine or driver read the key on the query path, so every OVER clause it declared was silently dropped. Delete the key. Window functions are a SQL-driver capability behind `SqlDriver.findWithWindowFunctions(object, query)` (embedder-level; not on the `IDataDriver` contract or the REST surface); request-level analytics are `aggregations` + `groupBy`. | +| **distinct** | `never` | optional | [REMOVED] `query.distinct` was removed in @objectstack/spec 17 (#4286, ADR-0049 / ADR-0078) — no driver ever rendered SELECT DISTINCT; the flag's only observable effect was MIS-WIRED: the REST list path treated a distinct query as not countable and silently degraded `total`/`hasMore` to a page-local estimate while still returning duplicate rows. Delete the key; `QueryBuilder.distinct()` was removed with it, and the count suppression is gone (`total` is truthful again). For unique values of one column use the SQL/memory drivers' `distinct(object, field)` door; for unique combinations, `groupBy`; for a deduplicated count, the `count_distinct` aggregation. | | **expand** | `Record` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. | diff --git a/content/docs/references/integration/connector.mdx b/content/docs/references/integration/connector.mdx index 5a47d19f7a..92acd71b7c 100644 --- a/content/docs/references/integration/connector.mdx +++ b/content/docs/references/integration/connector.mdx @@ -181,7 +181,7 @@ Circuit breaker configuration | **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration | | **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules | | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) | -| **rateLimitConfig** | `any` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to rewrite it automatically. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to rewrite it automatically. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `number` | optional | Connection timeout in ms | | **requestTimeoutMs** | `number` | optional | Request timeout in ms | @@ -483,7 +483,7 @@ Connector type | **syncConfig** | `{ strategy?: Enum<'full' \| 'incremental' \| 'upsert' \| 'append_only'>; direction?: Enum<'import' \| 'export' \| 'bidirectional'>; schedule?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; realtimeSync?: boolean; … }` | optional | Data sync configuration | | **fieldMappings** | `{ source: string; target: string; transform?: { type: 'constant'; value: any } \| { type: 'cast'; targetType: Enum<'string' \| 'number' \| 'boolean' \| 'date'> } \| { type: 'lookup'; table: string; keyField: string; valueField: string } \| { type: 'javascript'; expression: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object } } \| { type: 'map'; mappings: Record }; defaultValue?: any; … }[]` | optional | Field mapping rules | | **webhooks** | `{ name: string; label?: string; object?: string; triggers?: Enum<'create' \| 'update' \| 'delete' \| 'bulk_update' \| 'bulk_delete'>[]; … }[]` | optional | Webhook configurations (not yet enforced — never read at registration; see #3197) | -| **rateLimitConfig** | `any` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to rewrite it automatically. | +| **rateLimitConfig** | `never` | optional | [REMOVED] `connector.rateLimitConfig` was removed in @objectstack/spec 17.0.0 (#4911, ADR-0049 D2) — the entire shape is gone, not just this key: `ConnectorRateLimitConfig` and its `RateLimitStrategy` enum were removed with it, because no outbound rate-limiting engine ever existed. The platform's only token bucket (runtime `security/rate-limit.ts`) throttles INBOUND requests to us; nothing throttled the calls a connector makes out, so every knob here was inert while reading like a configured cap. Delete the key. Do NOT substitute `shared` `RateLimitConfig` — that is the inbound limiter and would cap the wrong direction; until an outbound throttle exists, rate-limit at the connector provider or upstream gateway. Run `os migrate meta --from 16` to rewrite it automatically. | | **retryConfig** | `{ strategy?: Enum<'exponential_backoff' \| 'linear_backoff' \| 'fixed_delay' \| 'no_retry'>; maxAttempts?: number; initialDelayMs?: number; maxDelayMs?: number; … }` | optional | Retry configuration | | **connectionTimeoutMs** | `number` | optional | Connection timeout in ms | | **requestTimeoutMs** | `number` | optional | Request timeout in ms | diff --git a/content/docs/references/security/rls.mdx b/content/docs/references/security/rls.mdx index aa8b0cbc34..f30f1f2c37 100644 --- a/content/docs/references/security/rls.mdx +++ b/content/docs/references/security/rls.mdx @@ -221,7 +221,7 @@ const result = RLSEvaluationResultSchema.parse(data); | **check** | `string` | optional | Validation condition for INSERT/UPDATE (defaults to USING clause if not specified - enforced at application level) | | **positions** | `string[]` | optional | Positions this policy applies to (omit for all) | | **enabled** | `boolean` | ✅ | Whether this policy is active | -| **priority** | `any` | optional | [REMOVED] `rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0 (#3896 security audit). It never had an effect and could not: applicable policies OR-combine (most permissive wins), so there is no conflict to order. Delete the key — policy outcomes are unchanged. | +| **priority** | `never` | optional | [REMOVED] `rowLevelSecurity[].priority` was removed in @objectstack/spec 17.0.0 (#3896 security audit). It never had an effect and could not: applicable policies OR-combine (most permissive wins), so there is no conflict to order. Delete the key — policy outcomes are unchanged. | | **tags** | `string[]` | optional | Policy categorization tags | diff --git a/content/docs/references/system/book.mdx b/content/docs/references/system/book.mdx index 31c13fc8e9..c7f45aacd1 100644 --- a/content/docs/references/system/book.mdx +++ b/content/docs/references/system/book.mdx @@ -64,7 +64,7 @@ const result = BookSchema.parse(data); | **icon** | `string` | optional | | | **order** | `number` | optional | Orders books within the portal | | **audience** | `'org' \| 'public' \| { permissionSet: string }` | optional | Access audience; defaults to 'org' (inherits package grant) | -| **groups** | `{ key: string; label: string; translations?: any; order?: number; … }[]` | ✅ | The spine: ordered sections. Two levels total. | +| **groups** | `{ key: string; label: string; order?: number; include?: string \| { tag: string }; … }[]` | ✅ | The spine: ordered sections. Two levels total. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | | **_lockSource** | `Enum<'artifact' \| 'package' \| 'env-forced'>` | optional | Layer that set _lock (artifact \| package \| env-forced). | @@ -115,7 +115,7 @@ Type: `'public'` | :--- | :--- | :--- | :--- | | **key** | `string` | ✅ | Stable group key (used by overrides, deep links, explicit `doc.group`) | | **label** | `string` | ✅ | Section title — first-class, i18n-homed | -| **translations** | `any` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **translations** | `never` | optional | [REMOVED] Inline `translations` on a book (and on a book group) was removed in @objectstack/spec 17.0.0 (#4667, ADR-0049) — no resolver ever read it. The book tree endpoint and the docs portal render `label` / `description` verbatim in every locale, so a localized book shipped its authoring-locale strings to every reader. Delete the key. NOTE the near neighbour that DOES work: `doc.translations` is live and read on every doc render path — localize the docs themselves, and the portal picks the reader's locale up from there. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **order** | `number` | optional | Order of THIS group within the book | | **include** | `string \| { tag: string }` | optional | Rule that derives membership (glob or tag) | | **package** | `string` | optional | Scope the rule to a package id (default: the book package; cross-package via ADR-0048) | diff --git a/content/docs/references/system/job.mdx b/content/docs/references/system/job.mdx index 7c40b68b69..3131b4335e 100644 --- a/content/docs/references/system/job.mdx +++ b/content/docs/references/system/job.mdx @@ -126,7 +126,7 @@ const result = CronScheduleSchema.parse(data); | **backoffMultiplier** | `number` | ✅ | Exponential backoff multiplier; 1 (the default) keeps the delay flat | | **maxRetryDelayMs** | `integer` | ✅ | Ceiling for a single backoff delay (ms) | | **jitter** | `boolean` | ✅ | Randomize each delay within [50%, 100%] of its computed value — spreads a thundering herd of simultaneous retries | -| **retryDelayMs** | `any` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | +| **retryDelayMs** | `never` | optional | [REMOVED] `retryDelayMs` was removed in @objectstack/spec 17.0.0 (#4661, #4964) — the retry policy now has ONE spelling for its base delay across every surface that carries it: `job.retryPolicy`, a `try_catch` node's `retry`, `flow.errorHandling` and an ETL pipeline's `retry`. Rename the key to `backoffMs`; the value (milliseconds before the first retry) is unchanged. `os migrate meta --from 16` rewrites it for you. | --- diff --git a/content/docs/references/ui/action.mdx b/content/docs/references/ui/action.mdx index 3f1c147929..89934e7481 100644 --- a/content/docs/references/ui/action.mdx +++ b/content/docs/references/ui/action.mdx @@ -87,7 +87,7 @@ const result = ActionSchema.parse(data); | **target** | `string` | optional | URL, Script Name, Flow ID, or API Endpoint. Supports $`{param.X}` and $`{ctx.X}` interpolation. | | **openIn** | `Enum<'self' \| 'new-tab'>` | optional | For type:'url' — where to open `target`. 'new-tab' opens a new browser tab; 'self' navigates in place. When omitted, external/absolute URLs open in a new tab and relative URLs navigate in place. Static execution option — keep it OUT of `params` (which is user-input-collection only). | | **body** | `{ language: 'expression'; source: string } \| { language: 'js'; source: string; capabilities?: Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'log'>[]; timeoutMs?: integer; … }` | optional | Action body — expression (L1) or sandboxed JS (L2). Only used when type is `script`. | -| **execute** | `any` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | +| **execute** | `never` | optional | [REMOVED] `execute` was removed in @objectstack/spec 17 (#3855) — use `target`. Rename the key; the value (a handler / flow / URL ref) is unchanged. Run `os migrate meta --from 16` to rewrite it automatically. | | **params** | `{ name?: string; field?: string; objectOverride?: string; label?: string; … }[]` | optional | Input parameters required from user | | **variant** | `Enum<'primary' \| 'secondary' \| 'danger' \| 'ghost' \| 'link'>` | optional | Button visual variant for styling (primary = highlighted, danger = destructive, ghost = transparent) | | **order** | `number` | optional | Sort order within a location group (lower = higher). Promotes/demotes an action toward the record_header primary button; stable, so actions without `order` keep their registration order. | @@ -101,8 +101,8 @@ const result = ActionSchema.parse(data); | **requiresFeature** | `Enum<'twoFactor' \| 'passkeys' \| 'magicLink' \| 'organization' \| 'multiOrgEnabled' \| 'degradedTenancy' \| 'oidcProvider' \| 'sso' \| 'ssoEnforced' \| 'deviceAuthorization' \| 'admin' \| 'phoneNumber' \| 'phoneNumberOtp'>` | optional | Public auth feature flag gating this action; lowered into `visible` at parse time. | | **disabled** | `boolean \| string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Boolean or predicate (CEL) — action is disabled when TRUE. | | **requiredPermissions** | `string[]` | optional | [ADR-0066 D4] Capabilities required to invoke this action. Enforced with 403 on the platform action route (script/flow/modal + MCP) and mirrored as a UI hide; a `type: api` action pointed at a custom endpoint must re-check it there. | -| **shortcut** | `any` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. | -| **bulkEnabled** | `any` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / `bulkActionDefs`, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's `bulkActions` instead. | +| **shortcut** | `never` | optional | [REMOVED] `action.shortcut` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — it never triggered anything: no keydown listener feeds ActionEngine.getShortcuts(), and objectui's keyboard stack (useKeyboardShortcuts) is hand-registered and never consults action metadata. Delete the key. For a real shortcut, register the key in the Console keyboard stack and have its handler invoke the action by name. | +| **bulkEnabled** | `never` | optional | [REMOVED] `action.bulkEnabled` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — the multi-select toolbar is driven by the LIST VIEW's `bulkActions` / `bulkActionDefs`, never by this flag, so setting it changed nothing. Delete the key and declare the action in the view's `bulkActions` instead. | | **ai** | `{ exposed?: boolean; description?: string; category?: Enum<'data' \| 'action' \| 'flow' \| 'integration' \| 'vector_search' \| 'analytics' \| 'utility'>; paramHints?: Record; … }` | optional | AI exposure (opt-in). Set ai.exposed=true + ai.description to make this callable by agents. | | **recordIdParam** | `string` | optional | Body key to inject the row id into when running from a list_item context. | | **recordIdField** | `string` | optional | Row field whose value seeds recordIdParam. Defaults to "id". | diff --git a/content/docs/references/ui/app.mdx b/content/docs/references/ui/app.mdx index 026d2b3300..e92d451e38 100644 --- a/content/docs/references/ui/app.mdx +++ b/content/docs/references/ui/app.mdx @@ -73,7 +73,7 @@ const result = ActionNavItemSchema.parse(data); | :--- | :--- | :--- | :--- | | **name** | `string` | ✅ | App unique machine name (lowercase snake_case) | | **label** | `string` | ✅ | App display label | -| **version** | `any` | optional | [REMOVED] `App.version` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no consumer in framework or objectui). An app is versioned by its owning package: use `manifest.version`. Delete the key. | +| **version** | `never` | optional | [REMOVED] `App.version` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no consumer in framework or objectui). An app is versioned by its owning package: use `manifest.version`. Delete the key. | | **description** | `string` | optional | App description | | **icon** | `string` | optional | App icon used in the App Launcher | | **branding** | `{ primaryColor?: string; accentColor?: string; logo?: string; favicon?: string }` | optional | App-specific branding | @@ -83,15 +83,15 @@ const result = ActionNavItemSchema.parse(data); | **navigation** | `({ id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { id: string; label: string; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| { id: string; label: string; icon?: string; order?: number; … })[]` | optional | Full navigation tree for the app sidebar | | **areas** | `{ id: string; label: string; icon?: string; description?: string; … }[]` | optional | Navigation areas for partitioning navigation by business domain | | **contextSelectors** | `{ id: string; label: string; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{}` template vars | -| **homePageId** | `any` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. | +| **homePageId** | `never` | optional | [REMOVED] `app.homePageId` was removed in @objectstack/spec 17.0.0 (#4667, #4709, ADR-0049). objectui's console did read it before v17 (`resolveLandingRoute`), so this key had a consumer — it was retired because the capability is better expressed on the navigation item itself than as an ID cross-reference that silently falls back when it dangles. An app's landing page IS its first navigation item (by `order`), and the root landing follows `isDefault` routing. Delete the key; to change where an app opens, reorder `navigation` so the intended entry is first, and set `isDefault` on the app that should own the root landing. Run `os migrate meta --from 16` to rewrite existing sources automatically. | | **requiredPermissions** | `string[]` | optional | Permissions required to access this app | -| **objects** | `any` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. | -| **apis** | `any` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE still belongs in a plugin manifest `contributes.routes` entry. | -| **sharing** | `any` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. | -| **embed** | `any` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. | -| **mobileNavigation** | `any` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. | +| **objects** | `never` | optional | [REMOVED] `App.objects` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read; the spec itself labelled it "config file convenience"). Objects belong to the stack (`defineStack({ objects })`); an app reaches them through its navigation items. Delete the key. | +| **apis** | `never` | optional | [REMOVED] `App.apis` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — never read). Delete the key and declare the endpoint one level up, on the STACK: `defineStack({ apis })`. That surface EXECUTES from protocol 17 (#5040). Between #4936 and the executor landing it was refused wholesale — nothing mounted a declared path, so every key including `authRequired` parsed and gated nothing — and that blanket refusal is now narrowed to five per-endpoint publish gates (namespace, supported target, mapping, policy, uniqueness): an endpoint that passes them is mounted and serves traffic as soon as the stack is published. Two things to get right when you move it: the path must sit inside your own carve-out, `/api/v1/apps//` with an explicit `manifest.namespace` (ADR-0121 D1/D2), and `authRequired` defaults to `true` — an explicit `false` is the only thing that opens anonymous access, and ADR-0121 D6 then requires an armed `rateLimit: { enabled: true, windowMs, maxRequests }`. Read the `declarative-apis-endpoints-live` entry of the protocol upgrade guide first; it is a security review, not a rename. A route that genuinely needs handler CODE still belongs in a plugin manifest `contributes.routes` entry. | +| **sharing** | `never` | optional | [REMOVED] `App.sharing` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049 enforce-or-remove) — no public-app route ever read it, so it declared sharing that did not exist. Public access is granted per FORM VIEW (`FormView.sharing`, the public-data-collection surface). Delete the key. | +| **embed** | `never` | optional | [REMOVED] `App.embed` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit / ADR-0049) — no iframe route ever read it. Embedding is a per-form-view surface (`FormView.sharing`), not an app-level switch. Delete the key. | +| **mobileNavigation** | `never` | optional | [REMOVED] `App.mobileNavigation` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — fully unimplemented; no renderer, including packages/mobile, ever read it). Delete the key; the block returns if/when a real mobile navigation ships. | | **defaultAgent** | `string` | optional | Platform agent bound to this app's ambient chat ('ask' is the implicit default; 'build' for authoring surfaces) — ADR-0063 §1 | -| **aria** | `any` | optional | [REMOVED] `App.aria` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no renderer read app-level ARIA attributes). Declare `aria` on the component/widget that renders the DOM node instead. Delete the key. | +| **aria** | `never` | optional | [REMOVED] `App.aria` was removed in @objectstack/spec 17.0.0 (2026-06 liveness audit — no renderer read app-level ARIA attributes). Declare `aria` on the component/widget that renders the DOM node instead. Delete the key. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this app. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | diff --git a/content/docs/references/ui/dashboard.mdx b/content/docs/references/ui/dashboard.mdx index 0033be0d44..6801369d34 100644 --- a/content/docs/references/ui/dashboard.mdx +++ b/content/docs/references/ui/dashboard.mdx @@ -39,8 +39,8 @@ const result = DashboardSchema.parse(data); | **refreshInterval** | `number` | optional | Auto-refresh interval in seconds | | **dateRange** | `{ field?: string; defaultRange: Enum<'today' \| 'yesterday' \| 'this_week' \| 'last_week' \| 'this_month' \| 'last_month' \| 'this_quarter' \| 'last_quarter' \| 'this_year' \| 'last_year' \| 'last_7_days' \| 'last_30_days' \| 'last_90_days' \| 'custom'>; allowCustomRange: boolean }` | optional | Global dashboard date range filter configuration | | **globalFilters** | `{ name?: string; field: string; label?: string; type?: Enum<'text' \| 'select' \| 'date' \| 'number' \| 'lookup'>; … }[]` | optional | Global filters that apply to all widgets in the dashboard | -| **aria** | `any` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. | -| **performance** | `any` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. | +| **aria** | `never` | optional | [REMOVED] `dashboard.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no dashboard renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. | +| **performance** | `never` | optional | [REMOVED] `dashboard.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; dashboard performance tuning was never implemented. Delete the key. | | **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this dashboard. | | **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). | | **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. | @@ -98,9 +98,9 @@ Dashboard header action | **colorVariant** | `Enum<'default' \| 'blue' \| 'teal' \| 'orange' \| 'purple' \| 'success' \| 'warning' \| 'danger'>` | optional | Widget color variant for theming | | **requiresObject** | `string` | optional | Hide the widget unless the named object is registered | | **requiresService** | `string` | optional | Hide the widget unless the named kernel service is registered | -| **actionUrl** | `any` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | -| **actionType** | `any` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | -| **actionIcon** | `any` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | +| **actionUrl** | `never` | optional | [REMOVED] `dashboard.widgets[].actionUrl` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | +| **actionType** | `never` | optional | [REMOVED] `dashboard.widgets[].actionType` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | +| **actionIcon** | `never` | optional | [REMOVED] `dashboard.widgets[].actionIcon` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 enforce-or-remove) — a dashboard widget has NO action button, and never had one. No renderer draws per-widget chrome for it: every action the dashboard dispatches comes from `header.actions[]`. The three keys `actionUrl` / `actionType` / `actionIcon` went together; delete all three. Put the affordance on the dashboard header instead — `header: { actions: [{ label, actionUrl, actionType, icon }] }` — which IS dispatched (`DashboardHeaderAction`, same vocabulary, and `icon` is the header spelling of `actionIcon`). For a per-ROW affordance, the widget to reach for is a `table`/`pivot` bound to a dataset: its rows are clickable and drill through the semantic layer. Run `os migrate meta --from 16` to rewrite it automatically. | | **filter** | `any` | optional | Presentation-scope filter (runtimeFilter) | | **compareTo** | `{ kind: Enum<'previousPeriod' \| 'previousYear'>; dimension?: string }` | optional | Period-over-period comparison window (`{ kind, dimension? }`) | | **dataset** | `string` | ✅ | Dataset name to bind (ADR-0021) | @@ -110,8 +110,8 @@ Dashboard header action | **options** | `{ dateGranularity?: Enum<'day' \| 'week' \| 'month' \| 'quarter' \| 'year'>; sortBy?: string; sortOrder?: Enum<'asc' \| 'desc'>; limit?: integer; … } & Record` | optional | Widget specific configuration | | **filterBindings** | `Record` | optional | Per-widget dashboard-filter bindings: filter name → this widget's field, or false to opt out | | **suppressWarnings** | `string[]` | optional | Build diagnostic rule ids suppressed on this widget | -| **responsive** | `any` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite it automatically. | -| **aria** | `any` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `app.aria` and `page.components[].aria`. Run `os migrate meta --from 16` to rewrite it automatically. | +| **responsive** | `never` | optional | [REMOVED] `dashboard.widgets[].responsive` was removed in @objectstack/spec 17.0.0 (#4876, ADR-0049 D2) — no renderer ever read it, so per-widget breakpoint overrides were never applied: the value parsed, validated, and then did nothing. The dashboard grid reflows by its own layout rules (`columns` + `gap` on the dashboard, the `layout` box on each widget). Delete the key. The shared `ResponsiveConfig` shape is NOT gone — it stays live on `page.components[].responsive`, which objectui `useResponsiveConfig` really does read; move the layout there if you need breakpoint behaviour today. Run `os migrate meta --from 16` to rewrite it automatically. | +| **aria** | `never` | optional | [REMOVED] `dashboard.widgets[].aria` was removed in @objectstack/spec 17.0.0 (#5010, ADR-0049 D2) — no renderer ever applied it, so ARIA attributes declared on a widget silently did not reach the DOM: the key promised accessibility compliance it did not deliver. This is the same removal the dashboard-level `aria` got in 17.0.0 (#3896). Delete the key. The dashboard renderer emits its own `aria-*` attributes for the widget grid; author a `title` (and `description`) on the widget instead — those ARE what the renderer labels the card with. The shared `AriaProps` shape is NOT gone: it stays live on `app.aria` and `page.components[].aria`. Run `os migrate meta --from 16` to rewrite it automatically. | --- diff --git a/content/docs/references/ui/theme.mdx b/content/docs/references/ui/theme.mdx index 7c5005d73a..c2963ede7f 100644 --- a/content/docs/references/ui/theme.mdx +++ b/content/docs/references/ui/theme.mdx @@ -99,11 +99,11 @@ const result = BorderRadiusSchema.parse(data); | **description** | `string` | optional | Theme description | | **mode** | `Enum<'light' \| 'dark' \| 'auto'>` | ✅ | Theme mode (light, dark, or auto) | | **colors** | `{ primary: string; secondary?: string; accent?: string; success?: string; … }` | ✅ | Color palette configuration | -| **typography** | `{ fontFamily?: object; fontSize?: any; fontWeight?: any; lineHeight?: any; … }` | optional | Typography settings | +| **typography** | `{ fontFamily?: object }` | optional | Typography settings | | **borderRadius** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Border radius scale | | **shadows** | `{ none?: string; sm?: string; base?: string; md?: string; … }` | optional | Box shadow effects | -| **animation** | `any` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite it automatically. | -| **zIndex** | `any` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **animation** | `never` | optional | [REMOVED] `theme.animation` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — unlike the #3494 props above, the engine DID emit `--duration-*` and `--timing-*`, faithfully and for years; what never existed was a reader. No first-party component or stylesheet has ever consumed one, so every transition ran at the renderer default whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "duration-fast": "150ms", "timing-ease_in": "cubic-bezier(0.4, 0, 1, 1)" }` emits exactly the same properties). Run `os migrate meta --from 16` to rewrite it automatically. | +| **zIndex** | `never` | optional | [REMOVED] `theme.zIndex` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--z-base` … `--z-tooltip` and nothing read one, so an overlay you "lifted" still stacked by document order. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "z-modal": "1050" }` emits exactly the same `--z-modal`). Run `os migrate meta --from 16` to rewrite it automatically. | | **customVars** | `Record` | optional | Custom CSS variables (key-value pairs) | | **extends** | `string` | optional | Base theme to extend from | @@ -127,11 +127,11 @@ const result = BorderRadiusSchema.parse(data); | Property | Type | Required | Description | | :--- | :--- | :--- | :--- | -| **fontFamily** | `{ base?: string; heading?: any; mono?: any }` | optional | | -| **fontSize** | `any` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **fontWeight** | `any` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **lineHeight** | `any` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite it automatically. | -| **letterSpacing** | `any` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **fontFamily** | `{ base?: string }` | optional | | +| **fontSize** | `never` | optional | [REMOVED] `theme.typography.fontSize` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-size-xs` … `--font-size-4xl` faithfully and NO first-party component or stylesheet has ever read one, so a declared type scale was real CSS that styled nothing. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-size-lg": "1.125rem" }` emits exactly the same `--font-size-lg`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **fontWeight** | `never` | optional | [REMOVED] `theme.typography.fontWeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--font-weight-*` and nothing read it, so text rendered at the inherited weight whatever you declared. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "font-weight-semibold": "600" }` emits exactly the same `--font-weight-semibold`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **lineHeight** | `never` | optional | [REMOVED] `theme.typography.lineHeight` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--line-height-*` and nothing read it, so every block kept its inherited leading. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "line-height-relaxed": "1.75" }` emits exactly the same `--line-height-relaxed`). Run `os migrate meta --from 16` to rewrite it automatically. | +| **letterSpacing** | `never` | optional | [REMOVED] `theme.typography.letterSpacing` was removed in @objectstack/spec 17.0.0 (#5021, ADR-0049 D2) — the engine emitted `--letter-spacing-*` and nothing read it, so tracking never moved. Delete the key; if your own CSS reads those variables, declare them under `customVars` (`{ "letter-spacing-wide": "0.025em" }` emits exactly the same `--letter-spacing-wide`). Run `os migrate meta --from 16` to rewrite it automatically. | --- diff --git a/content/docs/references/ui/view.mdx b/content/docs/references/ui/view.mdx index 260c3f9c14..ef53125cad 100644 --- a/content/docs/references/ui/view.mdx +++ b/content/docs/references/ui/view.mdx @@ -213,12 +213,12 @@ Column footer summary configuration | **sections** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | | **groups** | `{ name?: string; label?: string; description?: string; collapsible?: boolean; … }[]` | optional | | | **subforms** | `{ childObject: string; relationshipField?: string; columns?: any[]; amountField?: string; … }[]` | optional | Inline master-detail child collections | -| **defaultSort** | `any` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. | +| **defaultSort** | `never` | optional | [REMOVED] `form.defaultSort` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — nothing read it: a related list inside a form sorts by its own list view's `sort`. Delete the key and set the sort on the related list view instead. | | **sharing** | `{ enabled?: boolean; publicLink?: string; password?: string; allowedDomains?: string[]; … }` | optional | Public sharing configuration for this form | | **submitBehavior** | `{ kind: 'thank-you'; title?: string; message?: string } \| { kind: 'redirect'; url: string; delayMs?: integer } \| { kind: 'continue' } \| { kind: 'next-record' }` | optional | Post-submit behavior | | **buttons** | `{ submit?: object; cancel?: object; reset?: object }` | optional | Form action-button visibility & labels; folded onto the flat renderer props by ObjectUI ObjectForm (framework#1894 / #2998). | | **defaults** | `Record` | optional | Initial field values for create-mode forms (folded into ObjectUI ObjectForm initial values; framework#1894 / #2998). | -| **aria** | `any` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. | +| **aria** | `never` | optional | [REMOVED] `form.aria` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no form renderer ever applied it, so declared ARIA attributes silently did not reach the DOM. Delete the key. The form renderer emits its own semantic markup; report gaps as renderer issues rather than per-view attribute overrides. | --- @@ -442,8 +442,8 @@ List chart view configuration | **allowPrinting** | `boolean` | optional | Allow users to print the view | | **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | -| **responsive** | `any` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | -| **performance** | `any` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | --- @@ -530,8 +530,8 @@ List chart view configuration | **allowPrinting** | `boolean` | optional | Allow users to print the view | | **emptyState** | `{ title?: string; message?: string; icon?: string }` | optional | Empty state configuration when no records found | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes for the list view | -| **responsive** | `any` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | -| **performance** | `any` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | +| **responsive** | `never` | optional | [REMOVED] `view.responsive` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer ever read it; the grid is responsive by its own layout rules. Delete the key. | +| **performance** | `never` | optional | [REMOVED] `view.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime read it; list-view performance tuning was never implemented. Delete the key. | | **userFilters** | `{ element?: Enum<'dropdown' \| 'toggle'>; fields?: { field: string; label?: string; type?: Enum<'select' \| 'multi-select' \| 'boolean' \| 'date-range' \| 'text'>; options?: { value: string \| number \| boolean; label: string; color?: string }[]; … }[] }` | optional | | diff --git a/content/docs/references/ui/widget.mdx b/content/docs/references/ui/widget.mdx index 4defec5b23..329aca26a6 100644 --- a/content/docs/references/ui/widget.mdx +++ b/content/docs/references/ui/widget.mdx @@ -110,7 +110,7 @@ const result = WidgetEventSchema.parse(data); | **license** | `string` | optional | License (SPDX identifier) | | **tags** | `string[]` | optional | Tags for categorization | | **aria** | `{ ariaLabel?: string; ariaDescribedBy?: string; role?: string }` | optional | ARIA accessibility attributes | -| **performance** | `any` | optional | [REMOVED] `widget.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime ever read it. Delete the key. Virtual scrolling is the live top-level `virtualScroll` on list-shaped views. | +| **performance** | `never` | optional | [REMOVED] `widget.performance` was removed in @objectstack/spec 17.0.0 (#3896 audit close-out) — no renderer or runtime ever read it. Delete the key. Virtual scrolling is the live top-level `virtualScroll` on list-shaped views. | ---