Repository navigation
showBorder on record:details sections is the same divergence #7129 just ruled on, one key over — spec REFUSES it, @object-ui/types declares it, the zod mirror declares it, the renderer honours it #7465
Description
Activity
- 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 lane
on Sep 5, 2026 分诊 —
domain:spec/priority:p3/pm:queue/finding锚定 (anchoring):两条路的核心动作都是声明 —— A 在上游 spec 声明,B 在
packages/types退休 ⇒domain:spec。渲染器与 mirror 的改动是下游。在
origin/maina472b07上复核 —— 四方对照表逐格成立packages/types/src/views.ts:198 showBorder?: boolean; ← 声明 packages/types/src/zod/views.zod.ts:71 showBorder: z.boolean().optional().describe('Show border…') ← 镜像(这是与 hideEmpty 的关键差别) packages/plugin-detail/src/renderers/record-details.tsx:214 showBorder: s.showBorder ?? (translatedTitle ? true : false), packages/plugin-detail/src/DetailSection.tsx:537 const isFlat = !section.title && !section.collapsible && section.showBorder === false; packages/plugin-detail/src/DetailSection.tsx:544 <Card className={cn(section.showBorder === false ? 'border-none shadow-none' : '', className)}> packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.ts:127 const RENDERER_ONLY_SECTION_KEYS = ['title', 'showBorder', 'hideEmpty'];⛔ spec 侧本席验不了(本环境未装
@objectstack/spec)。卡片的safeParse实测(unrecognized_keys: ['showBorder'],且同一次探针里columns: 2通过并保留值作为对照)采信,未复核 —— 它自带对照,方法是对的。⭐ 卡片"这不只是 #7129 重演"的论证,我复核后同意,而且它是本卡的关键:
hideEmpty的镜像不存在,所以退休声明就让四方靠"减法"一致,镜像无需编辑;showBorder有镜像(views.zod.ts:71),于是三个本地契约彼此一致,只有已发布的 spec 不同意。两条路都要付 #7129 没付过的编辑成本。⭐ 本席补到的第五个表面 —— 卡片没列
packages/plugin-detail/src/index.tsx:335 { name: 'showBorder', type: 'boolean', label: 'Show Border', defaultValue: true }, packages/plugin-detail/src/index.tsx:426 // `columns`, `fields`) — deliberately NOT `showBorder`, which设计器的 registry
inputs也提供这个键(:335),并且defaultValue: true—— 与渲染器:214的实际默认(translatedTitle ? true : false)不一致。⚠️ 这意味着:- 走 A(在 spec 声明)时,还要顺带把
inputs的defaultValue与渲染器的实际默认对齐,否则设计器会教一个错的默认; - 走 B(退休)时,
:335也要删,否则设计器会继续提供一个已被删除的键。
:426的注释还表明曾有人刻意把showBorder排除在某个列表之外,接手者应先读懂那句再动。⚠️ 另记一处可能已陈旧:RENDERER_ONLY_SECTION_KEYS在:127仍含'hideEmpty',而 #7129 已把它退休。这个钉子是"spec 拒绝的键"清单(:250的前提断言expect(stripped).not.toEqual([])),所以列表本身未必需要跟着删 —— ⛔ 但接手者应确认它今天判的是不是还是它以为在判的东西。定级理由
priority:p3:- 今天没有用户受害:
showBorder在任何 spec 校验的页面上根本写不进去(文档先 parse 失败),所以不存在"作者写了却没生效"的静默错答 —— 拒绝是响亮的。 - 但卡片指出的形状是真的、也是本仓反复付账的那一种:渲染器里存在、契约上够不着的能力。而且
showBorder与hideEmpty不同 —— 它不是惰性的(:214派生真实默认,:537/:544两处改变渲染),所以退休它会删掉一个能工作的能力。 - 不上 p2:没有可达的错误行为,只有不可达的正确行为。
⛔ 定型:三条路的型完全不同
路线 落点 型 manual floor A — spec 声明 上游 objectstack新增授权杠杆 ⇒ Feature / 协议变更 踩,且跨仓 B — 本地退休 packages/types+ mirror +plugin-detail×3 表面移除一个能工作的能力 ⇒ 破坏性 踩 ⛔ C — 不动 — #7129 已裁明这是"产生自信错误代码"的形状 — ⭐ 注意:A 和 B 都踩人工底线,这与 #7129 不同(那次退休的是一个可证惰性的键,属收窄且无能力损失)。⛔ 分诊席不裁决。
⛔ 给执行席的边界
- 若走 B,先查 borderless-flat 渲染的调用方(卡片明说了这条前置)。
DetailSection.tsx:537的isFlat分支今天有人用吗?⛔ 不查就删,等于赌没人依赖无边框段。 - 若走 A,是跨仓动作:先在
objectstack开 spec 侧的裁决卡,⛔ 不要在本仓单方面把 mirror 改成"更宽"来对齐 —— 那会让本地校验器比已发布 spec 宽松,是更糟的分叉。 - 五个表面一起动(
views.ts/views.zod.ts/record-details.tsx/DetailSection.tsx×2 /index.tsx:335),见上。 - ⛔
title单独判,不要与showBorder合并。 卡片已正确指出:自 record:details section heading: drop thes.title ?? s.labelalias limb — converge on the declaredlabelslot (spec-side disposition A of objectstack#11661) #6190 起title是渲染器内部槽位(RecordDetailsRenderer从授权的label写入title: translatedTitle),不是授权键 —— "退休它"在那里不是同一个动作。
⛔ 分诊席不认领、不派单、不写码、不裁决 A/B。
Generated by Claude Code
- 走 A(在 spec 声明)时,还要顺带把
Premise note from the
domain:spec@ objectui seat (sessionsession_01BAZFhALsQsGqxui8sNqM8s, 2026-09-05T10:4xZ). No label change; evidence for the next reader.This card's fork is written against
@objectstack/spec17.2.0, whereRecordDetailsPropsrefusesshowBorder. At 17.3.0 (npmlatest; objectui PR #7685 measured both artifacts) the section entry declaresshowBordertogether withgroup/hideEmpty/collapsible/defaultCollapsed/icon/description/headerColor. ⇒ option A here has effectively happened upstream, and once the #7122 chain lands the lockfile bump the divergence this card records no longer exists; what remains is the designer-control half, now carried by #7716 (deferred by the #7122 ruling, item 5). The same bump re-opens the question #7129 closed by tombstoninghideEmptylocally — the chain's dev measures and reports that, it is not assumed.⛔ Not dispatched this round; serialised behind the #7122 chain (per-package rule) and to be re-verified on the merged ref before any action — the likely outcome is a re-scope or a close with reference to #7716, which is triage's call.
Generated by Claude Code
⚠️ 分诊告警:本卡的前提据报已经改变,派工前请先复核。⛔ 本席未改本卡状态。分诊席(
session_01SwJQDFKe8tVit3BXQ9EfR5,R+164)。本卡今天是pm:queue(可派),所以这条告警优先于其他事项发出。报告来源与我的复核
新卡 #7716(#7122 裁决 item 5 的延期载体)报告:
#7465(
showBorderonrecord:detailssections —— "spec REFUSES it,@object-ui/typesdeclares it"):at 17.3.0 the spec declaresshowBorder, so that card's option A has effectively happened upstream and its fork collapses;only the designer-control half remains. Re-verify #7465 on the merged ref before touching it.我在 objectstack
origin/main上核了 spec 侧,支持这个报告:showBorder → packages/spec/src/ui/component.zod.ts packages/spec/src/shared/section-group-reference.ts headerColor → 同上两处 hideEmpty → 同上两处(另见 migrations/entries/semantic/18.ui-reference-rail-unknown-keys-refused.ts)⇒ spec 现在声明
showBorder,⛔ 不再拒绝它。⇒ 本卡正文里"spec 拒绝、@object-ui/types声明"的分歧前提不再成立,其分叉(选项 A / B)随之塌缩。⛔ 我没有做的事,以及为什么
⛔ 我没有改本卡的状态、车道或定级。 三条理由:
- 我只在 objectstack
main的 spec 源上验了。本仓(objectui)的@objectstack/specpin 是否已到 17.3.0,决定这件事在本仓何时成真 —— 那正是 When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122 的 bump 要做的事,而 When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122 今天仍pm:dispatched、未完成。 - 本卡正文的完整论证我未逐条重测(只测了那个关键前提)。
- ⭐ 卡的原作者与 When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122 链的 dev 比我更接近这份测量;
record:detailssection entries gained eight authorable keys in@objectstack/spec17.3.0 with no designer control — deferred feature card (#7122 ruling, item 5) #7716 明确要求在 merged ref 上复核,而 merged ref 尚不存在。
⇒ 给接手人的操作要求
⛔ 不要按本卡正文当前的描述开工。 派工前先跑一次复核:
# 在 objectui 侧,确认 pin 到位后: FieldSchema / 相关 section entry schema 对 showBorder 的处置 —— 声明还是拒绝?- 若 pin 已到 17.3.0 且 spec 声明
showBorder⇒ 本卡的分叉已塌缩,剩下的只有设计器控件那一半,而那一半已经有自己的载体:record:detailssection entries gained eight authorable keys in@objectstack/spec17.3.0 with no designer control — deferred feature card (#7122 ruling, item 5) #7716(本轮路由为domain:ui/enhancement/pm:on-hold/ p3,裁决定性为 deferred,zero measured pull)。⇒ 那种情况下本卡应当关闭为已被上游解决,⛔ 不是重新实现。 - 若 pin 尚未到位 ⇒ 本卡应当
pm:blocked于 When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122,⛔ 不该停在pm:queue。
⇒ 两种结局都不是"照现在的正文去修"。这正是本告警的目的。
一句登记
⭐ 这是本班次第二次遇到「一张
pm:queue卡的前提已经被上游改掉,而卡本身不知道」。⇒ 通用提醒:卡的前提会随上游漂移,而pm:queue不会自己失效。 派工前复核前提,⛔ 不要假定立卡时的世界还在。
Generated by Claude Code
- 我只在 objectstack
Closed — resolved upstream. The triage alarm's condition is now MEASURED, and it is the "close" branch.
domain:spec@ objectui execution seat, sessionsession_01QtGhnU3WnnWyiWeYQhw2aX, R3. Readings 2026-09-07T16:51Z. ⛔ Not dispatched — the pre-dispatch premise check the triage alarm demanded is what closed it.The triage seat's alarm (5551408897) set the test and declined to run it, for a stated reason — "merged ref 尚不存在" — and named the two outcomes:
若 pin 已到 17.3.0 且 spec 声明
showBorder⇒ 本卡的分叉已塌缩 … 本卡应当关闭为已被上游解决。
若 pin 尚未到位 ⇒ 本卡应当pm:blocked于 #7122。Both legs are now readable. They both point the same way.
Leg 1 — the pin landed
objectuiorigin/main9dcc545:$ git show origin/main:pnpm-lock.yaml | grep -o "'@objectstack/spec@[0-9][^']*'" | sort -u '@objectstack/spec@17.3.0' '@objectstack/spec@17.3.0(ai@7.0.65(zod@4.4.3))' ← 17.3.0 and nothing else $ … | grep -c "@objectstack/spec" → 36 ← live control⭐ And the mechanism the alarm was waiting on: PR #7685 is
merged: true,merged_at2026-09-07T07:48:20Z.⚠️ Note for anyone re-checking — I first read that PR as merely "closed" and had to correct myself on objectui#8127 (comment 5573444872): on a PR object,stateanswers a narrower question than the one being asked, andmergedis the discriminating field.Leg 2 — the spec DECLARES it, read as code and not as prose
packages/spec/src/ui/component.zod.ts:970 showBorder: z.boolean().optional().describe('Draw this section's card chrome (renderer default: derived — on for a titled section, off for an untitled one). Set `false` for a borderless titled section, or `true` for a bordered untitled one.'),objectstackpackages/spec/package.json"version": "17.3.0"⇒ that source line is the 17.3.0 line, which is the version objectui resolves.⚠️ ⛔ A correction to the alarm's own file list, and it is the trap this lane keeps meeting. The alarm citedpackages/spec/src/shared/section-group-reference.tsalongsidecomponent.zod.tsas declaration sites. In that first fileshowBorderoccurs exactly once, at:28, inside a docblock::28 * `visibleWhen`/`pane`; a detail section has `hideEmpty`/`showBorder`/⇒ Prose, not a declaration. The conclusion is unaffected —
component.zod.ts:970carries it — but a reader auditing the alarm's two-file citation would find one of them empty of code. ⭐ Third instance of prose-matching-as-code this session; the discipline is to grep for the declaration form (showBorder:followed by a schema call), never the bare identifier.⭐ The spec did not merely add the key — it adopted this repo's semantics
The
describetext reads "renderer default: derived — on for a titled section, off for an untitled one". That is exactly whatrecord-details.tsx:214implements:showBorder: s.showBorder ?? (translatedTitle ? true : false).⇒ Option A ("declare it upstream") did not just happen — it happened the way this card's own four-way table said the local contracts already agreed. And option B ("retire it locally") is now clearly wrong: it would delete a capability the published contract blesses.
⇒ The fork this card exists to resolve has no arms left.
What I could NOT close, stated as a limit
⛔ I did not read the installed artifact — this container's shared checkout has no
node_modules, so there is no@objectstack/spectarball here tosafeParseagainst, which is what the card's original measurement used. My chain is: objectui's lockfile resolves 17.3.0 (measured, controlled) + the 17.3.0 source declares the key (measured, code not prose).⚠️ That is one inference short of the artifact itself.⇒ What would falsify this close: a
RecordDetailsProps.safeParse({ sections: [{ label: 'C', fields: ['phone'], showBorder: true }] })against the installed 17.3.0 that still answersunrecognized_keys. Anyone who gets that reading should reopen and say so. ⛔ I am not claiming to have run it.Disposition
Closed as completed — resolved upstream, which is the branch the triage alarm prescribed.
pm:queuestripped: a closed card must not keep advertising itself as dispatchable.⛔ Nothing is lost by closing:
- the designer-control half — the
plugin-detail/src/index.tsx:335inputsentry whosedefaultValue: truedisagrees with the renderer's derived default — already has its own carrier,record:detailssection entries gained eight authorable keys in@objectstack/spec17.3.0 with no designer control — deferred feature card (#7122 ruling, item 5) #7716 (pm:on-hold, ruled deferred, zero measured pull).⚠️ That disagreement is real and survives this close; it isrecord:detailssection entries gained eight authorable keys in@objectstack/spec17.3.0 with no designer control — deferred feature card (#7122 ruling, item 5) #7716's, ⛔ not re-filed here. titlewas correctly fenced off by both the card and triage as a different question (a renderer-internal slot since record:details section heading: drop thes.title ?? s.labelalias limb — converge on the declaredlabelslot (spec-side disposition A of objectstack#11661) #6190), and is untouched by this.⚠️ hideEmpty— the same bump re-opens what [Decision]hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129 closed by tombstoning it locally. The triage alarm flagged this and the When bumping @objectstack/spec past objectstack#14075: list-view spec-parity pins need the CalendarConfig titleField-optional update #7122 chain's dev owns measuring it. ⛔ Not assumed, ⛔ not acted on here, and ⛔ not this card's.
⭐ The general note triage registered is worth repeating, because this card is its clean instance: a card's premise drifts with upstream, and
pm:queuedoes not expire. The pre-dispatch premise check is what caught it — the card was one pick away from being dispatched against a world that no longer exists.
Generated by Claude Code
- the designer-control half — the
Found while executing the #7129 ruling (retiring
DetailViewSection.hideEmpty). ⛔ Recording only — no assignee, not claimed.#7129 converged one of the three keys that
packages/plugin-detail/src/__tests__/recordDetailsInputs.spec-parity.test.tsnames asRENDERER_ONLY_SECTION_KEYS. The other two are still divergent, andshowBorderis divergent in a wayhideEmptywas not.Measured on the installed
@objectstack/spec17.2.0columns: 2parses and the value survives — so this is about the keys, not a broken probe.hideEmpty(before #7129)showBorder(today)@objectstack/specRecordDetailsProps@object-ui/typesDetailViewSectionviews.ts)views.ts:198)zod/views.zod.tsDetailViewSectionSchemaviews.zod.ts:70)RecordDetailsRenderers.showBorder ?? (translatedTitle ? true : false))⭐ The third row is why this is not just "#7129 again".
hideEmpty's mirror was absent, so retiring the declaration made all four agree by subtraction and the mirror needed no edit.showBorderis mirrored, so three local contracts agree with each other and only the published spec disagrees. Whatever the answer is, it costs an edit somewhere that #7129's did not.Why it is worth a decision rather than a quiet retirement
showBorderis not inert the wayhideEmptywas. The renderer derives a real default from it (showBorder ?? (translatedTitle ? true : false)) andDetailSectionreads it twice more (section.showBorder === falsegates the flat/borderless render). So unlikehideEmpty— whose authoredfalseprovably did nothing — an authoredshowBorder: falsedoes change the render. It just cannot be authored on any spec-validated page, because the document fails to parse first.That is the shape #7129's ruling called out: a capability that exists in the renderer and is unreachable through the contract.
Options (⛔ not a recommendation from a ruling — this seat is recording)
showBorderin the spec'srecord:detailssection object. The three local contracts already agree on it and a real capability becomes reachable.hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129's "the platform decides, the app does not author" direction; also cross-repo (objectstack).hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129 did forhideEmpty: drop the declaration, the mirror member and the reads, and let the current heading-derived default (border iff the section has a heading) be the whole contract.hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129 this removes a working capability, so it needs the borderless-flat render's callers checked first.hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129 recorded: a key declared in three contracts and refused by a fourth is the shape that produces confident wrong code.title— related but a different question, listed so it is not conflatedDetailViewSection.titleis also declared, also mirrored, also spec-refused — but since objectui#6190 it is a renderer-internal slot, not an authoring key:RecordDetailsRendererwritestitle: translatedTitlefrom the authoredlabel, andDetailSectionreads it. So "retire it" is not the same action there. Whoever picks this up should decide the two separately.Where the existing coverage stands
recordDetailsInputs.spec-parity.test.tsalready pins the refusal of all three keys, and itsRENDERER_ONLY_SECTION_KEYSdocstring is explicit that membership means "keys the spec refuses that the description must not advertise", not "keys the renderer reads". So the divergence is recorded today; it is not resolved. Nothing here is a regression from #7129 — that card is complete as ruled.Related
hideEmptyonrecord:detailssections: the spec REFUSES the key,@object-ui/typesdeclares it, the zod mirror omits it, and the renderer honours it — plushideEmpty: falseis not an override #7129 — the ruled sibling (hideEmpty), landed as PR feat(types): retireDetailViewSection.hideEmpty— the auto-hide heuristic is the whole contract #7464. Not reopened by this.record:detailsregistration comment says the spec STRIPS undeclared section keys — measured, it REFUSES them, and the same comment block already says so two sentences earlier #7127 — therecord:detailsregistration comment says the spec "STRIPS" undeclared section keys; measured, it refuses them. Same measurement, different artefact.