Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
79eca49
wip(spec): picklist kind, Field picklist reference, translation face
claude Sep 30, 2026
5781860
wip(spec): picklist tests, Field.select overloads
claude Sep 30, 2026
c9f60fe
wip(spec): picklist liveness ledger rows, fixture triage, count pins
claude Sep 30, 2026
1c111c3
wip(spec): Field.select overload order, generated build outputs
claude Sep 30, 2026
205400b
wip(spec): regenerate meta-url spelling, export origins, declaration …
claude Sep 30, 2026
40bd262
wip(spec): regenerate api-surface and reference docs
claude Sep 30, 2026
ba32d57
wip: keep neither at the completeness gate; census pins in lint and m…
claude Sep 30, 2026
435ba62
chore(changeset): picklist kind, minor
claude Sep 30, 2026
df990ee
Merge remote-tracking branch 'origin/main' into claude/issue-19518-pi…
claude Sep 30, 2026
b1f9dad
chore: account for the picklist kind in the showcase kind coverage an…
claude Sep 30, 2026
7b1807b
docs(concepts): the metadata lifecycle page counts 28 registry types
claude Sep 30, 2026
0e70e85
chore: reconcile picklist collections in the stack-collection maps ga…
claude Sep 30, 2026
569b88a
chore(platform-objects): picklist type label in the metadata-forms tr…
claude Sep 30, 2026
3a85fdf
Merge remote-tracking branch 'origin/main' into claude/issue-19518-pi…
claude Sep 30, 2026
45dddd7
chore(spec): regenerate api-surface and export-origins over the merge…
claude Sep 30, 2026
4049ae3
docs(skills): objectstack-platform lists picklists and picklistExtens…
claude Sep 30, 2026
c9fe504
feat(cli): os i18n extract walks picklists.NAME.{label, options.VALUE}
claude Sep 30, 2026
e19f903
Merge remote-tracking branch 'origin/main' into claude/issue-19518-pi…
claude Sep 30, 2026
12592f1
fix(driver-sql): classify FieldSchema.picklist as a presentation key
claude Sep 30, 2026
a1f0668
test: count the picklist kind in the census pins that enumerate regis…
claude Sep 30, 2026
56d7584
docs(spec): open the picklist module doc on its description, not a le…
claude Sep 30, 2026
14b1f68
chore(changeset): name the cli and driver-sql changes in the picklist…
claude Sep 30, 2026
12896a0
Merge remote-tracking branch 'origin/main' into claude/issue-19518-pi…
claude Sep 30, 2026
388dcce
chore(spec): regenerate the API protocol reference over the merged tree
claude Sep 30, 2026
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
19 changes: 19 additions & 0 deletions .changeset/19518-picklist-kind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@objectstack/spec': minor
'@objectstack/platform-objects': patch
'@objectstack/cli': patch
'@objectstack/driver-sql': patch
---

feat(spec): the `picklist` metadata kind — a shared option list that select fields reference by name (#19518)

Clause-②: yes (widening)

- **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.
- **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.
- **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.
- **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.
- **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.
- **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).
- **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`.
- **SQL driver.** The SQL driver classifies the `picklist` field key as presentation, so it adds no column.
2 changes: 1 addition & 1 deletion content/docs/concepts/metadata-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ In shared-database multi-tenancy, **most metadata types must not be per-org cust
| `datasource` | ❌ | Connection strings; multi-tenant isolation is enforced at a higher layer. (`allowRuntimeCreate: true` — the datasource wizard persists `origin: 'runtime'` rows.) |
| `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`. |

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`.
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`.

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()`.

