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
55 changes: 55 additions & 0 deletions .changeset/identical-union-variant-dedupe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
"@objectstack/spec": patch
---

fix(spec): 参考文档的联合单元格每种拼写只印一次,重复的变体计入既有的 `… +N more` (#6569)

`content/docs/references/**` 的类型单元格里,同一个联合的多个变体如果**渲染成
逐字相同的字符串**,现在只印一次,被折叠掉的变体数计进 #6226 既有的
`… +N more` 标记。全语料实测:**14 格**改变(7 个页面),这些格的类型文本合计
**3682 → 2469 字符**;字面的 `object | object | object | object` 从 **9 格降到
0 格**。

## 为什么会有这种重复

#6374 给内联形状摘要加了深度预算(嵌套形状印 `object`)。后果之一:一个联合的
多个变体如果**都是对象**,它们在摘要之下就都渲染成 `object`,单元格里于是出现
连续重复。`kernel/manifest.mdx` 的 `Manifest.navigationContributions` 最刺眼 ——
它印了四个 `object`,再数「`… +5 more`」个**和已印出的那四个一模一样**的
`object`。

重复本身不是 #6374 造出来的新行为(`ui/app.mdx` 的 `App.navigation` 在 #6226
之前就把同一个形状印了九遍),但深度预算让它变成了语料里的常见拼写。这些页面是
AI 作者的权威输入(ADR-0033),一格读起来像渲染 bug 的代价高于它的宽度。

## 裁决落地的形状

维护者的裁决在 issue 正文的 B 与 C 之间:**折叠**(重复的拼写不携带第一份之外
的任何信息),但**自报元数**(#6226 的规则:省略必须说出自己的大小),并且用
表格**已有**的标记词汇,而不是新发明一种 `object ×4` 之类的记号 —— #6226 的
裁决理由恰恰是「同一张表里出现第二种省略风格,比它要修的宽度更糟」。

于是一句话对读者成立:**印出的拼写数 + 计数 = 联合的元数**,无论某个变体是被
变体数上限藏起来的,还是因为这一格已经逐字印过它而被折叠掉的。

## 三条有意的边界

- **判等在渲染出来的字符串上**,不在 schema 结构上:`object | object[]`
(`ui/page.mdx` 的 `Page.slots`)是两种拼写,永不折叠。这个渲染器判的是读者
看到的东西。
- **不限于相邻**。实测语料里唯一一处两种规则会分歧的地方,正是 #6226 立案的那
一格:`App.navigation` 的九个变体里,八个是同一种导航项拼写、一个是
`{ type: 'separator'; … }`,而分隔符排在**第七位**——`uniq` 式的相邻规则会
把第八份导航项**再印一遍**,等于把缺陷留在这一格里。
- **共享的「标记必须挣回自己的位置」守卫原样适用**,没有豁免:`object | object`
加上标记反而**变长 3 字符**,`object | object | object` 只省 6 字符而标记占
12 字符,两者都保留重复;四个重复才是第一个划算的宽度(省 15 字符)。语料里
14 处带重复拼写的联合渲染全部通过守卫。

## 一条既有 pin 因此翻转

#6226 曾把 `string | string | string | string | string` 逐字钉住,理由写在测试
注释里:按变体数上限印四个再加标记会**更长**,守卫拒绝。这次改的是**候选**而
不是守卫 —— 一种拼写加 `… +4 more` 是 18 字符对 42 字符,省 24 —— 同一条未经
修改的守卫因此接受。旧 pin 给出的理由,正是新 pin 给出相反结论的理由。守卫的
拒绝区间仍然有人占着,只是往下挪了一档(见上一节)。
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: (object \| object \| object \| object)[]; … }[]` | ✅ | |
| **messages** | `{ id: string; timestamp: string; role: Enum<'system' \| 'user' \| 'assistant' \| 'function' \| 'tool'>; content: (object \| … +3 more)[]; … }[]` | ✅ | |
| **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
4 changes: 2 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -948,9 +948,9 @@ Get package response
| **name** | `string` | optional | Item name — supplied by the metadata door; for an object-scoped container it is the object name. |
| **label** | `string \| Record<string, string>` | optional | Human-readable label shown in metadata lists. |
| **object** | `string` | optional | Object this container binds to — how a stack-level `views: [...]` entry says which object its views belong to; read by `getViewsByObject()` / `GET /meta/view?object=`. |
| **list** | `{ name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| object \| object \| object; … }` | optional | |
| **list** | `{ name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }` | optional | |
| **form** | `{ type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }` | optional | |
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| object \| object \| object; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Additional named list views (views mode — dropdown userFilters allowed, no tabs; ADR-0047) |
| **formViews** | `Record<string, { type?: Enum<'simple' \| 'tabbed' \| 'wizard' \| 'split' \| 'drawer' \| 'modal'>; layout?: Enum<'vertical' \| 'horizontal' \| 'inline' \| 'grid'>; columns?: integer; title?: string; … }>` | optional | Additional named form views |
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this view. |
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
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 @@ -136,7 +136,7 @@ const result = ApiMethod.parse(data);
| **titleFormat** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | optional | [DEPRECATED → nameField (ADR-0079)] Render-only title template; the server cannot return or query it, and an explicit nameField now takes precedence. Migrate a single-field title to nameField, a composite to a formula field designated as nameField. |
| **highlightFields** | `string[]` | optional | [ADR-0085] Ordered most-important fields; first entry wins where only one fits. Drives default columns, cards, previews, detail highlight strip. Renamed from compactLayout. |
| **stageField** | `string \| false` | optional | [ADR-0085] Lifecycle stage field (linear/ordered), or false to declare the status field non-linear and suppress stage heuristics. Absent = heuristic detection allowed. |
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| object \| object \| object; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
| **listViews** | `Record<string, { name?: string; label?: string \| Record<string, string>; type?: Enum<'grid' \| 'kanban' \| 'gallery' \| 'calendar' \| 'timeline' \| 'gantt' \| 'map' \| 'chart' \| 'tree'>; data?: object \| … +3 more; … }>` | optional | Built-in named list views (segmented tabs) shipped with the object schema — "views" mode, dropdown userFilters allowed, no page-only tabs (ADR-0047) |
| **searchableFields** | `string[]` | optional | Fields the `$search` query matches against (ADR-0061). Canonical default for the record picker, list quick-search and global search; views may narrow it. When unset, search auto-defaults to the name/title field plus short-text fields. |
| **enable** | `{ trackHistory?: boolean; searchable?: boolean; apiEnabled?: boolean; apiMethods?: Enum<'get' \| 'list' \| 'create' \| 'update' \| 'delete' \| 'bulk'>[]; … }` | optional | Enabled system features modules |
| **sharingModel** | `Enum<'private' \| 'public_read' \| 'public_read_write' \| 'controlled_by_parent'>` | optional | Org-Wide Default record visibility (OWD) for INTERNAL users. Canonical four only (legacy aliases removed, ADR-0090 D4): private (owner-only) \| public_read (everyone reads, owner writes) \| public_read_write (everyone reads+writes) \| controlled_by_parent (derived from the master record). A CUSTOM object that omits this resolves to private at runtime (ADR-0090 D1). |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/data/validation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,8 @@ const result = ConditionalValidationSchema.parse(data);
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
| **type** | `'conditional'` | ✅ | |
| **when** | `string \| { dialect: Enum<'cel' \| 'cron' \| 'template'>; source?: string; ast?: any; meta?: object }` | ✅ | Predicate (CEL). e.g. P`record.type == 'enterprise'` |
| **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| … +2 more` | ✅ | Validation rule to apply when condition is true |
| **otherwise** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| { name: string; label?: string; description?: string; active?: boolean; … } \| … +2 more` | optional | Validation rule to apply when condition is false |
| **then** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation) \| … +4 more` | ✅ | Validation rule to apply when condition is true |
| **otherwise** | `{ name: string; label?: string; description?: string; active?: boolean; … } \| [ConditionalValidation](#conditionalvalidation) \| … +4 more` | optional | Validation rule to apply when condition is false |


