Skip to content

feat(picklists): the shared option lists become picklist metadata (#2000) - #2111

Merged
objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-2000-picklist-metadata
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 8 commits into
mainfrom
claude/issue-2000-picklist-metadata

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #2000
Clause-②: no

The five option lists that more than one select field offers become platform picklist metadata (objectstack#18164), each field references its list by name with Field.select({ picklist }), and src/sales/objects/_picklists.ts is deleted. This is the finished 17.x branch (c8f331cf) with main (4de5343e, @objectstack/* 18.0.0-next.1) merged in, never rebased, and re-verified on the new pin. The platform change it waited for, objectstack-ai/objectstack#22178 (analytics dimension labels over a picklist-bound field), is in the pin: measured below.

What changes

Picklist Package Fields that reference it
industry sales crm_lead.industry, crm_account.industry
lead_source sales crm_lead, crm_contact, crm_opportunity .lead_source
salutation sales crm_lead, crm_contact .salutation
related_to_type sales crm_task, crm_event .related_to_type
payment_terms revenue crm_quote, crm_contract .payment_terms
  • Registration. Each package has an explicit barrel (src/sales/picklists/index.ts, src/revenue/picklists/index.ts). objectstack.composition.ts collects them as appPicklists and hands them to the app package stack (createHotCrmAppStack). Every referencing field is an app-package field; the service package references none, so it registers none. objectstack.config.ts is main's, unchanged.
  • Single-field lists return inline to their field: crm_opportunity.stage, crm_task.type, crm_event.type, crm_event.status, crm_event_attendee.response, crm_lead.need_type, crm_lead.duplicate_of_type. Their rationale comments moved with them.
  • Translations. In all four packs, the option labels of the 11 picklist-bound fields moved from the field to picklists.NAME (in LOCALE/app.ts). Field labels stay on the field. The ja-JP shared constants for these lists are gone.
  • Names are plain snake_case with no crm_ prefix: defineStack's prefix rule covers objects only, and the lint issue set is identical to main with these names.

Equivalence with main, measured mechanically

Run on the merged tree against an export of origin/main (4de5343e):

  • Objects: for all 18 objects and every field, the options each field offers are byte-identical to main's. For the 11 picklist-bound fields, the options are resolved through the platform's SchemaRegistry.resolvePicklistOptions. Every other field property and every object-level property is identical. Result: 0 diffs.
  • Locale packs: 384 option labels compared. Every label a picklist-bound field served on main equals the one picklists.NAME serves now. No picklist-bound field keeps field-level options. With those moved keys set aside, each pack is identical to main's. Result: 0 mismatches.
  • Every other collection of the composed app is identical to main's: actions, flows, views, pages, dashboards, reports, datasets, mappings, hooks, apps, permissions, sharing rules, seed data and the rest. picklists is the only new key.

Merge onto main (v18): conflicts and resolutions

File Resolution
objectstack.config.ts main's composeStacks artifact; registration moved to objectstack.composition.ts (appPicklists into the app stack)
objectstack.composition.ts main's two-stack structure + appPicklists
src/sales/objects/_picklists.ts (modify/delete) stays deleted; main's two comment edits (#2035, #2040) carried to where the comments live now (payment_terms.picklist.ts names quote_accepted_contract_draft)
src/revenue/objects/contract.object.ts main's hook name + the picklist reference
src/sales/views/opportunity.view.ts main's (the stage rowColor map is gone, #2108)
src/sales/dashboards/executive.dashboard.ts, src/sales/mappings/_shared.ts the branch's comments
8 engine suites built with ObjectQL.create (attendee-type-resolution, campaign-member-cascade, event-attendee-cascade, flow-variable-conditions, hook-org-inheritance, line-item-cascade, opportunity-line-item-add-picker, win-loss-capture) main's drivers, imports and syncSchemas(), plus registerStackPicklists(ql); the factory still takes no picklists on 18.0.0-next.1
global-actions, i18n-references, quote-accepted-payment-terms, view-references main's @objectstack/verify versions. The branch's additions moved onto the handle. No stand-in was re-grown. global-actions reads related_to_type off the booted registry's served field (verify.metadata.object). i18n-references checks picklist translation keys against runtime.metadata.items('picklist').
actions-flows-integrity both non-vacuity guards kept (main's copy counter and the branch's checked list)
import-mappings, placeholder-picklist-options main's imports + the branch's picklist reads

test/helpers/stack-picklists.ts now reads the composed view (the artifact carries packages[] only). It resolves a field's offered options through the platform's SchemaRegistry rather than a local lookup.

Scope item 2: every non-field consumer

Consumer Disposition
src/sales/views/lead.view.ts form subset imports DUPLICATE_OF_TYPE_AUTHORABLE_OPTIONS from lead.object.ts
src/sales/flows/schedule-followup.flow.ts screen options = Task.fields.type.options, projected to label/value (a screen option entry is strict)
src/sales/actions/opportunity.actions.ts mass_update_stage param = Opportunity.fields.stage.options, projected; pinned in view-references
src/sales/dashboards/executive.dashboard.ts lead_source filter roster = LeadSourcePicklist.options
src/sales/mappings/_shared.ts, src/sales/objects/lead.hook.ts, src/sales/actions/activity-actions.ts comments repointed
src/sales/flows/lead-conversion.flow.ts (arrived with #2068) comment repointed: the two selects a converted contact copies reference the shared lists
tests as in the merge table; do-not-call-enforcement comment repointed (named the retired plainOptions)

Scope item 4. DUPLICATE_OF_TYPE_AUTHORABLE_OPTIONS and DUPLICATE_OF_TYPE_ERASED are kept and moved into lead.object.ts beside their field. The authorable/erased split is the enforcement, and the form subset is not a field's option set, so it has no second field to share with. DUPLICATE_OF_TYPE_OPTIONS is retired: it is now the field's own options. plainOptions is retired: its two callers project the field they mirror. Scope item 5. _picklists.ts is deleted.

Live boot on 18.0.0-next.1: records-forms.picklist-shared-across-objects

objectstack dev -p 4120 on a fresh DB at 2b4eb38b (the source tree of the head below, which only adds a changeset sentence). Probes: REST with Accept-Language per locale, and Chromium DOM probes in the Console.

  • Clause 1, same resolved options. GET /api/v1/meta/picklist lists industry, lead_source, payment_terms, related_to_type, salutation.
    • Identical served options on every referencing object, with picklist kept, in 20 of 20 cells (5 lists × 4 locales).
    • Every served field parses as PicklistServedFieldSchema: 44 of 44 (11 fields × 4 locales).
    • Form enumeration: on the create form of crm_account, crm_lead, crm_contact and crm_opportunity, each picklist-bound select was checked in zh-CN and en. Its native options and the opened listbox both equal the served set in 14 of 14. Two fields sit on a form tab, which was opened first.
  • Clause 2, the write door.
    • Accepted and re-read: crm_lead with industry=logistics, lead_source=webinar, salutation=prof returns 201; crm_account with industry=logistics returns 201. Re-reads return the same stored values in all 4 locales.
    • Refused, each with 400 VALIDATION_FAILED and field code invalid_option, the message naming the picklist: industry=not_an_industry on crm_lead and on crm_account, and lead_source=carrier_pigeon on crm_contact.
    • The zh-CN refusal reads 行业必须是选项列表“industry”中的值之一.
  • Clause 3, locale relabel. crm_account.industry=logistics reads Logistics / 物流 / 物流 / Logística in en / zh-CN / ja-JP / es-ES. crm_lead.lead_source=webinar reads Webinar / 线上研讨会 / ウェビナー / Seminario Web. crm_contract.payment_terms=due_on_receipt reads Due on Receipt / 货到付款 / 請求書受領時払い / Pago a la Recepción. Stored values do not change.
  • Extension sub-clause: NOT MEASURED. hotcrm authors no picklistExtensions, which is outside this card.

Analytics labels (the former blocker), POST /api/v1/analytics/dataset/query:

Locale account_metrics × industry lead_metrics × lead_source Control: account_metrics × type
zh-CN 教育 / 能源公用事业 广告 / 陌生拜访 正式客户
ja-JP 教育 / エネルギー・公益 広告 / コールドコール 顧客
en Education / Energy & Utilities Advertisement / Cold Call Customer

drillRawRows keeps the stored values (education, advertisement, …). A picklist-bound dimension is translated exactly like the inline-options control.

Verification at 05f36d10 (final head)

pnpm verify through the shared verify lock: VERDICT command-exit 0.

  • validate: ✓ Validation passed · Data: 18 Objects 357 Fields 5 Picklists
  • typecheck: tsc --noEmit exit 0, and --listFiles confirms the 190 test files are inside it
  • lint (--strict): 18 suggestion(s), 0 errors, 0 warnings. The --json issue set is identical to main's (diff empty).
  • lint:i18n-gate: ✓ i18n lint gate: 0 i18n/missing-* issues
  • hygiene: ✓ source hygiene clean
  • hygiene:tokens: ✓ source token ratchet clean. src/sales authored ~103,047 of ~160,500; src/revenue ~19,038 of ~28,500; src/marketing ~9,142 of ~15,000; src/service ~21,989 of ~33,000.
  • build: ✓ Build complete · 5 Picklists
  • test: Test Files 179 passed (179) · Tests 3660 passed | 1 skipped (3661)

Ablations (each leg mutated with the ablation tool and restored to the HEAD blob, with git diff HEAD empty):

  • Leg A: a bogus key mister under zh-CN picklists.salutation.options.
    • objectstack lint --strict exits 0 with 0 picklist findings.
    • i18n-references goes red, 1 failed / 21 passed.
  • Leg B: a picklists.not_a_list entry no package declares.
    • Lint exits 0 with 0 findings.
    • i18n-references goes red, 1 failed / 21 passed.
    • Two earlier attempts at leg B did not measure it. One was refused by the tool (the anchor reappeared in the replacement). The other mutation stripped salutation.options, so the stack schema refused the pack first. Both are voided.

So the picklist-key guard stays, by the same rule that retired its field-level twin when strict lint took that over (#2012).

Acceptance notes

  • Strict lint does not read picklists.NAME in a locale pack on 18.0.0-next.1. An option key that is not a value, or a list nobody declares, is silently dropped (legs A and B above). This is reported to the seat for upstream filing, not routed around here. The local guard stays only until the platform covers it.
  • ObjectQL.create({ datasources, objects, hooks }) still takes no picklists on 18.0.0-next.1, so the 8 engine suites register the app's lists themselves. It is a harness factory, not an authoring surface; noted, not filed.
  • Internal maintainer docs still name _picklists.ts in dated or planning prose: docs/architecture/module-split-plan.md, docs/architecture/psa-module-plan.md, docs/architecture/module-split-inventory.json, and docs/requirements/0004–0006. They were not edited: outside the claim, and dated records stay. Noted, not filed.
  • content/docs/**: the three stage pages × three locales that named the deleted file now name the stage field.

Generated by Claude Code

claude added 8 commits October 8, 2026 03:38
The five option lists more than one select field offers are now platform
picklists, referenced by name with `Field.select({ picklist })`:
salutation, industry, lead_source and related_to_type (sales), and
payment_terms (revenue). Each list used by one field returns inline to it.
Option labels for picklist-bound fields move to `picklists.<name>` in all
four locale packs. `src/sales/objects/_picklists.ts` is deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
…red lists

Engines built with `ObjectQL.create({ objects })` register the stack's
picklists (the factory takes none, so every picklist-bound field refused
every value), and the static option readers read a field's offered
options — its inline list, or the picklist it names — so the #490/#531
superset guard and the import-mapping legality check keep reading the
industry and lead_source copies instead of skipping them. The placeholder
and translation-key guards follow the option labels to `picklists.<name>`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
…ngeset

The product pages named `OPPORTUNITY_STAGE_OPTIONS` in the deleted
`src/sales/objects/_picklists.ts`; the stage list is now the `stage`
field's own options in `opportunity.object.ts`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Brings in #2010 (hotcrm#1581): `pnpm lint` now runs `objectstack lint --strict`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012zh91QzFgePbkmuHnugLN3
Brings the finished picklist migration onto main 4de5343 (@objectstack
18.0.0-next.1, the two-package artifact, the suites on @objectstack/verify
and SQL). Conflicts, each resolved toward main's behaviour plus the
migration:

- objectstack.config.ts: main's composeStacks artifact. The picklists
  register with the app package instead: objectstack.composition.ts
  gains appPicklists (sales + revenue barrels) and hands it to
  createHotCrmAppStack. Every picklist-bound field is an app-package
  field; the service package references none.
- src/sales/objects/_picklists.ts: stays deleted. Main's two comment
  edits (#2035, #2040) are carried to where the comments moved:
  payment_terms.picklist.ts now names quote_accepted_contract_draft.
- contract.object.ts: main's hook name, the branch's picklist reference.
- opportunity.view.ts: main's (the stage rowColor map is gone, #2108).
- executive.dashboard.ts, mappings/_shared.ts: the branch's comments.
- 14 suites: main's imports, drivers and syncSchemas(), plus the
  branch's picklist lines. Engine suites still built with
  ObjectQL.create register the app's lists (the factory takes no
  picklists on 18.0.0-next.1). global-actions reads related_to_type off
  the booted registry's served field; i18n-references checks picklist
  translation keys against the lists the booted registry holds.
  actions-flows-integrity keeps both non-vacuity guards. view-references
  drops the retired rowColor pins and keeps the mass_update_stage pin.
- test/helpers/stack-picklists.ts reads the composed view (the artifact
  carries packages[] only) and resolves a field's options through the
  platform's SchemaRegistry.resolvePicklistOptions instead of a local
  lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
… projected task type

lead-conversion.flow.ts (arrived with #2068) still said the two selects a
converted contact copies live in _picklists.ts; they reference the
salutation and lead_source picklists. do-not-call-enforcement.test.ts named
plainOptions(TASK_TYPE_OPTIONS), retired by this migration; the screen
projects crm_task.type's own options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
…bels

Measured on the live boot (18.0.0-next.1): POST /api/v1/analytics/dataset/query
relabels a picklist-bound dimension from picklists.NAME in zh-CN and ja-JP,
like the inline-options control dimension beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P8mZuzuyZAHSrgQKmv6ASv
@vercel

vercel Bot commented Oct 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated
hotcrm Ignored Ignored Oct 15, 2026 12:39am UTC

Request Review

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

复核人:repo:hotcrm 执行席(#1353)· 判决 ACCEPT · 钉 head 05f36d10dc95f9a3992ca21c22140135e219babe · 2026-10-15T00:51Z

形态:一卡一 PR,Fixes #2000,Clause-②: no;75 文件(新增 9、删除 1、修改 65),+945/−744 = 1,689 行(< 3,000);check-governed-merges --pr:NOT governed → 非 Tier H,席位落地。续用 10 月 8 日完成的 17.x 分支,origin/main 4de5343e 以合并提交 346951af 合入(无 rebase、无 force-push);main 是 head 的祖先。

前提实测:裁决 6051357805(A,立即迁移)。抽查:src/sales/picklists/{industry,lead_source,related_to_type,salutation}.picklist.ts 与 src/revenue/picklists/payment_terms.picklist.ts 各一份 definePicklist,经 barrel 显式导出,在 objectstack.composition.ts 以 appPicklists 注册进 createHotCrmAppStack;字段改为 Field.select({ picklist: '<name>' })(例:account.object.ts 的 industry);_picklists.ts 删除。dev 的机械等价读数:18 个对象的下发选项与字段属性对 main 0 差异;384 个语言包选项标签对 main 下发标签 0 差异;其余组合集合一致,仅新增 picklists 键。H3(阻塞源 objectstack#22178 已修)在自起服务上实测:zh-CN 行业 / 线索来源维度读 教育 / 广告,ja-JP 读 教育 / 広告,内联选项对照不变。H4(records-forms.picklist-shared-across-objects)三条均成立:两对象下发同一组选项;越界值 400 VALIDATION_FAILED / invalid_option;切换语言重新取标签。测试改走 @objectstack/verify 句柄,未重建本地替身(6061167800)。合并冲突 22 处逐一列在报告与 PR 正文。

门禁 check-runs(钉 head):Build and Test (22.x) ✓ · Typecheck and Build ✓ · Quality Checks ✓ · Check Changeset ✓ · Playwright ✓ · link-check ✓ · CodeQL ✓ · Analyze Code (javascript) ✓ · Label Pull Request ✓ · Vercel Preview Comments ✓ — 10/10 success,mergeable_state clean。

接受的偏差:① 落点超出认领面的几处:两处注释(lead-conversion.flow.ts 经 #2068 带来的旧引用、do-not-call-enforcement.test.ts)、9 个文档页、test/helpers/stack-picklists.ts、未直接 import _picklists 的测试套件;均由删除 _picklists.ts 或字段改为引用列表直接导致,未碰平台文件。② changeset 沿用分支原名 2000-shared-picklists.md。③ 若干提交的署名 trailer 带模型名(照会话归属提醒写入);按 os-dev 规则只报、不改历史。

范围外去向:平台 lint 不检查语言包 picklists.<name> 下的翻译键(错键、未声明的列表都静默通过、运行时静默丢弃)→ 已立 objectstack-ai/objectstack#23566;HotCRM 的本地守卫 test/i18n-references 在平台覆盖前保留。ObjectQL.create 不接收 picklists(仅测试工厂,无产品可达面)与内部规划文档仍提 _picklists.ts:记录不立卡。#2110(报价总价只读)在本 PR 落地后可派(同改 quote.object.ts)。

落地条款:席位经中继 pr_ready + automerge_enable,以 objectstack-fleet[bot] 入合并队列;合并后在 #2000 落探针表。合并队列不在合并树上重建,故席位在本地把 head 与当前 origin/main 9e064b3d(#2109 已改 src/revenue 的两个 hook)合并:无冲突,scripts/check-source-token-ratchet.mjs 读数 src/revenue authored ~19,162 / ~28,500、src/sales authored ~103,047 / ~160,500,✓ clean。lead-conversion.flow.ts 上的注释改动与在飞的 #2060 第 2b 项同文件,后落地者合 main 解冲突。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 00:53
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 8dfed90 Oct 15, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend Server-side behaviour — hooks, flows, actions ci/cd CI plumbing and the verification pipeline configuration Build and app configuration files documentation Improvements or additions to documentation metadata Declarative metadata — schema, security posture, UI surfaces

Projects

None yet

2 participants