Expand Down
2 changes: 1 addition & 1 deletion content/docs/getting-started/quick-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Categories that have no section here at all are named under
[Categories Without a Section](#categories-without-a-section) — that curation is stated,
not left implicit.

## Data Protocol (16 of 29 schemas)
## Data Protocol (16 of 30 schemas)

Core business logic and data modeling schemas.

Expand Down
5 changes: 3 additions & 2 deletions content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **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 |
| **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 |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -715,7 +715,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| 'datasource' \| … +12 more>` | ✅ | Metadata type |
| **type** | `Enum<'object' \| 'field' \| 'hook' \| 'seed' \| 'mapping' \| 'picklist' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'dataset' \| 'flow' \| 'job' \| … +13 more>` | ✅ | Metadata type |
| **name** | `string` | ✅ | Item name (snake_case) |
| **data** | `Record<string, any>` | ✅ | Metadata payload |
| **namespace** | `string` | optional | Optional namespace |
Expand All @@ -727,6 +727,7 @@ Metadata query with filtering, sorting, and pagination
* `hook`
* `seed`
* `mapping`
* `picklist`
* `view`
* `page`
* `dashboard`
Expand Down
8 changes: 6 additions & 2 deletions content/docs/references/api/package-api-assembled.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,10 @@ Installed package row whose manifest is the assembled package body
| **integrity** | `Record<string, string>` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
| **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 |
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
| **translations** | `Record<string, { objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }>[]` | optional | I18n Translation Bundles |
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |
| **views** | `{ name?: string; label?: string \| Record<string, string>; object?: string; list?: object; … }[]` | optional | List Views |
| **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. |
Expand Down Expand Up @@ -352,8 +354,10 @@ Installed package row whose manifest is the assembled package body
| **integrity** | `Record<string, string>` | optional | Per-file content digests of the plugin artifact (ADR-0025 §3.2) |
| **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 |
| **datasourceMapping** | `{ namespace?: string; package?: string; objectPattern?: string; default?: boolean; … }[]` | optional | Centralized datasource routing rules for packages/namespaces/objects |
| **translations** | `Record<string, { objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }>[]` | optional | I18n Translation Bundles |
| **translations** | `Record<string, { objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }>[]` | optional | I18n Translation Bundles |
| **objectExtensions** | `{ extend: string; fields?: Record<string, object>; label?: string; pluralLabel?: string; … }[]` | optional | Extensions to objects owned by other packages |
| **picklists** | `{ name: string; label: string; description?: string; options: object[]; … }[]` | optional | Shared option lists that select fields reference by name |
| **picklistExtensions** | `{ extend: string; options: object[] }[]` | optional | Options added to picklists owned by other packages (additive only) |
| **apps** | `{ name: string; label: string \| Record<string, string>; description?: string \| Record<string, string>; icon?: string; … }[]` | optional | Applications |
| **views** | `{ name?: string; label?: string \| Record<string, string>; object?: string; list?: object; … }[]` | optional | List Views |
| **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. |
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/api/protocol.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1602,13 +1602,14 @@ The published metadata item body, opaque by ruling (1C). Shape is the item's own
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **locale** | `string` | ✅ | Locale code |
| **translations** | `{ objects?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; globalActions?: Record<string, object>; … }` | ✅ | Translation data |
| **translations** | `{ objects?: Record<string, object>; picklists?: Record<string, object>; apps?: Record<string, object>; messages?: Record<string, string>; … }` | ✅ | Translation data |

### Nested Shape: `GetTranslationsResponse.translations`

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **objects** | `Record<string, { label?: string; pluralLabel?: string; description?: string; fields?: Record<string, object>; … }>` | optional | Object translations keyed by object name |
| **picklists** | `Record<string, { label?: string; options: Record<string, string> }>` | optional | Picklist translations keyed by picklist name |
| **apps** | `Record<string, { label: string; description?: string; navigation?: Record<string, object> }>` | optional | App translations keyed by app name |
| **messages** | `Record<string, string>` | optional | UI message translations keyed by message ID |
| **globalActions** | `Record<string, { label?: string; description?: string; confirmText?: string; successMessage?: string; … }>` | optional | Global action translations keyed by action name |
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/field.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ const result = CurrencyConfigSchema.parse(data);
| **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. |
| **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. |
| **options** | `{ label: string; value: string; description?: string; color?: string; … }[]` | optional | Static options for select/multiselect |
| **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`. |
| **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. |
| **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. |
| **deleteBehavior** | `Enum<'set_null' \| 'cascade' \| 'restrict'>` | optional (default: `"set_null"`) | What happens if referenced record is deleted |
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/data/index.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Data Protocol — complete schema reference
navTitle: Data Protocol
description: "The ObjectStack Data Protocol in 29 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
description: "The ObjectStack Data Protocol in 30 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example."
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand Down Expand Up @@ -34,6 +34,7 @@ This section contains all protocol schemas for the data layer of ObjectStack.
<Card href="/docs/references/data/hook-body" title="Hook Body" description="Source: packages/spec/src/data/hook-body.zod.ts" />
<Card href="/docs/references/data/mapping" title="Mapping" description="Source: packages/spec/src/data/mapping.zod.ts" />
<Card href="/docs/references/data/object" title="Object" description="Source: packages/spec/src/data/object.zod.ts" />
<Card href="/docs/references/data/picklist" title="Picklist" description="Source: packages/spec/src/data/picklist.zod.ts" />
<Card href="/docs/references/data/query" title="Query" description="Source: packages/spec/src/data/query.zod.ts" />
<Card href="/docs/references/data/seed" title="Seed" description="Source: packages/spec/src/data/seed.zod.ts" />
<Card href="/docs/references/data/seed-loader" title="Seed Loader" description="Source: packages/spec/src/data/seed-loader.zod.ts" />
Expand Down
3 changes: 2 additions & 1 deletion content/docs/references/data/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"driver-postgres",
"driver-sqlite",
"driver-turso",
"field-value"
"field-value",
"picklist"
]
}
Loading
Loading