Skip to content

Commit addbbf0

Browse files
feat(spec): the picklist metadata kind — a shared option list select fields reference by name (#19518) (#20823)
Part of #19518 Clause-②: yes The spec layer of the shared picklist: - the `picklist` kind: registered, loads before `object`, package-owned; - `Field.select({ picklist })`, refused when `options` is also declared; - the served shape `PicklistServedFieldSchema`; - `picklistExtensions`; - the `picklists.NAME` translation face and its resolvers. Two gates this PR must keep green are also carried here: - the published platform skill lists the two new top-level keys (`check:skill-top-level-keys`); - `os i18n extract` walks `picklists.NAME.{label, options.VALUE}` (`check:i18n-walk-parity`). Every pin that enumerates registered kinds or `FieldSchema` keys moves with the kind. That includes `driver-sql`'s column-collision classification. Resolving the reference at runtime is #19519. Until then the liveness ledger grades the new keys `planned`. Refusing a `select` / `radio` with neither `options` nor `picklist` is #20827 (ruled A, after this PR). The CLI compile / validate path is #20825. #19518 stays open for its Tier H docs PR: the NORTH-STAR line and the two `records-forms` checklist items. ## 维护者速读 **改了什么:** `skills/objectstack-platform/SKILL.md` 的顶层键清单加上 `picklists`、`picklistExtensions` 两个键(第 43 行起)。同一文件 CLI 小节的一句指路文字(第 453 行附近)删掉两处: - 已失真的「below」:Part 3 已由 `595621d35c` 拆到 `references/operations.md`,不在本页下方; - 与 cheat sheet 重复的「High-level」。 **读数:** 整文件 489 行不变;token 5829 → 5827,上限 5833;技能包总行数 4396 不变。`check-governed-merges`:75 个路径中 1 个命中登记(`skills/**`),所以是 Tier H;共 1564 行改动,低于 5000。 **为什么改:** 本 PR 给 stack 新增了这两个顶层键。必需门禁 `check:skill-top-level-keys` 要求技能页的清单与 schema 一致;页面上没列的键,AI 作者就不会写。 **风险与回滚:** 只改文字,链接目标不变,作者需要的内容一处没删。回滚办法是 revert `4049ae31ba`,但若不同时去掉这两个 stack 键,该门禁会重新变红。 **席位意见:** 建议批准。复核 PASS 后,席位在本 PR 上贴出记录。 **你要做的:** 看 SKILL.md 的两处改动,然后给出授权的 APPROVED 审阅,由席位落地;也可以亲手合并。 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 6c96b37 commit addbbf0

75 files changed

Lines changed: 1432 additions & 132 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.changeset/19518-picklist-kind.md‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/platform-objects': patch
4+
'@objectstack/cli': patch
5+
'@objectstack/driver-sql': patch
6+
---
7+
8+
feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518)
9+
10+
Clause-②: yes (widening)
11+
12+
- **The kind.** `PicklistSchema` — `{ name, label, description?, options }`, where `options` is the field option shape (`SelectOptionSchema`) reused as is. Authored in a package as `*.picklist.ts` (`definePicklist`) or `defineStack({ picklists })`. It is a registered kind (`MetadataTypeSchema`, `DEFAULT_METADATA_TYPE_REGISTRY`, `getMetadataTypeSchema('picklist')`) that loads before `object`. It is package-owned, so a runtime create or a per-organization overlay is refused.
13+
- **The reference.** `Field.select({ picklist: 'industry' })` adds a `picklist` key to `FieldSchema`. It is valid on the option types only (select, radio, multiselect, checkboxes, tags). A field that declares both `picklist` and `options` is refused at `options`, with a prescription. The functional-completeness predicate counts a `picklist` reference as the field's option source.
14+
- **The served shape.** `PicklistServedFieldSchema` declares what a client reads for a picklist-bound field: the resolved `options` next to the `picklist` that names the list. This release does not resolve the reference. Until the runtime does, a picklist-bound field is served without options, and the liveness ledger grades the key `planned` and warns an author who writes it.
15+
- **Extensions.** `defineStack({ picklistExtensions: [{ extend, options }] })` adds options to a picklist that another package owns. It can only add; removing or renaming a value stays with the owning package.
16+
- **Translation.** `TranslationData` gains `picklists.<name>.{ label?, options: { value: label } }`. `translatePicklist` translates a served picklist item. `translateObject` gives a picklist-bound field the list's option labels, and a field-level `options` entry still wins over them.
17+
- **Studio type label.** `@objectstack/platform-objects` carries the `picklist` type's label and description in its metadata-forms translation bundles (en, zh-CN, ja-JP, es-ES).
18+
- **Extraction.** `os i18n extract` walks `picklists.NAME.{label, options.VALUE}`, including an extension's options under the list it extends, and `os lint` reports an untranslated option under its own rule, `i18n/missing-picklist`.
19+
- **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column.

