Repository navigation
A select / radio field with neither options nor picklist: refuse it at the FieldSchema door (an accept-set narrowing), or keep it at the ADR-0078 completeness gate? (split from #19518) #20827
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsos-decision-facets
决策请求:一个
select/radio字段既没写options也没写picklist,要不要在 schema 门口直接拒绝? · 2026-09-30T09:21Zdomain:specseat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。#19518 的 dev 在 PR #20823 上实现了拒绝,测到存量数据会受影响,于是撤回,把问题交了回来(报告5908082075,开放问题 1)。这件事会让已存的数据不再通过校验,卡片当初没有算过这笔账。所以本席不自己拍板,单独开这张卡请你定。共享选项集(picklist)本身不等这个决定,照常落地。一句话问题
字段类型是下拉单选,却一个选项都没给。今天这种字段能保存,运行时也不校验填进来的值;作者在
os validate/os lint/os build时会收到错误。要不要把它升级成保存时直接拒绝?Governing text
- picklist metadata kind — spec:
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 Scope 2 与验收:「both or neither on a select type is refused at the schema door」「neither refused」。 - ADR-0078:
field/choice-without-options(select/radio为 error)放在作者期的完整性检查里;注册期那一道「只警告,从不抛错」,理由是「一个没用的字段不能拖垮整个启动」。 - 先例:
0fb8760bec(fix(spec): require a non-empty reference on lookup/master_detail fields #13927)把「lookup没写reference」从完整性检查升级到 schema 门口拒绝。它按minor加 BREAKING 标题发布,并带 ADR-0087 的 not-required 标记。
前提(每条带复查方法)
- 升级拒绝后,存量测试会红。 4 个 spec 夹具和
metadata-protocol存量转换与迁移套件里的 9 个测试会红:存下来的无选项select行校验不过,migrateStoredMetadata也不再改写它。复查:看 picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 的 dev 报告5908082075;本席没有重跑。 - 今天的运行时后果。 选项为空的
select会关掉服务端的值校验,任何值都能写进去。复查:ADR-0078 该规则的理由一栏,指向record-validator.ts。 - 没测的两件事。 一是真实部署里有多少这样的存量行;二是 Studio 是否会先保存字段、后补选项(dev 报告说「会 422」,那是推断,不是实测)。
选项与代价
选项 做什么 客户看到的后果 A 升级到 schema 门口拒绝,照 lookup的先例:单开一个 PR,minor加 BREAKING 标题;先修夹具,再处理存量行(先普查,再定迁移或点名修复);先确认 Studio 的建字段流程不受影响,再落地任何入口保存无选项的下拉都会被拒,并提示「写 options或picklist」。已存的无选项字段需要补选项B 维持现状:作者期报错、注册期警告,schema 不拒 用 os工具的作者照样挡得住;经运行时接口或 Studio 保存的无选项下拉仍然能存,值不校验业务直译:A = 下拉必须有选项,哪里都不放行;B = 只在开发工具里拦,线上照样能存。
四轴(业务立场)
- 长远合理性: 主流平台(Salesforce 选项列表、Dataverse 选项列)都要求至少有一个值,或者引用一个全局选项集。A 与主流一致,也与
lookup先例同一个思路。B 让「门口放、工具拦」的双标准继续存在。 - 实际业务拉动: 没有测到任何作者是有意写无选项下拉的;存量行数量没测。拉动只影响排期。
- 防 AI 犯错: AI 漏写选项时,今天只有走
os工具才会被拦;经运行时接口保存则静默成功,而且值不校验。A 在所有入口响亮拒绝,并给出处方。 - 创业阶段不扩散: B 零成本;A 需要一个 PR 的量:修夹具、处理存量、核对 Studio。
推荐
A,排在 #19518 落地之后。 只看①选 A;②③④ 是否翻转:否(④只多一个 PR 的工作量,②只影响排期)。回退:B。
置信缺口:存量行数量和 Studio 的建字段顺序都没测。A 的第一步就是测这两件;如果测出的代价超出预期,本席会回到本卡报告,不会硬上。自检
- 这不是本席能定的:picklist metadata kind — spec:
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 当初的设计只写了结果,没算存量代价,而新代价会让已存的数据失效。 - 两个选项都能执行,不会卡在别的决定上。
裁后执行
- A: 本卡转
pm:queue,等 picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518(PR feat(spec): thepicklistmetadata kind — a shared option list select fields reference by name (#19518) #20823)落地后由 spec 席认领。第一步普查存量行,并核对 Studio 的建字段流程;然后单开 PR 按lookup先例做,达档复核后落地。 - B: picklist metadata kind — spec:
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 的验收里去掉「neither refused」这一条,保留 ADR-0078 的作者期报错;本卡以「不做」关闭,并记下理由。 - 无论选哪项,picklist metadata kind — spec:
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 都不等这张卡。
四棱
① 项目长远合理性:A 与主流和
lookup先例一致;B 保留双标准。
② 实际业务拉动:没有测到作者,存量未测;只影响排期。
③ 防 AI 犯错:A 在所有入口响亮拒绝;B 只在os工具里拦。
④ 创业阶段不扩散:B 零成本;A 一个 PR。
Prior rulings read: picklist, choice-without-options, schema door, neither refused → #18164 设计5715762696与裁决5755653853、5904864936;#19518 正文;ADR-0078、ADR-0049、ADR-0087;先例0fb8760bec(#13927);thread: 本卡无评论,#19518 的 dev 报告5908082075。
推荐:A。只看①选 A;②③④ 是否翻转:否。置信缺口:见上。(备注:本卡创建时,发卡工具报告回读未确认(退出码 6)。本席没有重发,读了看板:只有这一张 #20827,标题和正文都与发出的一致。)
Generated by Claude Code
- picklist metadata kind — spec:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRuling: batch #256 item 5 · letter A · maintainer 「同意 批次 #256」 2026-09-30T11:20Z
Director seat (objectstack#12708, summon #30 续 2,
session_01AsCNgFBs8HCjwhyHQsFbx3). Provenance: maintainer, live PM chat with the director seat, 2026-09-30, replying 「同意 批次 #256」 to batch #256 as presented (five cards, each with the seat's recommendation and the director's four-axis reading). Presented on this card: thedomain:specseat 5's decision request 5908178556 (from the #19518 dev report 5908082075, open question 1) and the director's reading (A after #19518, census and Studio-order check first).Ruled: A — a
select/radiowith neitheroptionsnorpicklistis refused at theFieldSchemadoor, on thelookup-without-referenceprecedent (0fb8760bec, #13927): its own PR,minorwith a BREAKING header, an ADR-0087 marker, a prescription naming both keys. Sequenced after #19518 (PR #20823) lands.Execution parameters, as presented: two measurements before the door closes. (1) A census of stored rows carrying such a field — the four spec fixtures and the nine
metadata-protocolstored-conversion / migration tests that went red show what a stored row does; the disposition for existing rows follows the count: none → thenot-requiredmarker as the precedent; some → new writes refused, stored rows read through the legacy path and named, since no migration can invent options. (2) The Studio create-field order (the dev's "would 422" is inferred): if Studio saves aselectbefore its options exist, the objectui side changes its order first (a card in that lane) and the door closes after. If either measurement's cost exceeds the card's estimate, the seat returns here before building. ADR-0078's author-time rule and the registration-time warning stay as they are — the door is one more gate, not a replacement.Readings that decided it: ① Salesforce and Dataverse require a value or a global set, and the lookup precedent is the same move; ② no author measured wanting an empty select; ③ today a runtime-API or Studio save of an empty select succeeds silently and turns off server-side value validation — A refuses loudly at every door with a prescription; ④ one PR plus the two measurements.
State:
needs-user-decision→pm:blocked,Blocked-by:#19518 (PR #20823); the triage unlock scan or the spec seat moves it topm:queuewhen #19518 lands; grading (type, priority) is triage's. The Ruled line goes on the body.objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsUnlock:
pm:blocked→pm:queue; #19518's spec layer landed · 2026-09-30T14:35Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1). The ruling5910124148names this seat as one of the two that move the card when #19518 lands. ⛔ Not a claim.- The condition is met: PR feat(spec): the
picklistmetadata kind — a shared option list select fields reference by name (#19518) #20823 merged asaddbbf02abat 2026-09-30T14:31Z (landing record5913418487on picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518). The body'sBlocked-by:line comes off in this act. - Serial on
packages/spec/src/data/field.zod.ts: this card claims after two things land that edit the same file. First, the picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 wording follow-up PR, which is in flight. Second, fields: number rendering honours the authoreduseGrouping(1 key) #20313 (FieldSchema.useGrouping), unless the claiming seat orders them differently and says why. - Execution when claimed, as the ruling states: first the stored-row census and the Studio create-field order. If either costs more than the card estimated, report back here before building.
Generated by Claude Code
- The condition is met: PR feat(spec): the
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 27 (ruling A
5910124148, step one: the two measurements before the door closes) · 2026-09-30T17:11Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch: none yet. Step one is read-only, and the build branch is named at step two.
Worktree:objectstack-verify-20827(read-only measurement)
Domain:domain:spec
Seat:domain:spec#5(seat post #19357)
Clause-②: yes (narrowing), for the step-two build. Step one changes nothing.
Scope, in the ruling's order:- The census: every stored or authored
select/radiofield with neitheroptionsnorpicklistthat this repository and objectui can see. That means examples, platform objects, spec andmetadata-protocolfixtures, and test stacks. Also howmigrateStoredMetadatatreats such a row today. Productionsys_metadatais NOT reachable from here, and the seat says so rather than guessing. - The Studio order: whether objectui's Studio create-field flow saves a
select/radiobefore its options exist, which would be a 422 once the door closes. It is read at objectuiorigin/mainand at the.objectui-shapin. - The step-two decision: if either reading costs more than the card estimated, the seat reports back here before building. Otherwise step two builds the refusal on the
lookupprecedent (0fb8760bec), with aminorBREAKING header and an ADR-0087 marker.
File surface: step one is none. Step two will bepackages/spec/src/data/field.zod.tsand its tests, the fixtures the census names, and the changeset. ⛔ Step two waits for fields: number rendering honours the authoreduseGrouping(1 key) #20313 (another seat, same file) to land.
Container & model:S, read-only measurement agent,model: opus.
Thread-read: 5913459642
Serial constraints: step one reads only. For step two,field.zod.tsis held by fields: number rendering honours the authoreduseGrouping(1 key) #20313 (os-warren).
Generated by Claude Code
- The census: every stored or authored
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsStep one (the two measurements) reported: the Studio order exceeds the estimate, so the ruling's objectui branch runs first.
pm:dispatched→pm:blockedon objectstack-ai/objectui#11253 · 2026-09-30T18:23Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5916083155. The measurement was read-only. The withdrawn door (1c111c3e67) was re-applied behind an environment switch in a throwaway worktree, and each package was run with the switch on and off. objectstackorigin/main8fec76a2b5; objectuiorigin/main1263e405de, pindb11afd496.Measurement 1, the census: within the estimate.
- Red with the door on, green with it off (the estimate was 4 spec fixtures plus 9
metadata-protocoltests):- Four oversight fixtures in
packages/spec:field.test.ts:2155;field-autonumber-default-unique.test.ts:45; and the exported fixturesfilter-number-comparand-declared-type.ts:705-713andfilter-text-operator-declared-type.ts:417-423. Each is fixed by adding one option. - One intended pin,
picklist.test.ts:129, with 2 tests. It pins today's "neither is only the completeness gate's error". The door flips it by design, and it is rewritten as a refusal pin. metadata-protocol: exactly 9 tests, from ONE fixture row,legacyObjectRow'sstatus: { type: 'select' }, atprotocol.stored-conversions.test.ts:102(2 tests) andprotocol.stored-migration.test.ts:143(7 tests). That row exists to testconditionalRequired, so it is an oversight, and it is fixed by adding one option.
- Four oversight fixtures in
- Every other suite: 0 reds caused by the door. The suites were spec, metadata-protocol, objectql, lint, rest, runtime, cli, metadata, mcp, formula, driver-sql, driver-sqlite-wasm, service-analytics/automation/settings and app-showcase. There are 141 more no-option stubs in 62 files, and they bypass
FieldSchema. The 7 intended no-option authorings are judged by the ADR-0078 author-time rule, which is unchanged. examples/**andplatform-objects: 0 real no-option choice fields.- What a stored row does once the door closes:
- It is still read, with
_diagnostics.valid: false, and listed by/meta/diagnostics. - It still registers at boot, counted
invalidwith a[metadata_spec_invalid]log line. migrateStoredMetadataapply marks itfailedand leaves its bytes untouched. Every later save of that object answers 422 until options or apicklistare added.⚠️ Theos migrate meta --storedPREVIEW does not validate, so it counts such a rowcanonical. Step two's disposition must name these rows through/meta/diagnosticsor the boot log (field/choice-without-optionsfromregistry.ts#warnFunctionalCompleteness, available today), not through the preview.
- It is still read, with
- Production
sys_metadata: not reachable from here, so not measured.
Measurement 2, the Studio order: it saves before the options exist, on both mounted paths. This exceeds the estimate.
- The Studio data page:
newField(name, 'text'), then a type change toselect, then an autosave after 1.5 s. Itsblockedcondition counts only CEL errors. - The metadata-admin editor:
newFieldseedsoptions: [], and it autosaves after 1.5 s. After a 422,#refusalBlockingholds the autosave. - plugin-designer: the drawer has no options editor, so once the door closes it could never create a choice field. It is not mounted in the console.
- The file-and-symbol evidence is on Studio autosaves a new
select/radiobefore it has options; once objectstack#20827 refuses such a field at the FieldSchema door, that save answers 422: guard or complete the write first objectui#11253.
Next, as the ruling's execution parameter directs ("the objectui side changes its order first (a card in that lane) and the door closes after"):
- Studio autosaves a new
select/radiobefore it has options; once objectstack#20827 refuses such a field at the FieldSchema door, that save answers 422: guard or complete the write first objectui#11253 is filed. It asks for a write guard on the half-filled-lookup precedent (object-metadata-write-guard.ts), or an options-first create, plus plugin-designer's options editor. - This card unblocks when that lands AND objectstack's
.objectui-shacarries it. Step two then runs as ruled: the 4 fixtures plus the one metadata-protocol row gain an option, the picklist pin is rewritten, and the door follows thelookupprecedent. - The maintainer may overrule the order. The door could close now and accept Studio's 422 until objectui lands. The seat does not recommend that.
One side finding:
packages/spec/src/ai/solution-blueprint.zod.ts:88and:320let a blueprintselectcarry no options, oroptions: null. The blueprint's expansion lives outside these two repos. Step two checks whether the door reaches blueprint output.Transition in this act:
pm:dispatched→pm:blocked. The seat's assignee comes off, and the claim is released because nothing is in flight here. The body gainsBlocked-by: objectstack-ai/objectui#11253.
Generated by Claude Code
- Red with the door on, green with it off (the estimate was 4 spec fixtures plus 9
10 remaining items
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsClaim: PM loop round 1 (ruling A
5910124148, step two: theFieldSchemadoor)
Session:session_01YDt3PzwfrkuFzUBF89WPmM
Account:os-tesla(the seat's linked user asGET /useranswers it; the card's assignee)
Branch:claude/issue-20827-choice-door
Worktree:objectstack-issue-20827
Domain:domain:spec
Seat:domain:spec#2
File surface:packages/spec/src/data/field.zod.ts: theFieldSchemasuperRefine (the door beside thelookupreferencecheck, about:2116), theoptions/picklistTSDoc and describes, and the "neither" comment at:2131.- The four fixtures named in step one (
5917187437):field.test.ts,field-autonumber-default-unique.test.ts,filter-number-comparand-declared-type.ts,filter-text-operator-declared-type.ts. The pin atpicklist.test.ts:129is rewritten as a refusal pin. packages/metadata-protocol: the onelegacyObjectRowfixture row (protocol.stored-conversions.test.ts,protocol.stored-migration.test.ts).- New door pins in
packages/spec/src/data/*.test.ts. - The regenerated spec artefacts and reference pages.
- Prose in
content/docs/**that states an optionless select parses. - One
.changeset/20827-*.md:minor, a BREAKING header, and an ADR-0087 disposition marker. - ⛔ No objectui edit. ⛔ ADR-0078's author-time rule and the registration warning are not edited.
- Stop on breach and explain in the report.
Container & model:
M,mode:subagent,model: opus(dispatch-gates --tierat16eefc6c0e: no path-derived mandate; floor sonnet · default opus · ceilingCONTRACT_REVIEW_TIER). The at-tier contract review is owed and runs as a separate isolated subagent after the PR opens.
Clause-②: yes
Thread-read: 5947557233
Serial constraints cleared:- The two named in
5913459642have both landed: picklist metadata kind — spec:picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518 is closed, and fields: number rendering honours the authoreduseGrouping(1 key) #20313 (useGrouping) is closed. - At 2026-10-02T07:46Z, none of the 10 open PRs touches this surface. chore: version packages #21352 (Version Packages) matched only a changeset file name.
- packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 (seat 5, off shift) has stage 11 in
migrations/registry.ts's hand-written parts. That region is disjoint from this surface. - The verify lock is free.
Execution, as the ruling and step one fix it:
- The objectui order landed (objectui#11253), and the pin
31971ff1e28fcarries it (5947557233). - The door follows the
lookupprecedent (0fb8760bec, fix(spec): require a non-empty reference on lookup/master_detail fields #13927). Its prescription names both keys. - Stored rows are named through
/meta/diagnosticsor the boot log (field/choice-without-options), not theos migrate meta --storedpreview. - Step one's side finding is checked and reported: whether the door reaches blueprint output (
solution-blueprint.zod.ts:88/:320).
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsos-dev-report
{ "issue": 20827, "status": "done", "branch": "claude/issue-20827-choice-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/21390", "session": "session_01YDt3PzwfrkuFzUBF89WPmM (subagent run; the PM's session id, identity = branch claude/issue-20827-choice-door per claim 5947645436)", "premise_still_valid": true, "summary": "FieldSchema's superRefine now refuses a select / radio with neither a non-empty options list nor a picklist, at the options path, with a custom issue naming the type and both remedies, beside the reference check (precedent 0fb8760bec). The door applies the ADR-0078 completeness predicate itself (checkFieldCompleteness: a field/choice-without-options finding at error severity), so options: [] is the same hole, a picklist is a source, and the type set is exactly select / radio; no export is added and functional-completeness.ts is not edited. The census matched step one exactly (4 oversight fixtures + the picklist pin in spec, 9 metadata-protocol tests from one row); every red is fixed, the picklist pin is a refusal pin, the stored-row disposition is pinned in metadata-protocol, and a spec minor BREAKING changeset carries the not-required (no-migration-prescription) marker. Draft PR 21390 is open on head ee5b089bbe (origin/main merged in), assigned os-tesla.", "tests": "Full @objectstack/spec suite at head ee5b089bbe: 646 files, 18391 passed, 1 todo (VERDICT command-exit 0). Spec typecheck incl. check:test-typecheck green; check:generated: all 15 artifacts up to date; metadata-protocol typecheck green. Consumer suites with the door on, at a9473b8d91 (the merge brought no change to their sources): metadata-protocol 201 files / 2983 passed / 19 skipped; objectql 363 / 7277 (incl. both engine door suites that consume the changed spec fixtures); lint 119 / 5575; metadata 56 / 836; runtime 306 / 5081 / 11 skipped. Red census at base 5fd4855a9a: spec 6 failed in 5 files, metadata-protocol 9 failed in 2 files. Ablation (reverse verification): scripts/ablation-replace.mjs swapped FIELD_CHOICE_WITHOUT_OPTIONS for the literal 'ablation-20827-never' (anchor 1 to 0, blob ea9313c768 to 9654443823), the spec rebuild was proved by ablation-dist-preflight (marker in 24 built files), and the mutate leg was RED: 8 spec pins and 3 metadata-protocol pins, positive controls green. The restore leg used git checkout HEAD (blob equal to HEAD, git diff HEAD empty); after a rebuild, preflight --absent reported 0 of 230 built files and a clean tree; 325/325 spec and 56/56 metadata-protocol passed. Expected direction red, observed red.", "mcp_calls": "0 — no MCP GitHub tool called", "api_writes": "3 relay writes as objectstack-fleet[bot], each one repository_dispatch to the board relay: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft, PR 21390), run 36993732961; (2) label-write assign, POST /repos/objectstack-ai/objectstack/issues/21390/assignees (os-tesla), run 36993794660; (3) the os-dev-report comment, POST /repos/objectstack-ai/objectstack/issues/20827/comments via post-stamped.mjs. Plus 8 git pushes, which are not REST writes.", "open_questions": [], "out_of_scope_findings": [ "class: c · reach: measured at a public door — ObjectSchema.safeParse on the exact os:check block 'The v17-canonical shapes, compiled' at skills/objectstack-upgrade/references/examples-upgrade.md:105 (status: { type: 'select', required: true, storage: { notNull: true } }) answers success:false, fields.status.options custom, against this branch's built spec; producer: the published objectstack-upgrade skill. Evidence: the block is only type-checked (check:skill-examples green), so no gate sees the parse refusal, and the block's own claim 'cannot rot into teaching a shape that no longer compiles' is false at parse. This change makes the published text false, so it is owed a fix, but skills/** is Tier H and the dispatch barred it. Fix: one options entry on status, in a skills-lane PR landing with this door. · dedupe words: examples-upgrade select options, objectstack-upgrade skill choice door, v17-canonical shapes select, crm_lead status select", "class: c · reach: named producer — StrictField (packages/spec/src/ai/solution-blueprint.zod.ts:320, options .nullable()) and BlueprintFieldSchema (:88, options optional), the AI structured-output target that apply_blueprint expands; apply_blueprint lives outside objectstack and objectui (objectui only renders its progress and plan cards; nothing in packages/** or examples/** of either repo expands a blueprint into FieldSchema input). The module header says the expansion is validated by the per-type schema at write time, so an optionless blueprint select is now refused there, loudly, one step after the AI emitted it. Not measured end to end. Fix direction: align BlueprintFieldSchema / StrictField options with the door for select / radio. · dedupe words: blueprint select options, StrictField options nullable, BlueprintFieldSchema options, apply_blueprint choice door", "carrier: the objectui lane on its next @objectstack/spec bump — packages/data-objectstack/src/object-metadata-write-guard.derivation.test.ts, describe 'the installed server still ACCEPTS a choice with no options', turns red by design once the installed spec carries this door; its own comment prescribes the rewrite into a refusal pin at options · noted, not filed" ], "gates": [ "dispatch-gates --repo objectstack-ai/objectstack --commands at ee5b089bbe: 110 families; each exit code written to ran.list; --ran: exit 0, '110 derived famil(ies) accounted for — 109 run, 1 NOT-MEASURED (0 DERIVED from a recorded exit 3, 1 claimed)'", "pnpm --filter @objectstack/spec build: exit 0 (VERDICT command-exit 0)", "pnpm --filter @objectstack/spec check:generated: exit 0, 'All 15 generated artifacts are up to date' (pre-merge run: exit 1, 1 stale check:docs, then --fix regenerated only that: content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx)", "pnpm --filter @objectstack/spec test (vitest run): exit 0, 646 files / 18391 passed / 1 todo", "pnpm --filter @objectstack/spec typecheck: exit 0, 'check:test-typecheck: OK … 52 file(s) / 246 error(s) / 135 pinned signature(s) held'", "pnpm --filter @objectstack/metadata-protocol test: exit 0, 201 passed / 3 skipped files, 2983 passed / 19 skipped tests; typecheck exit 0", "objectql exit 0 (7277); lint exit 0 (5575); metadata exit 0 (836); runtime exit 0 (5081 / 11 skipped)", "pnpm check:adr-0087-registration: exit 0, '1 declared-breaking changeset(s), each carrying an ADR-0087 disposition' · [BREAKING+bang] not-required (no-migration-prescription)", "node scripts/check-changeset-no-major.mjs --event (draft body): exit 0, 'This diff introduces no major bump' · 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch'", "node scripts/check-empty-changeset.mjs: exit 0, 'No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added)'", "pnpm check:changeset-gate-self-tests: exit 0", "pnpm check:doc-authoring: exit 0, '17226 customer-facing string(s) across 1229 spec sources clean — no internal issue-id references'", "pnpm check:nul-bytes: exit 0, 'scanned 9679 text file(s) … no raw ASCII control bytes'", "pnpm --filter @objectstack/spec run check:api-surface: exit 0, 'public API surface + factory signatures unchanged'", "pnpm --filter @objectstack/spec run check:skill-examples: first exit 3 (client dist missing), re-run after the dists existed: exit 0, '259 prose examples type-check across 3 surface(s)'", "NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (it needs every published package's dist). Declared narrowing: all 19 require-condition entries of @objectstack/spec dist load under CJS, and the door is live through dist/data/index.js. The full gate is CI's.", "CI on PR 21390: in_progress at report time (not awaited, per contract)" ], "line_budget": "344 changed lines (+307 / -37) over 16 files vs the 5000 human-merge threshold: under (dispatch-gates at ee5b089bbe). No skills/** or other governed-surface file is touched, so no line ratchet applies and the PR is not Tier S/H.", "deviations": [ "Clause-② line copied verbatim from the claim ('yes') into the PR body and the changeset. The diff narrows FieldSchema's accept set (BREAKING, carried by the changeset banner) and also widens one published type: the two exported door-fixture interfaces gain an optional options member. So 'yes (narrowing)' is the fuller spelling. Not self-amended; both gates pass on 'yes'.", "Conflict between the standing rule (a published defect this change makes false must be fixed) and the dispatch's '⛔ No skills/** edit': the dispatch was followed. The skills example is listed in Acceptance notes and out_of_scope_findings with a measured reach, for a skills-lane (Tier H) PR.", "Beyond the census: canonicalObjectRow (protocol.stored-migration.test.ts) got the same one option. It caused no red, but its doc ('already canonical') is false under the door. Three new stored-row disposition pins were added in the two metadata-protocol files the claim names.", "origin/main (11905a4f8b) was merged into the branch before the PR opened (AGENTS.md §10). Spec build, check:generated, the full spec suite, typecheck and the gate union ran at merged head ee5b089bbe. The consumer suites ran before the merge, at a9473b8d91; the merge brought no change to their package sources.", "The full spec suite takes about 25 minutes, past the foreground cap. It ran under the verify lock as a background job, waited on in the foreground with tail --pid.", "Labels: the dispatch named no label beyond the PR assignee, and skip-changeset does not apply (a changeset exists), so only --assign os-tesla was written. size/m was added by the size labeler, not by this run.", "Commit trailers use the model-free pair AGENTS.md requires (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named Co-Authored-By. The pre-push hook accepted all 8 pushes.", "A4 precision: migrateStoredMetadata marks such a row failed only when the row also carries an older spelling to lower (pinned). A row whose only defect is the missing option source has nothing to convert, so the pass counts it canonical. The changeset says so and names /meta/diagnostics and the boot log as the census tools." ], "files_changed": [ ".changeset/20827-choice-door-select-radio-needs-options.md (+50)", "packages/spec/src/data/field.zod.ts (+68 -7): the door, import of checkFieldCompleteness / FIELD_CHOICE_WITHOUT_OPTIONS, options / picklist TSDoc and describes, exclusivity comment", "packages/spec/src/data/field.test.ts (+77 -1): Choice-source pin block, and one option on the :2155 radio fixture", "packages/spec/src/data/picklist.test.ts (+14 -6): the neither pin rewritten as a refusal pin", "packages/spec/src/data/field-autonumber-default-unique.test.ts (+2 -1)", "packages/spec/src/data/filter-number-comparand-declared-type.ts (+3)", "packages/spec/src/data/filter-text-operator-declared-type.ts (+3)", "packages/metadata-protocol/src/protocol.stored-conversions.test.ts (+37 -1)", "packages/metadata-protocol/src/protocol.stored-migration.test.ts (+28 -2)", "content/docs/references/data/field.mdx, data/object.mdx, system/migration.mdx (regenerated)", "content/docs/deployment/troubleshooting.mdx, data-modeling/validation-rules.mdx, data-modeling/field-types.mdx, data-modeling/formulas.mdx (prose)" ], "fixture_census": [ "spec field.test.ts:2155 (radio + multiple:false): oversight; +1 option", "spec field-autonumber-default-unique.test.ts:45 (minimalField): oversight; select / radio get 1 option", "spec filter-number-comparand-declared-type.ts fixtureFieldFor (:705-713): oversight; SINGLE_OPTION_TYPES get 1 option, and the interface gains optional options", "spec filter-text-operator-declared-type.ts fixtureFieldFor (:417-423): same", "spec picklist.test.ts:129 (2 tests): FLIPPED. It pinned the old acceptance ('neither is the completeness gate's error, not a parse refusal') and is now a refusal pin at options; its checkFieldCompleteness assertions are unchanged", "metadata-protocol: 9 tests (stored-conversions 2, stored-migration 7) from legacyObjectRow's status select: oversight (the row tests conditionalRequired); +1 option {label: 'Sent', value: 'sent'} in both files", "beyond the census, no red: canonicalObjectRow +1 option (its doc says canonical)", "every other suite run (objectql, lint, metadata, runtime, the rest of metadata-protocol and spec): 0 reds caused by the door" ], "semver": "@objectstack/spec minor with a BREAKING header and a bang summary. Gate lines: 'check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition'; 'This diff introduces no major bump'; 'LEVEL AXIS: this PR declares clause-② yes, and no package whose packages/**/src/** it moves is graded patch'. No other package publishes a change.", "a6_blueprint": "Inside objectstack and objectui, nothing expands a blueprint into FieldSchema input. The only in-repo mentions are the spec schema, its tests, the cli eval harness's doc comment, and objectui's chat progress UI. apply_blueprint is outside both repos. solution-blueprint.zod.ts's own header says the expansion is validated by the per-type schema at write time, so the door reaches blueprint output at that write: refused loudly, not stored. The gap is that the blueprint schema accepts what the door refuses (BlueprintFieldSchema.options optional :88; StrictField.options nullable :320). It is named in Acceptance notes for the PM to file; solution-blueprint.zod.ts is not edited.", "adr_0087_marker": "not-required (no-migration-prescription). The change narrows the validity of two existing keys (options, picklist) and removes, renames or re-shapes none, so there is no tombstone and nothing for objectstack migrate meta to rewrite. Which choices a sourceless select meant is authoring intent no conversion can invent. It is the precedent 0fb8760bec's category. Production sys_metadata is unmeasurable here, so the marker text takes the ruling's 'some rows exist' arm: new writes are refused at parse; stored rows keep their bytes and are read and named (_diagnostics.valid false, /meta/diagnostics, counted invalid at boot, the field/choice-without-options boot line). That arm is pinned in metadata-protocol. The other categories are closed on facts: the package publishes; no id covers this rule; it is a schema narrowing, not a runtime-interface-only or type-surface-only change. The gate accepted it." }
Generated by Claude Code
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsACCEPT — PR #21390 @
ee5b089bdomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5947645436· 2026-10-02T10:29Z-
Shape (read on GitHub): a draft against
main. The first line isFixes #20827, andClause-②: yessits at a line start. 16 files, all inside the claim surface, pluscanonicalObjectRow's one option in a metadata-protocol file the claim names (its doc said "already canonical"). -
Contract review: at-tier PASS
5950368115on this exact head. -
CI on
ee5b089b: 33 success and 2 skipped by design (Console Pin Gatewith no.objectui-shachange, and the opt-in packed-tarball smoke). -
Governed surface:
check-governed-merges --pr 21390reads NOT governed, 344 changed lines. -
Ruling A (
5910124148), item by item:- The door: at
FieldSchema's superRefine, on theoptionspath, beside thereferencecheck (0fb8760bec), with a prescription naming both keys. - The predicate:
checkFieldCompleteness's own, read rather than restated. Sooptions: []is the same hole, apicklistis a source, and the types are exactlyselect/radio. - Release:
minorwith a BREAKING header and the ADR-0087 markernot-required (no-migration-prescription), the precedent's category. The stored-row arm is the ruling's "some" arm, pinned in@objectstack/metadata-protocol. - Unchanged: ADR-0078's author-time rule and the registration warning.
- The door: at
-
The census matched step one (
5917187437) exactly. Every red is fixed by one option, and the picklist pin is now a refusal pin. The ablation went red as predicted (8 spec and 3 metadata-protocol pins), and the restore went green. -
Prose read sentence by sentence by this seat:
- the changeset, including the stored-row paragraph, which names
/meta/diagnosticsand the boot log and states what the--storedpreview does and does not count; - the door's comment and message;
- the
options/picklistTSDoc and describes; - the four hand-written docs pages. The
multiselectrow now says options are optional (free-form), which is the completeness module's measured NON-rule; the old "Required" was wrong.
- the changeset, including the stored-row paragraph, which names
-
Accepted as is:
Clause-②: yes, whereyes (narrowing)would be fuller;- the PR title
feat!against the changeset'sfix!.
The review judged both optional.
Out-of-scope findings:
- The
objectstack-upgradeskill'sos:checkblock teaches an optionlessselect(skills/objectstack-upgrade/references/examples-upgrade.md:105), which this door refuses at parse (check:skill-examplesonly type-checks). → filed as finding(skills): the objectstack-upgrade skill's "v17-canonical shapes, compiled" block authors aselectwith no options, whichFieldSchemarefuses once PR #21390 lands #21392 for the skills lane (Tier H). It should ship in the same release as this PR. - The blueprint schema accepts what the door refuses:
solution-blueprint.zod.ts:88(BlueprintFieldSchema.optionsoptional) and:320(StrictField.optionsnullable). → noted, not filed: nothing in either repo expands a blueprint intoFieldSchemainput;apply_blueprintlives outside both, so reach is not measured. The module header says the expansion is validated per type at write, so the refusal is loud there. - ADR-0078's § 4 sentence and its first Non-goal ("No hard Zod
.refine()…") now trail two maintainer rulings that put a parse refusal in front of the author-time rule: this one, and thereferencedoor (0fb8760bec). The ruling governs. → carrier: the maintainer.docs/adr/**is Tier H; this seat raises it in its report to the maintainer, not as a card. - objectui's derivation test (
object-metadata-write-guard.derivation.test.ts, "the installed server still ACCEPTS a choice with no options") turns red by design on objectui's next@objectstack/specbump, and its own comment prescribes the rewrite. → carrier: the objectui lane at that bump. It is not a hazard for this repo'smain: the Console Pin Gate builds without running objectui tests.
Next: ready, auto-merge, the queue.
Generated by Claude Code
-
objectstack-fleet commented
on Oct 2, 2026 ContributorAuthorMore actionsLanded: PR #21390 →
c52c49d96edomain:specseat 2 (session_01YDt3PzwfrkuFzUBF89WPmM), holder of claim5947645436· 2026-10-02T10:55Z- Landed: PR feat(spec)!: FieldSchema refuses a select / radio with neither options nor picklist #21390 went through the merge queue as
c52c49d96e, with one parent (69a12a0952). This card closedcompletedby itsFixesline. - Blob check against the ACCEPTed head
ee5b089b(ACCEPT5950415930, at-tier PASS5950368115): 16 of 16 files are blob-equal, the three generated reference pages included. - Delivered (ruling A,
5910124148, step two):FieldSchemarefuses aselect/radiowith neither a non-emptyoptionsnor apicklist, at parse, on theoptionspath, with a prescription naming both keys.- The predicate is ADR-0078's completeness check, read rather than restated.
- Stored rows keep their bytes and are read and named (
_diagnostics,/meta/diagnostics, the boot log). - Studio's half (objectui#11253) was already in the pinned console.
- Ships in the same release as: finding(skills): the objectstack-upgrade skill's "v17-canonical shapes, compiled" block authors a
selectwith no options, whichFieldSchemarefuses once PR #21390 lands #21392 (domain:skills). Theobjectstack-upgradeskill'sos:checkblock authors an optionlessselect, which this door refuses at parse. Neither publishes on merge. - Carried elsewhere:
- objectui's derivation pin, which turns red on objectui's next
@objectstack/specbump, belongs to the objectui lane at that bump; - ADR-0078 § 4 and its first Non-goal trail this ruling and the
referenceone. They are raised to the maintainer (docs/adr/**is Tier H); - the blueprint schema's looser
optionsis noted, not filed: reach was not measured, because the expansion lives outside both repos.
- objectui's derivation pin, which turns red on objectui's next
- State:
pm:dispatchedis removed in this act. Domain, area and type labels stay.
Generated by Claude Code
- Landed: PR feat(spec)!: FieldSchema refuses a select / radio with neither options nor picklist #21390 went through the merge queue as
- added 4 commits that reference this issue
on Oct 7, 2026
Ruled: 5910124148 · letter A — a
select/radiowith neitheroptionsnorpicklistis refused at the FieldSchema door (lookup precedent), after #19518 lands; census of stored rows and the Studio create-field order first · 2026-09-30T11:25ZFiled by the
domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357), from the #19518 dev report5908082075(open question 1) on PR #20823. The line is split from #19518 so that the picklist kind can land without it. ⛔ Not a claim. The decision analysis is the first comment.Governing text
picklistcollection,Field.select({ picklist }), server-resolved options, translation face (phase 1 of objectstack#18164) #19518, Scope item 2: "Field.select({ picklist: 'industry' }), mutually exclusive withoptions: both or neither on a select type is refused at the schema door with a prescription." Its Acceptance line reads: "picklist+optionstogether refused, neither refused".docs/adr/0078-no-silently-inert-metadata.md) putsfield/choice-without-options(error forselect/radio) in the author-time completeness gate thatos build,os validateandos lintenforce. Its registration-time twin inpackages/objectql/src/registry.ts"warns and never throws".0fb8760bec(fix(spec): require a non-empty reference on lookup/master_detail fields #13927) refused alookup/master_detailwithoutreferenceat parse. It shipped as aminorwith a BREAKING header and an ADR-0087 not-required marker.What the #19518 dev measured (branch
claude/issue-19518-picklist-kind)select/radiowith neither key was implemented and then withdrawn. With it in place, 4packages/specfixtures and 9 tests in@objectstack/metadata-protocol's stored-conversions and stored-migration suites went red: a storedselectrow with no options stops validating, andmigrateStoredMetadatastops rewriting it.selectfield before its options exist. The dev's report says such a save "would 422"; that is inferred, not measured.maintoday, aselectwith an empty option list at runtime turns off server-side value validation (record-validator.ts, the reason the ADR-0078 rule gives).Dedupe words: choice-without-options, select without options, picklist neither, FieldSchema select refuse
Blocked-by: objectstack-ai/objectui#11253