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
75 changes: 75 additions & 0 deletions .changeset/reference-cell-width-enum-relocation-variant-cap.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
---
"@objectstack/spec": patch
---

docs(spec): 参考文档表格单元格的两条残留宽度 —— 顶层长枚举移入 `### Allowed Values`,联合变体印数量 (#6225, #6226)

#5340(PR #6211)把**内联摘要里**的长枚举压掉之后,`content/docs/references/**` 仍有 **27 个超过
400 字符的类型单元格**。它们不是一种毛病,是两种,机制不同,因此分两条修,一次重生成:

**#6225 —— 9 个「整格就是一个 `Enum<…>`」的顶层词表。**

`build-docs.ts` 一直有一条更适合长词表的渲染路径 —— `### Allowed Values` 加每个成员一行项目符号
—— 但它只在**整个 schema** 是 `type: 'string'` + `enum` 时才走。同样 49 个成员,被 Zod 提升成具名
schema(`data/FieldType`)的走项目符号,内联在**属性**上的(`Field.type`)得到一个 561 字符的表格
单元格,而 `ApiError.code` 的 261 个成员得到 **6092 字符**。一份词表落在哪种渲染上,取决于它有没有
被提升 —— 这是关于 Zod 的事实,不是关于读者怎么读它的事实。

新增的 `formatPropertyType` 就是那条分支的镜像,**逐条件对齐**(`type === 'string'` 且 `enum` 是
数组):属性自己的类型节点就是那份词表。单元格印一个样本加数量,完整成员表由 `build-docs.ts` 印在
表格正下方 —— **信息一个字都没有离开页面**。省略与搬迁是同一个 `elideEnum` 调用的两个返回值,不可能
各说各话;预算只在会搬迁的那一个入口读取,`formatType` 本身一行没改,所以 #5340 立下的「词表自己那
一行永不省略」在 `formatType` 上原样成立。

刻意**不**匹配的三种位置(各自都在单元格里渲染出 `Enum<…>`):`Enum<…>[]`、
`Record<string, Enum<…>>`、以及联合变体(`Enum<…> | string`,`ui/page.mdx` 的 `PageComponent.type`)。
对它们来说「本属性的允许值」不是实话 —— 成员是**元素 / 值 / 某一个变体**的词表 —— 表格下面挂一份项目
符号会宣称 schema 没说过的事。数字枚举同样不匹配:项目符号把每个成员渲染成 `` `x` ``,分不出 `2` 与
`'2'`,那正是 #5729 修掉的错(镜像的整 schema 分支也要求 `type === 'string'`,所以这是对齐不是巧合)。

**#6226 —— 另外 18 个里的联合变体重复。** 维护者裁决:变体数上限 + 自报被省略的变体数。
`formatType` 的 `anyOf` 分支原本是 `variants.map(...).join(' | ')`,对变体之间的相似度一无所知 ——
`ui/app.mdx` 的 `App.navigation` 把同一个 `{ id; label; icon?; order?; … }` 印了 9 遍,其中 7 遍逐字
相同,582 字符。现在最多印 4 个,其余印 `… +N more`。

**阈值全部实测,没有拍脑袋的常数。** 在真实语料上逐个候选值重生成再量:

- 枚举预算:40 到 176 之间的**每一个**值都给出**完全相同**的宽度结果(>200 为 121、>400 为 18、
>900 为 1、p99 227、max 1538),所以这段区间里选哪个跟宽度无关,只跟**搬走多少份词表**有关 ——
从 227 份掉到 24 份。取 **160**:拿到能拿到的最好宽度,只搬 25 份而不是 227 份,距 184 的悬崖还
留有余量;它恰好是内联预算 80 的两倍,把两个位置的不对称说明白了 —— 摘掉一份副本几乎免费,把词表
搬出它自己那一行要在页面上多开一节。
- 变体上限:语料共 353 个联合,2 个变体的占 256(72.5%),累计到 4 个变体是 92.6%,之后是薄尾。
上限 2 → 8 个宽单元格,3 → 8,**4 → 9**,5 → 11,6 → 18(与不设上限无异)。取 **4**:它是仍能
做完几乎全部工作的最松上限(收到 3 只多救回 1 个单元格,却要省略 44 个联合而不是 26 个),并且
与读者上一行刚见过的 `INLINE_KEY_LIMIT = 4` 同源 —— 同一个单元格里两个不同的「印几个之后 `…`」
才是裁决要避免的那种不一致。

**「标记必须挣回自己的位置」守卫**沿用 #5340 并被两条新省略共用。实测(把守卫改成无条件省略再重生成
对比):它拒绝了 248 个候选省略里的 **54 个**,那 54 个加起来只省 **328 字符** —— 平均每个 6 字符,
而标记本身要花 12–15 字符。其中 **7 个是搬迁候选**,也就是七整节 `### Allowed Values` 本来会为了给
一个单元格削掉个位数字符而被加进页面。守卫在搬迁上比在 #5340 的内联省略上更要紧:那边一次拒绝省下
一个标记,这边省下一整节。

