Skip to content

contract(spec): should dependsOn be authorable on a bulk action param? BulkActionParamSchema is not strict, so its accept is a null reading — and toBulkParam never consults the field defs, so the one spelling the renderer honours cannot reach the surface #18177

Description

@os-sam

os-decision-facets

维护者速读 —— 执行卡回箱,⛔ 不是未裁卡

Ruling-ref: 5714975133(批次 #146 item 4 · letter A,维护者「146 同意」 2026-09-17T13:16Z)。⛔ 方向早已裁定:BulkActionParamSchema 收严成 strictObject,并声明渲染器确实读取的每一个键。本块 ⛔ 不重新呈递方向。

回箱的唯一原因:裁决那句话按字面执行,包含 6 个协议从未承载过的键。

PR #19090 的达档复核(记录:PR 上的 5734128006)判 FAIL · 一条 BLOCKING:裁决写「declares every key the renderer measurably honours」,实测渲染器确实读取 21 个键,而 PR 只声明了 1 个(dependsOn),把另外 20 个拒了。复核的判语本席逐字采纳:席位不得先收窄裁决、事后再把差额立成一张卡。

⭐ 复核同时测出一条裁决当时没有的事实,它才是这张卡必须回到你手上的原因:

那 21 个「渲染器确实读取」的键 性质
15 个FieldSchema 上已有声明(min max step precision scale rows accept maxSize dimensions descriptionField idField allowCreate lookupColumns lookupPageSize lookupFilters) 可按裁决的「same shape and describe as the single-record twin」直接声明
6 个在协议里从未存在过任何声明(crop capture defaultName picker subtitle avatarField) objectui 渲染器的私有旋钮

⇒ 按字面执行 = 协议收编 objectui 的 6 个渲染器旋钮,从此是已发布契约,将来去掉要付整套退役成本。⛔ 那是你的决定,不是席位的,也不是 dev 的。

选项

做什么 客户/产品可感知的后果
A 声明那 15 个已在 FieldSchema 有形状的键;6 个私有旋钮保持被拒,并在拒绝处方里说清它们不是 FieldSchema 的键 作者在 bulk 参数上能写的键与单记录孪生对齐;objectui 的 6 个旋钮仍只能走渲染器,不进协议
B 声明全部 21 同上,且那 6 个旋钮成为已发布协议面 —— 以后删要走完整退役
C 维持 1 个(即 PR #19090 现状) ⚠️ 需要你明确改掉裁决那句话;代价是契约开始拒绝运行时确实提供的能力,正是裁决保留 dependsOn 时所给理由的镜像

⛔ 本席不替你选。本席唯一的读数:C 需要你改裁决原话,A 和 B 不需要。

四棱

  • ① 实际业务需求 —— 受害者是量到的:objectui 的渲染器今天就读这 21 个键,而协议收严之后,任何它没声明的键都会被拒绝。A 与 B 都让作者能写已在产的能力;C 让契约开始拒绝运行时确实提供的东西。
  • ② 项目长远合理性 —— 指向 A。那 15 个在 FieldSchema 上已有既定形状与 describe,声明它们是对齐而非扩面;那 6 个没有任何 spec 侧形状可对齐,收编它们是拿协议去背另一个仓的渲染器实现细节。
  • ③ 防 AI 犯错 —— 决定性,且指向 A 或 B 而非 C。北极星:「声明了的…在运行时兑现」。C 造出反向缺口:运行时兑现、契约拒绝 —— AI 作者按 schema 写就写不出已经能用的界面。⚠️ 但 B 有它自己的 AI 风险:6 个旋钮进了协议,AI 会以为它们是平台能力,而它们只是一个渲染器的。
  • ④ 创业阶段不扩散 —— 指向 A。15 个是对齐已有声明,零新概念;B 新增 6 个协议概念,且按阶段姿态「声明了但不兑现的键按发布批量退役」,收编一个只有单一渲染器兑现的旋钮正是它警告的方向。

⇒ 席位建议 A,并明说这是建议不是裁决。

同一轮还欠四项修订(⛔ 非 BLOCKING,但必须与裁决执行在同一次修订里落):① 拒绝处方对那 6 个键称「are keys of a FIELD (FieldSchema)」是假话,changeset / 迁移条目 replacement/reason / 模块 JSDoc 都重复了它;② hotcrm 那行「NOT REACHABLE … UNMEASURED」已过期 —— 现在测了,读数 0;③ 一个测试标题写错且与另一个重复(ActionParamSchema 在该文件里从未被 import);④ objectui 协同提示应为一个测试而非两个。

⛔ 席位自己的错,留在案:派发词里写的「hotcrm 本会话够不到」是错的 —— hotcrm 是公开仓,git proxy 直接服务匿名读;我从系统提示的「仓库范围」清单推断了不可达,而那份清单只列已挂载的仓。同样的错误也写进过 #18163 的派发词,已一并更正。

Prior rulings read: dependson,bulkactionparamschema,tobulkparam,contract,spec,authorable,bulk,action,param,strict,accept,null (+9 more) → 185 hits; ADR-0068 D1, ADR-0070 D5, ADR-0129 D3, ADR-0029 D6, ADR-0058 D3, ADR-0066 D3, ADR-0087 D1, ADR-0087 D2, ADR-0087 D3; thread: 1 ruling(s) (5714975133)

⚠️ 那 185 个语料命中本席没有逐条读,只逐条读了线程上的那 1 条(5714975133,即上面的 Ruling-ref),它是这张卡直接适用的裁决。9 个 ADR 决策单元按宽词网命中,未逐个核对是否切题 —— ⛔ 不当作已读背书。

本决策面由持卡席位 domain:spec#4(session_01AmH9bKvGoLjiY86Q4Z3og2)在 2026-09-19T12:59Z 补入卡面,应 H62 「落卡即带」 的归档职责;⛔ 卡体原文一字未动,原样保留在下方分隔线之后。四棱与选项的内容取自本卡评论 5734136342(达档复核 FAIL 的转箱记录),⛔ 非本次新判。


Filed unassigned and ungraded by the domain:spec @ objectui PM seat (os-sam, session session_01L5xpA5q533BgTTNADibEFt), 2026-09-14. ⛔ No domain:*, no priority:*, no pm:* — routing and grading are objectstack triage's, not a sister-repo seat's. ⛔ Not claimed.

Carrier for the half of objectui#8755 that is upstream of objectui and was deliberately left undone when its consumer-side fix landed (PR objectui#9479, merged as b8a006883d, 2026-09-14).

The question

Should dependsOn become authorable by contract on a bulk action param, and if so by which route?

  • Route A — close BulkActionParamSchema and declare the key. Makes the accept mean something. ⚠️ Strictness is a behaviour change for every existing author of a bulk param, not just for this key.
  • Route B — give the bulk surface the field-backed param route the single-record dialog already has. resolveActionParams consults the object's field definitions for the single-record path; resolveBulkActions.ts's toBulkParam never does, so FieldSchema.dependsOn — the one spelling objectui#8672 was ruled to honour — cannot reach the bulk surface at all, whatever the param schema says.
  • Route C — leave it undeclared, and accept that the renderer honours a key the protocol neither declares nor refuses.

⛔ This seat is not answering it. A lane PM answering a contract-shape question for the maintainer is out of bounds, which is exactly why objectui#9479 was written to neither depend on the answer nor pre-empt it.

The measurement that makes the accept a null reading

Taken against the installed @objectstack/spec 17.4.0, three parses per schema in one process, by the os-dev seat that landed objectui#9479:

positive control (minimal valid) negative control (nonsense key) subject (dependsOn)
BulkActionParamSchema parses ACCEPTED accepted
ActionParamSchema parses refused unrecognized_keys refused unrecognized_keys

BulkActionParamSchema accepted zzz_nonsense_key_that_no_producer_emits_8755 in the same run that it accepted dependsOn. ⇒ "the bulk schema accepts dependsOn" is not evidence that the key is licensed; it is evidence that the schema examines nothing. Its strict twin one surface over refuses both.

⚠️ Recorded because the instrument nearly lied the other way too: the first run of that probe used one shared base object and ActionParamSchema's positive control failed (it requires reference on an inline lookup param). Those legs were void, and the probe was re-run with a per-schema valid base rather than reported around. Both readings are now pinned in objectui as bulkLookupDependsOnReach-8755.test.tsx leg B, so the inference this card exists to stop fails loudly instead of being re-derived.

What objectui has already shipped, so the question is about the contract and not about behaviour

The consumer side is live and is not waiting on this card.

  • dependsOn was already a honoured key on a BulkActionParam for one of the two widget families that read it: bulkParamToField does not destructure it out, so it rides the ...extra spread onto the field bag, and SelectField / RadioField read field.dependsOn through useCascadingOptions. A bulk radio param declaring dependsOn gated and ungated on unmodified main before objectui#9479 — pinned there as the already-live CONTROL case.
  • objectui#9479 supplied the dialog record to the second family (reference-bearing pickers), which had the same key on the same bag and were never handed the record.
  • ⇒ retiring the key was measured off the table: an ablation that destructured dependsOn out of the adapter's spread turned 7 of 12 cases red, the option family's cascade included. Deleting the key would delete a capability that ships.

Why it is filed here and not in objectui

The two candidate routes both live in @objectstack/spec — one is the param schema's strictness and member list, the other is what resolveBulkActions.ts's toBulkParam is contractually required to consult. objectui can only observe the result. Per the cross-repo rule, an issue lives in the repository where the fix lands: remove objectstack from this card and nothing is left to do.

Dedup — declared with its boundary, ⛔ not asserted clean

Two semantic searches over this repository (BulkActionParamSchema + dependsOn authorability; BulkActionParamSchema + strictness/passthrough/unrecognized keys) returned 2 and 3 results respectively, all closed and none this question. Nearest neighbours, listed so they are not re-conflated:

  • objectstack#6257 — BulkActionDefSchema missing requiredPermissions (closed). Same schema family, different member, different defect.
  • objectstack#12615 — tsc does not police unknown keys on plugin action-param literals; the only enforcement is the ActionParamSchema strict parse at module load (closed). ⭐ That card is about the surface whose strictness this one asks the bulk twin to consider, so it is the most relevant precedent rather than a duplicate.

⚠️ Bound, and it is a real one. search_issues here is semantic, not an enumeration. A control query on an unrelated spec-export subject returned 31 results including open cards from 2026-09-10, so the index is live — but it did not return a card this seat filed in this repository earlier today, so the index appears to lag by at least hours. ⇒ a duplicate filed in the last day or two would not have been seen, and no complete enumeration of this repository's open issues was run. ⛔ Do not read the two zeros above as proof of no duplicate.

Source

  • objectui#8755 — the consumer-side card. Its three exits were: mirror the single-record twin, retire the inert emit-and-gate, or route a question to the producer. Arm 1 was taken and landed; this is the routed question.
  • objectui#9479 — the merged PR, with the strictness table, the ablations and the pins.
  • objectui#8672 / objectui#9398 — the single-record twin, ruled A, wire it, via the field-backed route.
  • objectui#4771 — closed not_planned 2026-08-18 with an explicit reopen trigger ("a real use case needs filter-condition / recipient-picker / lookup-filter params inside an action or bulk dialog") that both objectui#8672 and objectui#8755 satisfy. Worth reading before answering Route C.

Generated by Claude Code

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions