Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .changeset/format-type-union-array-brackets.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
"@objectstack/spec": patch
---

fix(spec): 参考页给「元素是联合类型」的数组补上括号 —— `(string | number)[]`,不再是 `string | number[]` (#5338)

参考页类型单元格由 `packages/spec/scripts/lib/format-type.ts` 渲染,数组分支此前直接把
`[]` 拼在元素渲染结果之后。TypeScript 里 `[]` 的结合优先级高于 `|`,所以
`string | number[]` 表达的是「一个 string,**或者**一个 number 数组」,而 schema 说的是
「一个数组,元素是 string 或 number」—— **单元格印出的类型和 schema 声明的不是同一个**。
参考页的类型单元格正是元数据作者(尤其是 AI 作者)直接照抄的那一行:照着
`string | number[]` 写下一个裸 string,schema 会当场拒绝,而页面看起来是允许的。

修法只有一处:#4912 为交叉类型引入的深度扫描 `hasTopLevelIntersection` 放宽成
`hasTopLevelUnionOrIntersection`,同时识别顶层 `&` 与 `|`。两者本来就是同一条规则——
`[]` 对这两个运算符都不分配律(`A & B[]` 是 `A & (B[])`,`A | B[]` 是 `A | (B[])`)——
所以合用一次扫描。深度扫描原本就正确忽略 `{}` / `< >` / `[]` / `()` 内部的运算符,
因此 `Enum<'a' | 'b'>[]`、`Record<string, string | number>[]`、`{ k?: string | number }[]`
以及 markdown 链接都保持原样,不会多出括号。

重新生成 `content/docs/references/**` 后共 19 个参考页、42 行单元格得到修正
(47 处补括号),没有新增页,也没有页面被复活;`check:docs` 报告 240 个生成文件全部同步。

#4912 的交叉类型侧行为不变:`({ label: string; value: … } & Record<string, any>)[]`
仍然带括号,该 PR 的全部 pin 用例保持绿。
4 changes: 2 additions & 2 deletions content/docs/references/ai/conversation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ const result = CodeContentSchema.parse(data);
| **id** | `string` | ✅ | Unique message ID |
| **timestamp** | `string` | ✅ | ISO 8601 timestamp |
| **role** | `Enum<'system' \| 'user' \| 'assistant' \| 'function' \| 'tool'>` | ✅ | |
| **content** | `{ type: 'text'; text: string; metadata?: Record<string, any> } \| { type: 'image'; imageUrl: string; detail: Enum<'low' \| 'high' \| 'auto'>; metadata?: Record<string, any> } \| { type: 'file'; fileUrl: string; mimeType: string; fileName?: string; … } \| { type: 'code'; text: string; language: string; metadata?: Record<string, any> }[]` | ✅ | Message content (multimodal array) |
| **content** | `({ type: 'text'; text: string; metadata?: Record<string, any> } \| { type: 'image'; imageUrl: string; detail: Enum<'low' \| 'high' \| 'auto'>; metadata?: Record<string, any> } \| { type: 'file'; fileUrl: string; mimeType: string; fileName?: string; … } \| { type: 'code'; text: string; language: string; metadata?: Record<string, any> })[]` | ✅ | Message content (multimodal array) |
| **functionCall** | `{ name: string; arguments: string; result?: string }` | optional | Legacy function call |
| **toolCalls** | `{ id: string; type: Enum<'function'>; function: object }[]` | optional | Tool calls |
| **toolCallId** | `string` | optional | Tool call ID this message responds to |
Expand All @@ -119,7 +119,7 @@ const result = CodeContentSchema.parse(data);
| **context** | `{ sessionId: string; userId?: string; agentId?: string; object?: string; … }` | ✅ | |
| **modelId** | `string` | optional | AI model ID |
| **tokenBudget** | `{ maxTokens: integer; maxPromptTokens?: integer; maxCompletionTokens?: integer; reserveTokens: integer; … }` | ✅ | |
| **messages** | `{ id: string; timestamp: string; role: Enum<'system' \| 'user' \| 'assistant' \| 'function' \| 'tool'>; content: { type: 'text'; text: string; metadata?: Record<string, any> } \| { type: 'image'; imageUrl: string; detail: Enum<'low' \| 'high' \| 'auto'>; metadata?: Record<string, any> } \| { type: 'file'; fileUrl: string; mimeType: string; fileName?: string; … } \| { type: 'code'; text: string; language: string; metadata?: Record<string, any> }[]; … }[]` | ✅ | |
| **messages** | `{ id: string; timestamp: string; role: Enum<'system' \| 'user' \| 'assistant' \| 'function' \| 'tool'>; content: ({ type: 'text'; text: string; metadata?: Record<string, any> } \| { type: 'image'; imageUrl: string; detail: Enum<'low' \| 'high' \| 'auto'>; metadata?: Record<string, any> } \| { type: 'file'; fileUrl: string; mimeType: string; fileName?: string; … } \| { type: 'code'; text: string; language: string; metadata?: Record<string, any> })[]; … }[]` | ✅ | |
| **tokens** | `{ promptTokens: integer; completionTokens: integer; totalTokens: integer; budgetLimit: integer; … }` | optional | |
| **totalTokens** | `{ promptTokens: integer; completionTokens: integer; totalTokens: integer }` | optional | Total tokens across all messages |
| **totalCost** | `number` | optional | Total cost for this session in USD |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ const result = ApiErrorSchema.parse(data);
| **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']`). |
| **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) |
| **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. |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ const result = AiAgentCapabilitiesSchema.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **models** | `string \| { id: string; label: string; default: boolean }[]` | ✅ | Models this environment offers |
| **models** | `(string \| { id: string; label: string; default: boolean })[]` | ✅ | Models this environment offers |
| **defaultModel** | `string` | optional | Default model id, when the service reports one |


Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/automation/approval.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const result = ApprovalDecision.parse(data);
| **lockRecord** | `boolean` | ✅ | Lock the record from editing while pending |
| **approvalStatusField** | `string` | optional | Business-object field to mirror request status onto |
| **onEmptyApprovers** | `Enum<'admin_rescue' \| 'fail' \| 'auto_approve'>` | ✅ | Behavior when no concrete approver resolves at node entry |
| **decisionOutputs** | `string \| { key: string; label?: string; type?: Enum<'text' \| 'user' \| 'department' \| 'position' \| 'team'>; multiple?: boolean; … }[]` | optional | Author-declared decision outputs — bare keys or typed `{ key, type, multiple }` declarations |
| **decisionOutputs** | `(string \| { key: string; label?: string; type?: Enum<'text' \| 'user' \| 'department' \| 'position' \| 'team'>; multiple?: boolean; … })[]` | optional | Author-declared decision outputs — bare keys or typed `{ key, type, multiple }` declarations |
| **escalation** | `{ enabled: boolean; timeoutHours: number; action: Enum<'reassign' \| 'auto_approve' \| 'auto_reject' \| 'notify'>; escalateTo?: string; … }` | optional | Per-node SLA escalation |
| **maxRevisions** | `integer` | ✅ | Max send-backs for revision before auto-reject (0 = send-back disabled) |

