Repository navigation
finding(types,docs): BaseSchema.hidden is declared boolean, but the renderer evaluates it as a predicate — the third key on visible/disabled's evaluated path, never widened #7455
Description
Activity
- addeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 3, 2026 - addeddomain:specobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneobjectui spec stream: fix lands on packages/types, schema corpus or spec pin coupling — spec laneand removeddomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatobjectui ui stream: fix lands on the published library or apps — objectui execution seat
on Sep 3, 2026 Triage — R+118.
needs-user-decision·priority:p2·domain:spec. ⛔ Not claimed, not dispatched.os-decision-facets
⚠️ Re-routed:domain:ui→domain:spec⛔ Not a correction to the filer — the symptom really is in the renderer. But the routing rule reads the lane from where the fix lands, and "症状位置不改流向". Under either option the diff is
packages/types/src/base.ts+packages/types/src/zod/base.zod.ts+ the docs row;SchemaRenderer.tsxis not touched at all.packages/typesis the spec lane's co-governed surface, and contract decision cards route there on the same line. Two independent grounds, same answer.Verification (
origin/mainac8d523,git show/git grep— ⛔ never the working tree)Every claim in the card holds:
TS ( base.ts)zod ( base.zod.ts)visibleboolean | string:281z.union([z.boolean(), z.string()]):158hiddenboolean:328z.boolean():175disabledboolean | string:354z.union([...]):190hiddenis the odd one out on both faces, and the docs row (content/docs/api/schema-reference.md:72) states the narrow reading as behaviour verbatim as quoted. The renderer leg and the pins are real:hasDeclaredPredicateatpackages/core/src/evaluator/declaredPredicate.ts:149is the single shared definition —visible/hidden/disabled, all three*Onsiblings,ActionRunnerandActionEngineall ask it, and its own header records that consolidation.SchemaRenderer.hiddenDeclaredGate.test.tsxpins expression-valuedhiddenboth ways, at:180and:188.⭐ Two facts the card does not have, both of which move the decision
① The runtime accepts a THIRD shape, and no key declares it. The pins at
:188exercisehidden: { dialect: 'cel', source: 'true' }— a CEL envelope object.hasDeclaredPredicateaccepts it for every key that asks. So the declared/actual gap is nothiddenalone:visibleanddisabledareboolean | stringand the envelope form is undeclared on all three. ⇒ Option A as written ("widen toboolean | string, consistent with #4581/#4580-Q3-A") makeshiddenconsistent with two siblings that are themselves still under-declaring the same shape. That is a defensible thing to choose, but it should be chosen knowingly, not as "matching the precedent".⛔ I checked whether the zod mirror already accepts the envelope and it does not —
base.zod.ts:170isz.string(). The envelope is accepted atform.zod.ts:583(ExpressionWireSchema), but that isFormFieldSchema, a different schema. I nearly published "the zod mirror is already ahead of TS"; it is not, and that claim is withdrawn before it was made.② ADR-0089 put the boolean key out of scope ON PURPOSE — and objectui widened it anyway. ADR-0089 (Accepted 2026-07-14,
docs/adr/0089-unify-visibility-predicate-naming.md) consolidated the predicate spelling onvisibleWhen, deprecatedvisibleOn, and states verbatim:The boolean
visible(Tab on/off) is a different type and concept and is explicitly out of scope.objectui#4581 then widened
visibletoboolean | string— turning the key the ADR carved out into a second predicate spelling besidevisibleWhen. Measured here:visibleWhenexists on both faces (base.ts:289,base.zod.ts:164), andhiddenWhen/disabledWhendo not exist at all.⇒ The precedent the card leans on is not neutral. Option A extends, a third time, a direction an accepted ADR explicitly scoped itself against.
⚠️ Stated precisely, ⛔ not overstated: ADR-0089 governspackages/spec's keys. objectui'sBaseSchemais objectui's own surface, so the ADR does not bind this decision. It is evidence of intent about the same concept, and it is why this is a ruling rather than a mechanical widening.四棱
① 长期正确性(主导,不可被其余合并否决) —— 三键同处一条求值路径,却有三种声明宽度和两种未声明的形。真正的长期正确解是先定义这条路径的声明契约(布尔键到底是不是谓词位),再让三键一致落到该契约上;单独把
hidden抬成boolean | string,是把「不一致」从一处搬到另一处,并且让 ADR-0089 的边界第三次被越过而无人记账。⇒ 本轴明确不支持 A 的现写法。② 真实业务拉力 —— 能力已发货且有 pin,今天没有任何作者被挡住:写表达式的
hidden现在就能跑,只是要一次 cast。拉力弱且真实,不是零。③ 防 AI 编码错误 —— 本轴分量最重的一处,且指向与 ② 相反:声明说
boolean、文档说「takes no expression」、运行时却求值——这正是 ADR-0089 自己写的最坏失败模式(「no signal to self-correct from」)。AI 作者读文档会得到一个假事实。⛔ 但注意:修好这一条不需要加宽,把文档行改成描述实际行为同样消除假事实。④ 创业期不扩张 —— A 扩张已发布类型面且需 zod 镜像跟随;B 只改一行散文与一行文档;C(补信封形)扩张最大、动三键。
建议(一句):走 B 的最小形——只把文档行改成描述渲染器的实际行为,⛔ 不动声明,并在同一 PR 里把「布尔键是否为谓词位」作为契约问题原样立卡,留给 ADR 层面回答。
回退:若维护者认为
hidden与visible/disabled的不对称本身就是不可接受的作者体验,则走 A,并同时记一笔:信封形在三键上仍未声明,ADR-0089 的边界已第三次被越过——⛔ 不要把 A 写成「与 #4581 一致」就算完。置信缺口(必填):我没有测
visible的加宽(#4581)与 ADR-0089 的接受时间线是否被当时的裁决显式权衡过——我只测到 ADR 早于 #4581,没读到 #4581 的裁决正文。若 #4581 当时已经明确讨论并接受了「objectui 侧布尔键可以是谓词位」,那么 ADR-0089 的边界在 objectui 侧就已经被有意让开,我上面 ① 与建议的分量都应下调,A 变成正解。这一条足以翻转建议。For whoever takes this, whichever way it is ruled
⛔ Do not land a diff that leaves the declaration, the zod mirror, the docs row and the pins saying more than one thing — that is the card's own closing point and it survives every option. Stop-condition: if the ruling is B and you find the docs row is generated rather than hand-written, ⛔ do not hand-edit the generated file — name the generator in the report and stop.
Related, ⛔ not duplicates: #7088 (where this was found; its ruling is comment-level, Clause ②
no) · #4581 · #4580 Q3-A · #3955 · #3850.
Generated by Claude Code
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsMaintainer ruling recorded — A: widen
hiddentoboolean | stringon both faces, docs row and pins follow; the envelope form goes to one card for all three keys (2026-09-03)Provenance. Ruled by the maintainer (pm@objectstack.ai) in the director seat's live session (
session_01ShyhexkB2d1AeRZ85tgAAe), decision batch #24, 2026-09-03 after 16:30Z. Verbatim reply: 「11977 转维护者,其他同意」 — this card was presented with recommendation A plus one three-key card for the envelope shape (fallback B), so 「其他同意」 adopts that. Ledger: objectstack#12708, batch #24 ledger comment.What was ruled.
packages/types/src/base.tshidden?: boolean→boolean | string;packages/types/src/zod/base.zod.tsz.boolean()→z.union([z.boolean(), z.string()]), matchingvisibleanddisabledon both faces;content/docs/api/schema-reference.md:72says what the renderer does (an expression is evaluated;hiddenOnremains the sibling spelling) — if the row is generated, the generator is run, ⛔ never a hand edit of a generated file;SchemaRenderer.hiddenDeclaredGate.test.tsxdrops theRecordcast for the string form; the JSDoc onhiddenrecords the reasoning the wayvisible/disabled's blocks already do, and notes that ADR-0089 governspackages/spec's keys, not this surface.- The envelope object form is not decided here. It is accepted by the shared evaluator on all three keys and declared on none; objectui#7530 carries that question for
visible/hidden/disabledtogether (declare on all three, or refuse on all three). ⛔ This PR does not declare the envelope onhidden.
Execution.
domain:speclane (packages/typesco-governed surface), S. Clause-② yes — published type surface widens —needs:contract-reviewcarrier; changesetminoron@object-ui/types.Fixes #7455.State.
needs-user-decision→pm:queuein the same stroke;priority:p2,domain:speckept; unassigned.
Generated by Claude Code
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actions🔧 认领
domain:ui执行席,sessionsession_01EMrWaQw3XS5DxTHxp4yRyC,分支claude/issue-7455-hidden-predicate-widen。按 2026-09-03 的维护者裁决 A(评论 5529434441,provenance:维护者 pm@objectstack.ai 在 director seat 实时会话中,决策批次 #24,逐字「11977 转维护者,其他同意」,同笔迁
needs-user-decision→pm:queue)派发。⚠️ 给实现者的第一条提醒:本卡正文写着「## Not decided here」并列了两个方向——那是历史,写于裁决之前。裁决评论取代它。⛔ 不要按正文去做「先立契约再决定」的那条路,裁决已经选了 A。(本席今天在 #6939 组 3 上正因为读正文没读裁决评论,把一张已裁的卡误记为待裁两小时。)裁决划定的范围,逐条
packages/types/src/base.ts:hidden?: boolean→boolean | stringpackages/types/src/zod/base.zod.ts:z.boolean()→z.union([z.boolean(), z.string()]),与visible/disabled在两面对齐content/docs/api/schema-reference.md:72:改成陈述渲染器的实际行为(表达式会被求值;hiddenOn仍是兄弟拼法)。⛔ 若该行是生成的,运行生成器,绝不手改生成文件——裁决明写了这条停止条件SchemaRenderer.hiddenDeclaredGate.test.tsx:字符串形去掉Recordcasthidden的 JSDoc 按visible/disabled已有的写法记录理由,并注明 ADR-0089 管的是packages/spec的键,不管这个面
⛔ 明确划在范围外
**信封对象形
{ dialect: 'cel', source: … }不在本卡。**共享求值器在三个键上都接受它、三个键都没声明它;objectui#7530 统一裁这三个键(要么三个都声明,要么三个都拒)。⇒ 本 PR 不得在hidden上声明信封形,即使 pin 里:188那一例正在用它。执行参数
Clause-② yes(已发布类型面加宽)⇒ draft +
needs:contract-review,隔离审查由本席委派,⛔ 免复核不放行。changesetminor(@object-ui/types)——⛔ 不是 patch,裁决明写。Fixes #7455。
Generated by Claude Code
os-project-manager commented
on Sep 3, 2026 CollaboratorMore actionsos-dev-report
Clause-②: yes — draft +
needs:contract-review(read back and confirmed present), not marked ready, no auto-merge.{ "issue": 7455, "status": "done", "branch": "claude/issue-7455-hidden-predicate-widen", "pr": "https://github.com/objectstack-ai/objectui/pull/7543", "premise_still_valid": true, "summary": "Clause-②: yes. Ruling A implemented at f7b8948b1 (origin/main merged in before the final run, so the #7533 collision on schema-reference.md is clear). BaseSchema.hidden is boolean | string on both faces, the docs row at :72 now states what the renderer does with hiddenOn named as the sibling spelling, the JSDoc records the reasoning the way visible/disabled already do and notes ADR-0089 governs packages/spec's keys and not this surface, and the string form in SchemaRenderer.hiddenDeclaredGate.test.tsx now goes through a typed cast-free helper while the envelope pin keeps its existing helper untouched. The envelope object form is declared on nothing and the PR body says so per key rather than claiming consistency with #4581/#4580-Q3-A. Two things the ruling could not have known: (1) content/docs/api/schema-reference.md is hand-written, measured four ways with a lit control, so the generated-file stop-condition does not fire; (2) the widening seeds one KnownDrift entry, because @objectstack/spec@17.2.0's AppSchema declares hidden (boolean-only) and declares neither visible nor disabled, so SpecAppFields overrides the base's hidden on AppComponentSchema's zod face while the TS interface inherits the widened union. Handled as that file instructs, by measuring; the collision underneath is filed unassigned as objectui#7542 and explicitly not decided here. Nothing declares the envelope form; nothing was closed by dropping hidden from SpecAppFields.", "tests": "All at f7b8948b1 (final commit, re-run after merging origin/main). Exit codes captured before any pipe; results quoted from each gate's own verdict line. GREEN: pnpm build whole repo, 43 packages 0 errors — this is also the consumer sweep, direction dependents of @object-ui/types, all of them. pnpm --filter @object-ui/types type-check (tsc + examples + tsconfig.test.json) exit 0, 0 error-TS lines, script name echoed. pnpm --filter @object-ui/react type-check (tsc + tsconfig.test.json) exit 0, 0 error-TS lines. vitest packages/types/ = 97 files 1642 tests passed. vitest on 6 react visibility/enablement suites = 6 files 105 tests passed. Gates: control-bytes OK (6212 files), doc-types OK, doc-fences OK, doc-snippets OK (456/456 blocks judged against built types), phantom-deps OK, published-tsconfig-exclude OK, readme-exports OK over the full population (0 unbuilt, 52 keys compared — it was PREREQUISITE-NOT-MET before the full build and is reported as a real reading only after it), changeset-presence OK (1 changeset), changeset-no-major OK, governed-queue-guard --test over all 7 changed paths = NOT GOVERNED. pnpm lint (eslint . --no-inline-config) run over the FULL repo, not narrowed: 4257 files linted, my 5 changed source files 0 errors and 0 new warnings; 93 pre-existing errors across 77 files, none touched by this diff. RED-FIRST, on the unmodified base tree: hidden:'<predicate>' safeParse.success=false with invalid_type expected boolean at path hidden, while the same string on visible parsed (lit control) and hidden:true parsed (control legal in both states) — so the zod mirror was NOT ahead of TS, confirming the triage's withdrawn-then-corrected claim. REVERSE VERIFICATION, direction predicted before running, mutation = restore hidden to boolean-only on both faces keeping every pin. Mutation and restore each proved on disk by anchor counts in both directions; dist re-read each way so nothing was judged against a stale .d.ts; the restore proved by STATE — git diff HEAD empty AND both worktree blob hashes byte-identical to their HEAD blobs — never by an exit code; the mutating script carried a trap on EXIT INT TERM with absolute paths. Predicted RED and observed RED: 3 invariant-Equal assertions TS2344 at :90/:95/:98, 2 authorable string fixtures TS2322 at :106/:112, the zod-face runtime case failed, and the 2 declared-path react call sites TS2322 at :210/:213 which were the ONLY errors in the entire react test project. Predicted GREEN and observed GREEN in both states (carriers legal either way): every runtime case in SchemaRenderer.hiddenDeclaredGate.test.tsx including the new declared-path one, base-schema-visible-predicate.test.ts, and the visible/disabled/boolean/number control cases inside the new pin — the number refusal being the anti-overshoot guard that z.any() would defeat. ONE PREDICTION WRONG, and it was NOT-MEASURED rather than green: the first mutation run measured types and react in one recursive pnpm --filter, which stopped at the first failing package (ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL), so the react leg never ran; it was re-run alone under the same mutation and produced exactly the 2 predicted errors. Attribution of the zod-mirror-parity firing is measured, not assumed: under the same mutation that error disappears. The pin file is genuinely compiled — tsc -p packages/react/tsconfig.test.json --listFiles reports it, so the type-level half is checked and not merely written. NOT MEASURED, declared: remote CI convergence (handed over at draft-PR time per the dispatch contract); and the two react-runtime build errors seen once under pnpm --filter '...@object-ui/types' build were a filter artifact (react-runtime depends only on sucrase, so it fell outside the dependents closure and its dist was never built) — superseded by the full green pnpm build, not carried as a finding.", "mcp_calls": "5 — issue_read get, issue_read get_comments, issue_write create (#7542), create_pull_request, this comment. De-duplication and every read-back went through the zero-quota repo-scoped REST channel, which was probed first and answered 200.", "open_questions": [], "out_of_scope_findings": [ "filed as #7542: AppComponentSchema inherits the renderer's hidden predicate from BaseSchema while its Zod mirror takes @objectstack/spec's app-catalogue hidden boolean — one name, two meanings. Measured on spec@17.2.0: AppSchema declares hidden (boolean-only) and declares neither visible nor disabled, which is why #4581 and #4580-Q3-A seeded nothing and this widening seeds one KnownDrift entry. Unassigned, unlabelled, three directions measured and none chosen. De-duplicated against 369 open issues via a bounded repo-scoped REST listing plus a local filter, with a lit control that returned #7455 and #7530." ] }De-duplication control, since a zero here would otherwise be indistinguishable from a dead channel: the same listing-plus-filter that returned no candidate for #7542's subject did return #7455 and #7530 for the lit control term, over 369 open issues across 4 pages.
Written by an ObjectUI development agent (Claude Code), session reference
session_01EMrWaQw3XS5DxTHxp4yRyC.
Generated by Claude Code
Found while correcting
BaseSchema.hidden's JSDoc for #7088. Filed unassigned and out of that card's scope: #7088's ruling is comment-level with Clause ②no, and this is a change to the declared type surface, which is the kind of thing #4580 needed a ruling for.The claim
packages/types/src/base.tsdeclares:content/docs/api/schema-reference.mdstates the same thing as behaviour:What the renderer does
SchemaRenderer.tsx'sshouldHidechain does not read this key as a boolean either. It calls the same evaluator every other leg calls:and predicate strings on
hiddenare already pinned as working, inpackages/react/src/__tests__/SchemaRenderer.hiddenDeclaredGate.test.tsx:it('an expression-valuedhiddenkeeps its verdict, both ways')—hidden: '${data.status === "draft"}'hides,hidden: '${data.published}'does not;it('a non-empty CEL envelope keeps its verdict, both ways')—hidden: { dialect: 'cel', source: 'true' }hides.So the declaration under-reports a shipped, tested capability, and the docs row states the under-report as a fact about behaviour.
Why this is the same defect twice already fixed next door
visiblecarried exactly this asymmetry until objectui#4581 widened it toboolean | string, on the evidence that the renderer evaluates rather than reads it.disabledcarried it until #4580 ruling Q3-A widened it on the identical evidence.base.ts's own JSDoc for both keys now records that reasoning in full — and both blocks note the sibling*Onkey exists for the same reason, which is the argument that was NOT taken to mean the base key should stay boolean.hiddenis the third key on that evaluated path and the only one still declared boolean-only, withhiddenOnas its sibling. The two earlier widenings also record that fixtures had been casting past the declaration; the pins above pass arbitrary values through aRecordcast, so the same pressure is present here.Not decided here
Whether to widen
hiddentoboolean | string(with the zod mirror and the docs row following), or to keep the narrow declaration deliberately and correct the docs row to describe the shipped behaviour instead. The two produce different diffs and different published surfaces:hiddenOn) already.hiddenOnas the declared expression spelling and the evaluator's tolerance as incidental; then the docs sentence "this key takes no expression" needs rewording, since it is false of the renderer, and the pins that exercise expression-valuedhiddenare pinning tolerance rather than contract.Either way the declaration, the docs row and the pins should end up saying one thing; today they say two.
Refs #7088 · #4581 · #4580 · #3955.
Generated by Claude Code
Generated by Claude Code