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
53 changes: 53 additions & 0 deletions .changeset/inline-shape-depth-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
"@objectstack/spec": patch
---

fix(spec): 参考文档的内联形状摘要只展开一层,不再沿数组/Record/联合无预算下钻 (#6374)

`content/docs/references/**` 的类型单元格里,最宽的一格是 `ui/page.mdx` 的
`Page.slots`,**1538 字符** —— 而且是在 #5340 的枚举省略已经在这一格生效 8 次
之后的宽度。本次修完,这一格是 **122 字符**。

## 机制

`format-type.ts` 一直只展开**一层** `{ … }` 形状:再往下的对象打印 `object`。
但这条预算写在键循环的三元表达式里,于是只有**直接对象子节点**受它约束。数组元素
(`{ … }[]`)、`Record` 的值(`Record<string, { … }>`)、联合的变体
(`{ … } | { … }[]`)这三条路径都会重新进入对象分支,而预算不在作用域内 ——
单元格宽度于是等于「每层键数 × 变体数 × 每个形状的宽度」,一层一层乘上去。

同一个形状在同一个阅读深度上,**印全还是印 `object`,取决于作者有没有把它包在数组
里** —— 这是关于 Zod 写法的事实,不是关于读者怎么读的事实,和 #6225 拆掉的那种
不对称完全同类。

新常量 `SHAPE_DEPTH_LIMIT` 把同一条预算移到对象分支本身,四条下钻路径都要过它。
**阈值 1 不是选出来的,是取回来的** —— 它就是直接子节点路径上一直生效的那个值;
全语料实测,任何大于 1 的取值都比不改还差(>200 字符的单元格 121 → 173+,因为
提高上限必然放松那条本来就是 1 的路径)。

## 读者看到的变化(全语料 215 页 / 8499 个类型单元格)

| | 修改前 | 修改后 |
|---|---|---|
| >200 字符 | 121 | **42** |
| >400 字符 | 9 | **1** |
| >900 字符 | 1 | **0** |
| p95 / p99 | 145 / 229 | **124 / 180** |
| 最宽单元格 | 1538 | **656** |

修改后仅剩的那个 >400 单元格是 `ui/page.mdx` 的 `PageComponent.type`(656),
一个落在联合变体里的顶层词表 —— #6225 有意不收它,它也**不含任何嵌套形状**。
也就是说:**由形状深度带来的宽度已经从语料里消失了**。

## 省略掉的信息去哪了

`object` 不是截断:它对键**什么都不声称**,所以不像前缀那样会被误读成完整列表 ——
这正是 #5340 定下的原则用在形状上而不是枚举成员上。它也不是这些表格里的新省略
风格:嵌套形状本来就一直印 `object`。完整形状仍在原处 —— 生成器为它出页时是它
自己的 `## Schema` 一节,任何情况下都在 `json-schema/` 里。

#5340 / #6226 的两个标记都还活着,只是有些出现位置被上游的深度预算吸收了:
枚举标记 178 → 156,变体标记 16 → 9。#6226 的旗舰样本 `App.navigation` 在深度 0,
逐字未变。

⛔ 所有 `.mdx` 均由 `gen:schema && gen:docs` 重生成,无一处手改。
2 changes: 1 addition & 1 deletion content/docs/references/ai/conversation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,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: (object \| object \| object \| object)[]; … }[]` | ✅ | |
| **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/ai/model-registry.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const result = ModelCapabilitySchema.parse(data);
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Registry name |
| **models** | `Record<string, { model: object; status?: Enum<'active' \| 'deprecated' \| 'experimental' \| 'disabled'>; priority?: integer; fallbackModels?: string[]; … }>` | ✅ | Model entries by ID |
| **promptTemplates** | `Record<string, { id: string; name: string; label: string; system?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }>` | optional | Prompt templates by name |
| **promptTemplates** | `Record<string, { id: string; name: string; label: string; system?: string \| object; … }>` | optional | Prompt templates by name |
| **defaultModel** | `string` | optional | Default model ID |
| **enableAutoFallback** | `boolean` | optional | Auto-fallback on errors |

Expand Down
12 changes: 6 additions & 6 deletions content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,10 @@ const result = BlueprintAppSchema.parse(data);
| **summary** | `string` | optional | One-line description of the proposed solution |
| **assumptions** | `string[]` | ✅ | Design assumptions made from the underspecified goal |
| **questions** | `string[]` | optional | At most 1-2 structure-deciding questions to confirm before building |
| **objects** | `{ name: string; label?: string; description?: string; fields: { name: string; label?: string; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| … +42 more>; required?: boolean; … }[]; … }[]` | ✅ | Objects (tables) to create |
| **objects** | `{ name: string; label?: string; description?: string; fields: object[]; … }[]` | ✅ | Objects (tables) to create |
| **views** | `{ object: string; name: string; label?: string; type: Enum<'list' \| 'form' \| 'kanban' \| 'calendar' \| 'gallery' \| 'gantt'>; … }[]` | optional | Views to create |
| **dashboards** | `{ name: string; label?: string; widgets?: { id: string; title?: string; object?: string; chart?: Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'>; … }[] }[]` | optional | Dashboards to create |
| **app** | `{ name: string; label?: string; icon?: string; nav?: { type: Enum<'object' \| 'dashboard'>; target: string; label?: string; icon?: string }[] }` | optional | The navigation shell (app) that surfaces the created objects/dashboards to end users |
| **dashboards** | `{ name: string; label?: string; widgets?: object[] }[]` | optional | Dashboards to create |
| **app** | `{ name: string; label?: string; icon?: string; nav?: object[] }` | optional | The navigation shell (app) that surfaces the created objects/dashboards to end users |
| **seedData** | `{ object: string; records: Record<string, any>[] }[]` | optional | Suggested seed data (reported, not auto-applied in Phase C) |


Expand All @@ -244,10 +244,10 @@ const result = BlueprintAppSchema.parse(data);
| **summary** | `string` | ✅ | One-line description of the proposed solution |
| **assumptions** | `string[]` | ✅ | Design assumptions made from the underspecified goal |
| **questions** | `string[] \| null` | ✅ | At most 1-2 structure-deciding questions to confirm before building, or null |
| **objects** | `{ name: string; label: string \| null; description: string \| null; fields: { name: string; label: string \| null; type: Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| … +42 more>; required: boolean \| null; … }[] }[]` | ✅ | Objects (tables) to create |
| **objects** | `{ name: string; label: string \| null; description: string \| null; fields: object[] }[]` | ✅ | Objects (tables) to create |
| **views** | `{ object: string; name: string; label: string \| null; type: Enum<'list' \| 'form' \| 'kanban' \| 'calendar' \| 'gallery' \| 'gantt'> \| null; … }[] \| null` | ✅ | Views to create, or null |
| **dashboards** | `{ name: string; label: string \| null; widgets: { id: string; title: string \| null; object: string \| null; chart: Enum<'metric' \| 'bar' \| 'line' \| 'pie' \| 'table'> \| null; … }[] \| null }[] \| null` | ✅ | Dashboards to create, or null |
| **app** | `{ name: string; label: string \| null; icon: string \| null; nav: { type: Enum<'object' \| 'dashboard'>; target: string; label: string \| null; icon: string \| null }[] \| null } \| null` | ✅ | The navigation shell (app) that surfaces the created objects/dashboards, or null |
| **dashboards** | `{ name: string; label: string \| null; widgets: object[] \| null }[] \| null` | ✅ | Dashboards to create, or null |
| **app** | `{ name: string; label: string \| null; icon: string \| null; nav: object[] \| null } \| null` | ✅ | The navigation shell (app) that surfaces the created objects/dashboards, or null |


---
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/analytics.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const result = AnalyticsEndpoint.parse(data);
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; 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** | `{ cubes: { name: string; title?: string; description?: string; sql: string; … }[] }` | ✅ | |
| **data** | `{ cubes: object[] }` | ✅ | |


---
Expand Down Expand Up @@ -81,7 +81,7 @@ const result = AnalyticsEndpoint.parse(data);
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; 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** | `{ rows: Record<string, any>[]; fields: { name: string; type: string }[]; sql?: string }` | ✅ | |
| **data** | `{ rows: Record<string, any>[]; fields: object[]; sql?: string }` | ✅ | |


---
Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/api/automation-api.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ const result = AutomationApiErrorCode.parse(data);
| **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<string, any>; … }[]` | ✅ | Flow nodes |
| **edges** | `{ id: string; source: string; target: string; condition?: string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }; … }[]` | ✅ | Flow connections |
| **edges** | `{ id: string; source: string; target: string; condition?: string \| object; … }[]` | ✅ | Flow connections |
| **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 |
Expand Down Expand Up @@ -243,7 +243,7 @@ const result = AutomationApiErrorCode.parse(data);
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; 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** | `{ flows: { name: string; label: string; type: string; status: string; … }[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
| **data** | `{ flows: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |


---
Expand Down Expand Up @@ -271,7 +271,7 @@ const result = AutomationApiErrorCode.parse(data);
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; 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** | `{ runs: { id: string; flowName: string; flowVersion?: integer; status: Enum<'pending' \| 'running' \| 'paused' \| 'completed' \| 'failed' \| 'cancelled' \| … +2 more>; … }[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |
| **data** | `{ runs: object[]; total?: integer; nextCursor?: string; hasMore: boolean }` | ✅ | |


---
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/batch.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const result = BatchConfigSchema.parse(data);
| **total** | `number` | ✅ | Total number of records in the batch |
| **succeeded** | `number` | ✅ | Number of records that succeeded |
| **failed** | `number` | ✅ | Number of records that failed |
| **results** | `{ id?: string; success: boolean; errors?: { code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }[]; data?: Record<string, any>; … }[]` | ✅ | Detailed results for each record |
| **results** | `{ id?: string; success: boolean; errors?: object[]; data?: Record<string, any>; … }[]` | ✅ | Detailed results for each record |


---
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ const result = ApiErrorSchema.parse(data);
| **success** | `boolean` | ✅ | Operation success status |
| **error** | `{ code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; 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** | `{ id?: string; success: boolean; errors?: { code: Enum<'VALIDATION_ERROR' \| 'INVALID_FIELD' \| 'MISSING_REQUIRED_FIELD' \| … +257 more>; message: string; category?: string; httpStatus?: integer; … }[]; index?: number; … }[]` | ✅ | Results for each item in the batch |
| **data** | `{ id?: string; success: boolean; errors?: object[]; index?: number; … }[]` | ✅ | Results for each item in the batch |


---
Expand Down Expand Up @@ -417,7 +417,7 @@ const result = ApiErrorSchema.parse(data);
| **having** | `any` | optional | HAVING — filter over the AGGREGATED rows (aggregation aliases + groupBy projections); applied engine-side after 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<string, { object: string; fields?: string[]; where?: any; search?: string \| { query: string; fields?: string[]; fuzzy: boolean; operator: Enum<'and' \| 'or'>; … }; … }>` | 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. |
| **expand** | `Record<string, { object: string; fields?: string[]; where?: any; search?: string \| object; … }>` | 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. |


---
Expand Down
Loading
Loading