Expand Down
14 changes: 7 additions & 7 deletions content/docs/references/automation/state-machine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ Type: `string`
| **description** | `string` | optional | |
| **contextSchema** | `Record<string, any>` | optional | Zod Schema for the machine context/memory |
| **initial** | `string` | ✅ | Initial State ID |
| **states** | `Record<string, { type: Enum<'atomic' \| 'compound' \| 'parallel' \| 'final' \| 'history'>; entry?: string \| { type: string; params?: Record<string, any> }[]; exit?: string \| { type: string; params?: Record<string, any> }[]; on?: Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string }[]>; … }>` | ✅ | State Nodes |
| **on** | `Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string }[]>` | optional | |
| **states** | `Record<string, { type: Enum<'atomic' \| 'compound' \| 'parallel' \| 'final' \| 'history'>; entry?: (string \| { type: string; params?: Record<string, any> })[]; exit?: (string \| { type: string; params?: Record<string, any> })[]; on?: Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string }[]>; … }>` | ✅ | State Nodes |
| **on** | `Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string }[]>` | optional | |


---
Expand All @@ -192,10 +192,10 @@ Type: `string`
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'atomic' \| 'compound' \| 'parallel' \| 'final' \| 'history'>` | ✅ | |
| **entry** | `string \| { type: string; params?: Record<string, any> }[]` | optional | Actions to run when entering this state |
| **exit** | `string \| { type: string; params?: Record<string, any> }[]` | optional | Actions to run when leaving this state |
| **on** | `Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string }[]>` | optional | Map of Event Type -> Transition Definition |
| **always** | `{ target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: string \| { type: string; params?: Record<string, any> }[]; description?: string }[]` | optional | |
| **entry** | `(string \| { type: string; params?: Record<string, any> })[]` | optional | Actions to run when entering this state |
| **exit** | `(string \| { type: string; params?: Record<string, any> })[]` | optional | Actions to run when leaving this state |
| **on** | `Record<string, string \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string } \| { target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string }[]>` | optional | Map of Event Type -> Transition Definition |
| **always** | `{ target?: string; cond?: string \| { type: string; params?: Record<string, any> }; actions?: (string \| { type: string; params?: Record<string, any> })[]; description?: string }[]` | optional | |
| **initial** | `string` | optional | Initial child state (if compound) |
| **states** | `Record<string, [StateNode](#statenode)>` | optional | |
| **meta** | `{ label?: string; description?: string; color?: string; aiInstructions?: string }` | optional | |
Expand All @@ -211,7 +211,7 @@ Type: `string`
| :--- | :--- | :--- | :--- |
| **target** | `string` | optional | Target State ID |
| **cond** | `string \| { type: string; params?: Record<string, any> }` | optional | Condition (Guard) required to take this path |
| **actions** | `string \| { type: string; params?: Record<string, any> }[]` | optional | Actions to execute during transition |
| **actions** | `(string \| { type: string; params?: Record<string, any> })[]` | optional | Actions to execute during transition |
| **description** | `string` | optional | Human readable description of this rule |


Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/data/field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,10 @@ const result = AddressSchema.parse(data);
| **relatedListColumns** | `any[]` | optional | Explicit columns for the detail-page related list (derived from the child object when omitted) |
| **displayField** | `string` | optional | Field shown as each candidate's label in the picker/popover (defaults to the referenced object's name/title). |
| **descriptionField** | `string` | optional | Secondary field shown under the label in the quick-select popover. |
| **lookupColumns** | `string \| { field: string; label?: string; width?: string; type?: string }[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. |
| **lookupColumns** | `(string \| { field: string; label?: string; width?: string; type?: string })[]` | optional | Explicit columns for the record-picker table; auto-derived from the referenced object when omitted. |
| **lookupPageSize** | `integer` | optional | Rows per page in the record-picker dialog (default 10). |
| **lookupFilters** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'lt' \| 'gte' \| 'lte' \| 'contains' \| 'in' \| 'notIn'>; value: any }[]` | optional | Base filters restricting which records are selectable (e.g. only active). The structured, picker-honoured lookup filter. |
| **dependsOn** | `string \| { field: string; param?: string }[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. |
| **dependsOn** | `(string \| { field: string; param?: string })[]` | optional | Declares that this field's available values depend on the value of other field(s) on the same record — the form gates the field until they are set and re-evaluates as they change. For `lookup`/`master_detail` it scopes the candidate query (string = same local/remote key; `{field,param}` when the remote filter key differs — the `{field,param}` form is lookup-only). For `select`/`multiselect`/`radio` the actual per-option rule lives in each option's `visibleWhen`; list the referenced fields here (string form) so the option list gates and refreshes with the parent. |
| **allowCreate** | `boolean` | optional | Allow inline quick-create from the record picker: when no match exists the user can create a record from the typed text (optimistic dataSource.create with the display field). Best for simple objects whose only required field is the display field. |
| **expression** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | Formula expression (CEL). e.g. F`record.amount * 0.1` |
| **returnType** | `Enum<'number' \| 'text' \| 'boolean' \| 'date'>` | optional | Inferred value type of a formula field (number/text/boolean/date) |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ const result = ApiMethod.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **class** | `Enum<'record' \| 'audit' \| 'telemetry' \| 'transient' \| 'event'>` | ✅ | Persistence contract: record (business truth, permanent) \| audit (compliance ledger) \| telemetry (high-freq log) \| transient (ephemeral state) \| event (bus messages). |
| **retention** | `{ maxAge: string; onlyWhen?: Record<string, string \| number \| boolean \| { $in: string \| number[] }> }` | optional | Age-based retention window enforced by the LifecycleService Reaper. |
| **retention** | `{ maxAge: string; onlyWhen?: Record<string, string \| number \| boolean \| { $in: (string \| number)[] }> }` | optional | Age-based retention window enforced by the LifecycleService Reaper. |
| **ttl** | `{ field: string; expireAfter: string }` | optional | Per-row TTL auto-expiry (transient/event classes). |
| **storage** | `{ strategy: 'rotation'; shards: integer; unit: Enum<'day' \| 'week' \| 'month'> }` | optional | Physical storage strategy for high-frequency telemetry (LifecycleService Rotator). |
| **archive** | `{ after: string; to: string; keep?: string }` | optional | Cold-store archival (LifecycleService Archiver) — audit-class hot→cold hand-off. |
Expand Down
Loading
Loading