Skip to content

Commit 5d3ff11

Browse files
committed
Merge origin/main (2d5fe76) into claude/issue-20771-wall-clock-one-rule
Claude-Session: https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY Co-authored-by: Claude <noreply@anthropic.com>
2 parents aebac66 + 2d5fe76 commit 5d3ff11

82 files changed

Lines changed: 1476 additions & 214 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.

‎.claude/skills/pm-dispatch/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ PM 的工作是循环:选卡 → 认领 → 派发 → 收集 → 复核 → 报
199199
- 该账户改为把 PR assign 给它替代通知,另一账户照常请审;轮次报告点名说明走了兜底。
200200
- 请审走免碰 draft 位的 REST 专用路,ready/draft 走中继 op;MCP 兜底已拒;端点见 rest-channel。
201201
- ④ 轮次报告单列 awaiting a human merge。
202-
- 已入队才读到本条 ⇒ 转 draft 与 disable 都做;出队以阳性探针答,ref 缺席只旁证。
202+
- 已入队 ⇒ 转 draft 并 disable,即请维护者手动出队(席位无实测手段);出队以阳性探针答。
203203
- skills 车道自有 PR:纯代码面如 `scripts/pm/` 由本席按达档自审(清单不减)后落地。
204204
- 受管面两层:Tier S = `.claude/**` 全树,余皆 Tier H 等人批;S 经达档复核 PASS 后 ready → 入队。
205205
- 路径面干净的才转 ready → 入队;队列是唯一被认可的落地路径,⛔ 永不队列外合并。

‎.claude/skills/pm-dispatch/references/platform-readings.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@
3838
- 状态核验用最小字段(search/list 加 `fields`)或等事件。
3939
- 聚合读数(`blocked`/`dirty`)只作阴性筛查再定位;放行按名定向读单条 job,⛔ 不拉全表。
4040
- 按名定位失败才拉全表;订阅来的 `check_suite.completed` 是唤醒不是放行读数。
41-
- 转 draft 不是可靠的踢队手段:两向相反读数并存,处置按最坏走。
42-
- 本仓转 draft 同时掉 auto-merge 与队列成员资格,不自动恢复,转正后重挂;姊妹仓曾保位照合。
43-
- 补救:转 draft 与卸载 auto-merge 都做 —— 本仓卸载 auto-merge 单独不踢队。
41+
- 转 draft 不是可靠的踢队手段:本仓与姊妹仓均见已入队转 draft 仍保位照合,处置按最坏走。
42+
- 本仓转 draft 同秒掉 auto-merge 仅见于未入队时,转正后重挂;已入队者 2026-08-08、09-29 照合。
43+
- 补救:转 draft 与卸载 auto-merge 都做,本仓卸载单独不踢队;两手齐做亦未见踢队(08-08、09-28)。
4444
- 出队按队列 ref 阳性探针答不在队向,加未落地;ref 缺席只作旁证,⛔ 永不承载结论。
4545
- `update_pull_request` 单字段调用也发送 `draft` 位,曾把治理面 draft 发进队列 ⇒ 锁 1 已拒。
4646
- 请审专用路 = REST `POST /pulls/{n}/requested_reviewers`,载荷只有 reviewers/team_reviewers。