**全语料重新测量(216 页 / 8541 个单元格):**

| | 之前 | 之后 |
|---|---|---|
| 超过 200 字符 | 145 | 121 |
| 超过 400 字符 | 27 | **9** |
| 超过 900 字符 | 4 | **1** |
| p99 | 247 | 227 |
| p95 | 145 | 145(不动) |
| max | **6092** | 1538 |

p95 纹丝不动,是这次改动的重点:普通单元格一个都没有移位。9 份被搬迁的顶层词表连同另外 16 份 160
字符档的,全部逐条核过完整成员表就在本页(25/25,0 缺失);新加的 25 个 `### Allowed Values: X.y`
标题按 schema + 属性双重限定,全页面唯一(`api/errors.mdx` 上 `EnhancedApiError` 与 `FieldError` 都有
`code`,只写属性名会撞锚点)。全语料 MDX 编译保持 **216/216**。

**没有被这次改动够到的、如实记录:** 剩下 9 个超过 400 字符的单元格里,`ui/page.mdx` 的 `Page.slots`
(1538,本轮新的 max)恰恰**不是**变体重复 —— 它的每个联合都只有 **2** 个变体(`T` 或 `T[]`),宽度
来自 `INLINE_KEY_LIMIT` 的 4 个键 × 2 个变体 × 每个约 176 字符,任何 ≥2 的变体上限都够不到它。另有
4 个(`Manifest.capabilities`、`PluginRegistryEntry.capabilities`、
`GetTranslationsResponse.translations`、`PluginSecurityManifest.permissions`)**根本没有联合**,宽度
纯粹来自嵌套深度。这是第三种机制,已另立观察单,不在本 PR 范围内。
54 changes: 53 additions & 1 deletion content/docs/references/ai/solution-blueprint.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,65 @@ const result = BlueprintAppSchema.parse(data);
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Field machine name (snake_case) |
| **label** | `string` | optional | Human-readable field label |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| 'datetime' \| 'time' \| 'boolean' \| 'toggle' \| 'select' \| 'multiselect' \| 'radio' \| 'checkboxes' \| 'lookup' \| 'master_detail' \| 'tree' \| 'user' \| 'image' \| 'file' \| 'avatar' \| 'video' \| 'audio' \| 'formula' \| 'summary' \| 'autonumber' \| 'composite' \| 'repeater' \| 'record' \| 'location' \| 'address' \| 'code' \| 'json' \| 'color' \| 'rating' \| 'slider' \| 'signature' \| 'qrcode' \| 'progress' \| 'tags' \| 'vector'>` | ✅ | Field data type |
| **type** | `Enum<'text' \| 'textarea' \| 'email' \| 'url' \| 'phone' \| 'password' \| 'secret' \| 'markdown' \| 'html' \| 'richtext' \| 'number' \| 'currency' \| 'percent' \| 'date' \| … +35 more>` | ✅ | Field data type |
| **required** | `boolean` | optional | Whether the field is required |
| **reference** | `string` | optional | Target object name for lookup / master_detail relationship fields |
| **options** | `{ label: string; value: string }[]` | optional | Choices for select / multiselect / radio fields |
| **summaryOperations** | `{ object: string; function: Enum<'count' \| 'sum' \| 'avg' \| 'min' \| 'max'>; field?: string; relationshipField?: string; … }` | optional | REQUIRED when `type` is "summary" (a roll-up of child records: 任务总数 / 报名人数 / 合计金额 / 已完成任务数). Names the child object, the aggregation, and — for a qualified count/sum — the condition. A "summary" field without it materializes runtime-dead. |
| **expression** | `string` | optional | REQUIRED when `type` is "formula" — the CEL body the field computes, e.g. "record.quantity * record.unit_price", or "record.order_no + ' · ' + record.customer" for a composed title. A "formula" field without it materializes runtime-dead: the engine builds its formula plan only from fields that HAVE an expression, so the field reads null everywhere, forever. Same failure shape as a "summary" with no `summaryOperations`. Note `nameField` on the object recommends a formula for numbered entities (invoice/ticket) — that formula needs THIS key, or the record title is blank on every card, lookup chip and breadcrumb. |

### Allowed Values: `BlueprintField.type`

* `text`
* `textarea`
* `email`
* `url`
* `phone`
* `password`
* `secret`
* `markdown`
* `html`
* `richtext`
* `number`
* `currency`
* `percent`
* `date`
* `datetime`
* `time`
* `boolean`
* `toggle`
* `select`
* `multiselect`
* `radio`
* `checkboxes`
* `lookup`
* `master_detail`
* `tree`
* `user`
* `image`
* `file`
* `avatar`
* `video`
* `audio`
* `formula`
* `summary`
* `autonumber`
* `composite`
* `repeater`
* `record`
* `location`
* `address`
* `code`
* `json`
* `color`
* `rating`
* `slider`
* `signature`
* `qrcode`
* `progress`
* `tags`
* `vector`


---

Expand Down
Loading
Loading