Skip to content

Commit 6e82972

Browse files
os-zhuangclaude
andauthored
fix(spec): docs-gen 把 retiredKey() 墓碑渲染成 never,而不是 any (#5606) (#6058)
* fix(spec): docs-gen 把 retiredKey() 墓碑渲染成 never,而不是 any (#5606) `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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 * docs(spec): regenerate content/docs/references/** with the never-tombstone renderer (#5606) 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wsZeReNTqiceBfLb5Pyf5 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7e791e5 commit 6e82972

33 files changed

Lines changed: 411 additions & 123 deletions
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the reference generator prints a `retiredKey()` tombstone as `never`, not `any` (#5606)
6+
7+
`retiredKey()` is `z.never()`, which `z.toJSONSchema` emits as `{ "not": {} }`
8+
a node with no `type`, no `$ref` and no `enum`. `formatType()` had no branch for
9+
it, so every one of the ~28 tombstones in the spec fell through to the
10+
`prop.type || 'any'` tail and the generated reference pages typed a **removed**
11+
key as **`any`**.
12+
13+
That is the worst available rendering for a retirement. These pages are the
14+
primary input for an upgrading author — very often an AI one (ADR-0033) — and
15+
`heading?: any` does not read "this key was deleted", it reads "this slot exists
16+
and nothing validates it": strictly *more* inviting than the `heading?: string`
17+
it replaced. The author writes it, the parse rejects it with the `[REMOVED]`
18+
prescription, and the prescription arrives only after a wrong metadata file
19+
already exists.
20+
21+
Two changes, both in `scripts/lib/format-type.ts`:
22+
23+
- **`{ not: {} }` now renders as `never`.** Accurate TypeScript — the key's
24+
`z.input` type *is* `never` — and, unlike `any`, self-evident with no prose
25+
to lean on.
26+
- **Tombstones are dropped from an inline shape summary before
27+
`INLINE_KEY_LIMIT` counts.** A summary cell prints `k?: type` for the first
28+
four declared keys and has no description column, so a nested tombstone had
29+
nowhere to put its prescription at all: `ui/theme.mdx` advertised
30+
`{ base?: string; heading?: any; mono?: any }` with both prescriptions
31+
appearing NOWHERE on the page. Retired keys are no longer authorable surface,
32+
so they no longer spend one of the four slots — nor push a key the author
33+
must write behind the ``. The known workaround of moving a tombstone to the
34+
bottom of the shape cannot cover this: #5248 retired `IndexSchema` down to
35+
three live keys, and with a limit of four the first tombstone is then
36+
*mathematically* guaranteed into the summary.
37+
38+
Per-key table rows are unaffected and keep carrying the full `[REMOVED]`
39+
prescription in their description column; their type cell simply now says
40+
`never` instead of `any`.

content/docs/references/ai/agent.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ const result = AIModelConfigSchema.parse(data);
5353
| **lifecycle** | `{ id: string; description?: string; contextSchema?: Record<string, any>; initial: string; … }` | optional | [EXPERIMENTAL — not enforced] State machine defining the agent conversation flow and constraints. Parsed but no runtime consumer yet (liveness #1878/#1893). |
5454
| **surface** | `Enum<'ask' \| 'build'>` || Product surface this agent binds ('ask' \| 'build') — ADR-0063 §1 |
5555
| **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) |
56-
| **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_<name>` for one of your own AI-exposed Actions. Run `os migrate meta --from 16` to rewrite it automatically. |
57-
| **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. |
56+
| **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_<name>` for one of your own AI-exposed Actions. Run `os migrate meta --from 16` to rewrite it automatically. |
57+
| **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. |
5858
| **active** | `boolean` || |
5959
| **access** | `string[]` | optional | Who can chat with this agent |
6060
| **permissions** | `string[]` | optional | Required permission-set capabilities |

content/docs/references/ai/skill.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ const result = SkillSchema.parse(data);
3939
| **surface** | `Enum<'ask' \| 'build' \| 'both'>` || Agent surface this skill binds to ('ask' \| 'build' \| 'both') — ADR-0063 §3 |
4040
| **instructions** | `string` | optional | LLM instructions when skill is active |
4141
| **tools** | `string[]` || Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) |
42-
| **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. |
42+
| **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. |
4343
| **triggerConditions** | `{ field: string; operator: Enum<'eq' \| 'neq' \| 'in' \| 'not_in' \| 'contains'>; value: string \| string[] }[]` | optional | Programmatic activation conditions |
4444
| **active** | `boolean` || Whether the skill is enabled |
4545
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this skill. |

content/docs/references/api/analytics.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ const result = AnalyticsEndpoint.parse(data);
6767
| **limit** | `number` | optional | |
6868
| **offset** | `number` | optional | |
6969
| **timezone** | `string` | optional | |
70-
| **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? }`. |
71-
| **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. |
70+
| **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? }`. |
71+
| **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. |
7272

7373

7474
---

content/docs/references/api/automation-api.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,12 @@ const result = AutomationApiErrorCode.parse(data);
102102
| **errorMessage** | `string` | optional | Toast shown when a screen flow fails (defaults to the raw error). |
103103
| **version** | `integer` | optional | Version number |
104104
| **status** | `Enum<'draft' \| 'active' \| 'obsolete' \| 'invalid'>` | optional | Deployment status |
105-
| **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. |
105+
| **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. |
106106
| **type** | `Enum<'autolaunched' \| 'record_change' \| 'schedule' \| 'screen' \| 'api'>` || Flow type |
107107
| **variables** | `{ name: string; type: string; isInput?: boolean; isOutput?: boolean }[]` | optional | Flow variables |
108108
| **nodes** | `{ id: string; type: string; label: string; config?: Record<string, any>; … }[]` || Flow nodes |
109109
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` || Flow connections |
110-
| **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. |
110+
| **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. |
111111
| **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. |
112112
| **errorHandling** | `{ strategy?: Enum<'fail' \| 'retry' \| 'continue'>; maxRetries?: integer; backoffMs?: integer; backoffMultiplier?: number; … }` | optional | Flow-level error handling configuration |
113113
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this flow. |

content/docs/references/api/batch.mdx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ const result = BatchConfigSchema.parse(data);
4747
| :--- | :--- | :--- | :--- |
4848
| **enabled** | `boolean` || Enable batch operations |
4949
| **maxRecordsPerBatch** | `integer` || Maximum records per batch |
50-
| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Default batch options |
50+
| **defaultOptions** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Default batch options |
5151

5252

5353
---
@@ -89,7 +89,7 @@ const result = BatchConfigSchema.parse(data);
8989
| **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. |
9090
| **returnRecords** | `boolean` || If true, return full record data in response |
9191
| **continueOnError** | `boolean` || If true (and atomic=false), continue processing remaining records after errors |
92-
| **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. |
92+
| **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. |
9393

9494

9595
---
@@ -115,7 +115,7 @@ const result = BatchConfigSchema.parse(data);
115115
| :--- | :--- | :--- | :--- |
116116
| **operation** | `Enum<'create' \| 'update' \| 'upsert' \| 'delete'>` || Type of batch operation |
117117
| **records** | `{ id?: string; data?: Record<string, any>; externalId?: string }[]` || Array of records to process (server caps the count — see batch.maxBatchSize) |
118-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Batch operation options |
118+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Batch operation options |
119119

120120

121121
---
@@ -199,7 +199,7 @@ A cross-object batch strip event: dropped fields plus the operation index
199199
| Property | Type | Required | Description |
200200
| :--- | :--- | :--- | :--- |
201201
| **ids** | `string[]` || Array of record IDs to delete (server caps the count — see batch.maxBatchSize) |
202-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Delete options |
202+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Delete options |
203203

204204

205205
---
@@ -223,7 +223,7 @@ A cross-object batch strip event: dropped fields plus the operation index
223223
| Property | Type | Required | Description |
224224
| :--- | :--- | :--- | :--- |
225225
| **records** | `{ id: string; data: Record<string, any> }[]` || Array of records to update (server caps the count — see batch.maxBatchSize) |
226-
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean; validateOnly?: any }` | optional | Update options |
226+
| **options** | `{ atomic: boolean; returnRecords: boolean; continueOnError: boolean }` | optional | Update options |
227227

228228

229229
---

0 commit comments

Comments
 (0)