---
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/kernel/manifest.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const result = ManifestSchema.parse(data);
| **data** | `{ object: string; externalId?: string \| string[]; mode?: Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>; env?: Enum<'prod' \| 'dev' \| 'test'>[]; … }[]` | optional | Initial seed data (prefer top-level data field) |
| **capabilities** | `{ implements?: object[]; provides?: object[]; requires?: object[]; extensionPoints?: object[]; … }` | optional | Plugin capability declarations for interoperability |
| **extensions** | `Record<string, any>` | optional | Extension points and contributions |
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| object \| object \| object \| … +5 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
| **navigationContributions** | `{ app: string; group?: string; priority?: integer; items: (object \| … +8 more)[] }[]` | optional | Navigation items this package contributes into apps owned by other packages |
| **loading** | `{ strategy?: Enum<'eager' \| 'lazy' \| 'parallel' \| 'deferred' \| 'on-demand'>; preload?: object; codeSplitting?: object; dynamicImport?: object; … }` | optional | Plugin loading and runtime behavior configuration |
| **engine** | `{ objectstack: string }` | optional | Platform compatibility requirements (legacy; superseded by `engines`) |
| **engines** | `{ platform?: string; protocol?: string }` | optional | Plugin compatibility ranges (ADR-0025 §3.2; supersedes `engine`) |
Expand Down
6 changes: 3 additions & 3 deletions content/docs/references/ui/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const result = ActionNavItemSchema.parse(data);
| **active** | `boolean` | optional | Whether the app is enabled |
| **isDefault** | `boolean` | optional | Is default app |
| **hidden** | `boolean` | optional | Hide from the App Switcher; the shell surfaces hidden apps via the avatar menu instead |
| **navigation** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| … +5 more)[]` | optional | Full navigation tree for the app sidebar |
| **navigation** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | optional | Full navigation tree for the app sidebar |
| **areas** | `{ id: string; label: string \| Record<string, string>; icon?: string; description?: string \| Record<string, string>; … }[]` | optional | Navigation areas for partitioning navigation by business domain |
| **contextSelectors** | `{ id: string; label: string \| Record<string, string>; icon?: string; optionsSource: object; … }[]` | optional | App-level scope dropdowns whose value is injected into nav items as `{<id>}` template vars |
| **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. |
Expand Down Expand Up @@ -204,7 +204,7 @@ const result = ActionNavItemSchema.parse(data);
| **label** | `string \| Record<string, string>` | ✅ | Area display label |
| **icon** | `string` | optional | Area icon name |
| **description** | `string \| Record<string, string>` | optional | Area description |
| **navigation** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items within this area |
| **navigation** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items within this area |


---
Expand All @@ -220,7 +220,7 @@ A navigation contribution: a package injecting nav items into an app it does not
| **app** | `string` | ✅ | Target app name to contribute navigation into (e.g. "setup") |
| **group** | `string` | optional | Target group nav-item id to append into (e.g. "group_integrations"); omit to append at the app top level |
| **priority** | `integer` | optional | Merge priority within the target group — lower applied first (matches object extender priority) |
| **items** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| … +5 more)[]` | ✅ | Navigation items contributed into the target app/group |
| **items** | `({ id: string; label: string \| Record<string, string>; icon?: string; order?: number; … } \| { type: 'separator'; id?: string; order?: number } \| … +7 more)[]` | ✅ | Navigation items contributed into the target app/group |


---
Expand Down
Loading
Loading