‎.claude/skills/spec-property-retirement/SKILL.md‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,9 @@ ratchet(#2978)会先开火,要求你**有意删除**对应的 manifest key;删
138138
为 #4650
139139
路径 3(`def no longer emitted by this build`)。这串输出本身就是路线的证据,留在 PR 里。
140140

141-
枚举值收窄既然对四张 ratchet 不可见,它的处方就只能挂在**枚举自己的 `error` map**
142-
上、按 `issue.input` 分派(`packages/spec/src/data/hook-body.zod.ts` 的
143-
`HookBodyCapability`,沿用 `object.managedBy: 'system'` 的先例)—— 三条路线里没有一条
144-
适用于「def 存活、只少一个值」。
141+
枚举值收窄既然对四张 ratchet 不可见,它的处方就只能挂在**枚举自己**上:用
142+
`enumWithRetiredValues()`(`shared/retired-key.ts`)声明退役值及其处方,⛔ 不再手写 `error` map ——
143+
三条路线里没有一条适用于「def 存活、只少一个值」。
145144

146145
### guidance 字符串怎么写
147146

@@ -180,11 +179,12 @@ conversion 是消费者跟的;D3 条目是升级方的 agent 读的。三个都
180179
个 `fixture`,其 `expectedNotices` 等于**键**数,不是条目数。walker
181180
(`mapCollection`、`mapFlowNodes`、`renameKey`)住在 `conversions/walk.ts`,
182181
copy-on-write —— 没命中就原样返回输入引用。
183-
- [ ] **一条 `RETIRED_KEYS_BY_MAJOR` 条目**,在
184-
`packages/spec/src/migrations/registry.ts` —— 字面 `'<defKey>:<name>'`,拼法照
182+
- [ ] **一条 `RETIRED_KEYS_BY_MAJOR` 条目**,在 `packages/spec/src/migrations/entries/retired-keys/`
183+
加一个文件(命名见 `entries/README.md`),再跑 `pnpm --filter @objectstack/spec gen:migration-registry`
184+
把它生成进 `registry.ts`,⛔ 永不在那里手打。条目值是字面 `'<defKey>:<name>'`,拼法照
185185
`authorable-surface/<category>.json`(去掉 `[RETIRED]` 标),挂在本 major 下。
186-
这就是门 (b) 按精确集合成员读的字符串;不做推断,也不从邻键辐射。门禁的失败输
187-
出会打印该粘贴的那一行。⚠ **不要**在墓碑落地前先加条目:点名一个还活着的键的
186+
这就是门 (b) 按精确集合成员读的字符串;不做推断,也不从邻键辐射。
187+
⚠ **不要**在墓碑落地前先加条目:点名一个还活着的键的
188188
条目,会以「无人消费的注册」打红门 (b2)。
189189
*为什么要第二张表:* #4659 之前门 (b) 拿键的**叶名**去比对每条已注册的
190190
`surface`(`endsWith('.' + name)`,全 major,def 忽略),于是 `dashboard.aria`

‎AGENTS.md‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -282,9 +282,10 @@ localStorage / auth gotchas.
282282
governed-surface PR, under any account** — an authorized account is agent-operated too; "CI is green" carries no
283283
information about a governance change.
284284

285-
**Already armed or queued when you read this?** Convert it back to **draft** AND disable auto-merge — draft is
286-
what removes queue membership, disabling alone drops only the arming — then confirm from the remote that it is in
287-
neither the queue nor `origin/main`. **Draft is no barrier by itself — the barrier is this directive**, and a
285+
**Already armed or queued when you read this?** Disable auto-merge AND convert it back to **draft**, which disarms
286+
an unqueued PR; no seat act, those two included, is measured to dequeue one (`platform-readings.md`, queue
287+
membership), so ask the maintainer at once to remove a queued PR by hand — then confirm from the remote that it is
288+
in neither the queue nor `origin/main`. **Draft is no barrier by itself — the barrier is this directive**, and a
288289
spent approval or a standing record IS the review record, ⛔ not a relaxation. Behind it: the queue guard refuses a
289290
governed diff without its tier's record; CODEOWNERS routes review requests for `docs/adr/` only, so nothing summons
290291
the maintainer on the other Tier H surfaces; the post-merge audit (`scripts/pm/check-governed-merges.mjs`) lists
@@ -425,8 +426,8 @@ write; never a token in a remote URL, a `git config` value on disk or a log line
425426
it.** Under one shared identity every other participant's write arrives unsigned: the PM
426427
flipping your draft to ready and arming auto-merge, a bot re-labelling, the platform
427428
rewriting your body. A rewritten body is evidence about the body and of nothing else —
428-
⛔ never extend it to the draft flag, which flipped back destroys auto-merge and queue
429-
membership at once (§7's draft-flip re-arm note), invisibly. Read the timeline event's
429+
⛔ never extend it to the draft flag, which flipped back destroys an unqueued PR's
430+
auto-merge at once (§7's draft-flip re-arm note), invisibly. Read the timeline event's
430431
actor, or ask; undo only once you know who set it and why.
431432

432433
**Write the attribution footer in the form the surface keeps — blank line, rule, ONE footer line:**
@@ -529,8 +530,8 @@ Even inside your own worktree, operate defensively:
529530
known-flaky signature, then re-arm once, never reflexively; **collateral eviction is
530531
silent** (triage comments only on `failure`, so an entry cancelled because something
531532
*ahead* failed gets nothing) — neither on `main` nor in the queue means dropped, re-arm;
532-
**flipping back to draft drops auto-merge and queue membership at once**, and neither
533-
returns by itself — ready *first*, arm *second*. One non-fix: **a stale red does not
533+
**flipping back to draft drops an unqueued PR's auto-merge at once**, and it does not
534+
return by itself — ready *first*, arm *second*. One non-fix: **a stale red does not
534535
clear by re-running** — `rerun_failed_jobs` reuses the original run's commit and merge
535536
ref, so a fix that landed on `main` since is invisible to it; only a new commit
536537
(`git merge origin/main`) helps. Whether a direct `gh pr merge` is refused here is

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

0 commit comments

Comments
 (0)