‎content/docs/concepts/metadata-lifecycle.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ In shared-database multi-tenancy, **most metadata types must not be per-org cust
115115
| `datasource` | ❌ | Connection strings; multi-tenant isolation is enforced at a higher layer. (`allowRuntimeCreate: true` — the datasource wizard persists `origin: 'runtime'` rows.) |
116116
| `job` | ❌ | **Also `allowRuntimeCreate: false` since protocol 17** (#4509). `JobSchema.handler` names a function in the compiled bundle's function table, which a runtime writer has no way to reach — so a job created in Studio or through `PUT /meta` parsed, saved, reported success and was never scheduled. The door is closed rather than bridged: `job` stays first-class through `*.job.ts` / `defineStack({ jobs, functions })`, where every schedule shape, `retryPolicy` and `timeout` does reach the scheduler. Existing rows are untouched — they were never scheduled — and `migrateStoredMetadata` reports them `skipped`. |
117117

118-
Those five are the **complete** `allowOrgOverride: true` set: of the 27 types in `DEFAULT_METADATA_TYPE_REGISTRY`, every other one is `false`. The ❌ rows above are the `false` types whose *second* tier (`allowRuntimeCreate`) is worth calling out; any type not listed is `allowOrgOverride: false`.
118+
Those five are the **complete** `allowOrgOverride: true` set: of the 28 types in `DEFAULT_METADATA_TYPE_REGISTRY`, every other one is `false`. The ❌ rows above are the `false` types whose *second* tier (`allowRuntimeCreate`) is worth calling out; any type not listed is `allowOrgOverride: false`.
119119

120120
There is no `workflow` metadata type (per [ADR-0020](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0020-state-machine-converge-and-enforce.md), record state machines are a `state_machine` validation). Nor is there a standalone `validation` type any more — it was retired in protocol 17 under [ADR-0088](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0088-metadata-kind-admission-and-retirement.md) because `ValidationRuleSchema` carries no object-binding key, so a rule authored through that door could never say what it protected; author rules in the object's own `validations[]` instead. The runtime gate is implemented in `OVERLAY_ALLOWED_TYPES` (derived from the registry) and enforced by `SysMetadataRepository.put()`.
121121

‎content/docs/getting-started/quick-reference.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ Categories that have no section here at all are named under
2222
[Categories Without a Section](#categories-without-a-section) — that curation is stated,
2323
not left implicit.
2424

25-
## Data Protocol (16 of 29 schemas)
25+
## Data Protocol (16 of 30 schemas)
2626

2727
Core business logic and data modeling schemas.
2828

‎content/docs/references/api/metadata.mdx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -648,7 +648,7 @@ Metadata query with filtering, sorting, and pagination
648648

649649
| Property | Type | Required | Description |
650650
| :--- | :--- | :--- | :--- |
651-
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
651+
| **types** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'api' \| 'email_template' \| 'doc' \| 'book' \| 'permission' \| 'position' \| 'capability' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
652652
| **namespaces** | `string[]` | optional | Filter by namespaces |
653653
| **packageId** | `string` | optional | Filter by owning package |
654654
| **search** | `string` | optional | Full-text search query |
@@ -715,7 +715,7 @@ Metadata query with filtering, sorting, and pagination
715715

716716
| Property | Type | Required | Description |
717717
| :--- | :--- | :--- | :--- |
718-
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| … +12 more>` | ✅ | Metadata type |
718+
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +13 more>` | ✅ | Metadata type |
719719
| **name** | `string` | ✅ | Item name (snake_case) |
720720
| **data** | `Record<string, any>` | ✅ | Metadata payload |
721721
| **namespace** | `string` | optional | Optional namespace |
@@ -727,6 +727,7 @@ Metadata query with filtering, sorting, and pagination
727727
* `hook`
728728
* `seed`
729729
* `mapping`
730+
* `picklist`
730731
* `view`
731732
* `page`
732733
* `dashboard`

‎content/docs/references/api/package-api-assembled.mdx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,10 @@ Installed package row whose manifest is the assembled package body
110110
| **integrity** | `Record<string, string>` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
111111
| **functions** | `Record<string, string \| { handler?: string; effect?: Enum<'pure' \| 'writes'> }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
112112
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
113-
| **translations** | `Record<string, { objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }>[]` | optional | I18n Translation Bundles |
113+
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
114114
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
115+
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
116+
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
115117
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |
116118
| **views** | `{ name?: string; label?: string \| Record<string, string>; object?: string; list?: object; … }[]` | optional | List Views |
117119
| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |
@@ -352,8 +354,10 @@ Installed package row whose manifest is the assembled package body
352354
| **integrity** | `Record<string, string>` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
353355
| **functions** | `Record<string, string \| { handler?: string; effect?: Enum<'pure' \| 'writes'> }> \| { name: string; handler?: string; packageId?: string; effect?: Enum<'pure' \| 'writes'> }[]` | optional | Named handler functions, lowered to the refs a JSON document carries |
354356
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
355-
| **translations** | `Record<string, { objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }>[]` | optional | I18n Translation Bundles |
357+
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
356358
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
359+
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
360+
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
357361
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |
358362
| **views** | `{ name?: string; label?: string \| Record<string, string>; object?: string; list?: object; … }[]` | optional | List Views |
359363
| **viewItems** | `never` | optional | [MACHINE-ASSEMBLED] Non-container view artifacts of a runtime-assembled manifest (standalone ViewItems, flattened overlays) — written by package export and artifact factories, refused in authored stack sources. |

‎content/docs/references/api/protocol.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1602,13 +1602,14 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
16021602
| Property | Type | Required | Description |
16031603
| :--- | :--- | :--- | :--- |
16041604
| **locale** | `string` | ✅ | Locale code |
1605-
| **translations** | `{ objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }` | ✅ | Translation data |
1605+
| **translations** | `{ objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }` | ✅ | Translation data |
16061606

16071607
### Nested Shape: `GetTranslationsResponse.translations`
16081608

16091609
| Property | Type | Required | Description |
16101610
| :--- | :--- | :--- | :--- |
16111611
| **objects** | `Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, object>; … }>` | optional | Object translations keyed by object name |
1612+
| **picklists** | `Record<string, { label?: string; options: Record<string, string> }>` | optional | Picklist translations keyed by picklist name |
16121613
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, object> }>` | optional | App translations keyed by app name |
16131614
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
16141615
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |

‎content/docs/references/data/field.mdx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ const result = CurrencyConfigSchema.parse(data);
7575
| **accept** | `string[]` | optional | Permitted upload types for media fields, as MIME types or extensions (e.g. ["image/*", ".pdf"]). Offered to the file picker AND enforced on write. |
7676
| **maxSize** | `integer` | optional | Maximum permitted file size in BYTES for media fields. Enforced on write against the stored file size, not just checked in the browser. |
7777
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
78+
| **picklist** | `string` | optional | Name of a shared `picklist` whose options this field offers — instead of `options`, never with it. Option types only (select, radio, multiselect, checkboxes, tags). The server resolves the reference: the field clients read carries the resolved `options`. |
7879
| **reference** | `string` | optional | Target object name (snake_case) for lookup/master_detail fields. Required for relationship types. Used by $expand to resolve foreign key IDs into full objects. On a `tree` field it is optional and, if given, must be the declaring object's own name — the object schema refuses any other target. |
7980
| **referenceVia** | `string` | optional | Declares this text field as the id half of a polymorphic pointer pair (ADR-0052 §5 ActivityPointer): the value is a record id of the object named by the SIBLING FIELD this key names — e.g. `record_id` with `referenceVia: 'object_name'`. The sibling must be a declared field on the same object holding an object machine name. Text fields only; mutually exclusive with `reference` (a static and a per-record target contradict). Enforced today at seed load: the value resolves as a natural key against the object the sibling column names, and an unresolvable pointer is refused loudly instead of stored verbatim. Adds no referential integrity or $expand behavior. |
8081
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |

‎content/docs/references/data/index.mdx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
title: Data Protocol — complete schema reference
33
navTitle: Data Protocol
4-
description: "The ObjectStack Data Protocol in 29 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
4+
description: "The ObjectStack Data Protocol in 30 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
55
---
66

77
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
@@ -34,6 +34,7 @@ This section contains all protocol schemas for the data layer of ObjectStack.
3434
<Card href="/docs/references/data/hook-body" title="Hook Body" description="Source: packages/spec/src/data/hook-body.zod.ts" />
3535
<Card href="/docs/references/data/mapping" title="Mapping" description="Source: packages/spec/src/data/mapping.zod.ts" />
3636
<Card href="/docs/references/data/object" title="Object" description="Source: packages/spec/src/data/object.zod.ts" />
37+
<Card href="/docs/references/data/picklist" title="Picklist" description="Source: packages/spec/src/data/picklist.zod.ts" />
3738
<Card href="/docs/references/data/query" title="Query" description="Source: packages/spec/src/data/query.zod.ts" />
3839
<Card href="/docs/references/data/seed" title="Seed" description="Source: packages/spec/src/data/seed.zod.ts" />
3940
<Card href="/docs/references/data/seed-loader" title="Seed Loader" description="Source: packages/spec/src/data/seed-loader.zod.ts" />

‎content/docs/references/data/meta.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"driver-postgres",
3535
"driver-sqlite",
3636
"driver-turso",
37-
"field-value"
37+
"field-value",
38+
"picklist"
3839
]
3940
}

0 commit comments

Comments
 (0)