fix(spec): 给 dashboard widget 子树补下钻,并让门禁拒绝「未声明的容器继承」(#4956) - #5012
Merged
Conversation
…ontainer inheritance (#4956) The liveness ledger's `dashboard.widgets` entry claimed in prose that its widget-level props were 'classified in the DashboardWidgetSchema subtree'. No such subtree ever existed, the walk drills one level only through an explicit `children`, and `widgets` declared none — so all 22 authorable keys of the strict DashboardWidgetSchema sat outside the map while the gate printed green. - classify all 22 widget keys with closed-call-graph evidence (objectui @91757a7 + this checkout, 2026-08-03): 16 live, 6 dead - scripts/liveness/drill.mts: a container entry riding on inheritance must be DECLARED — drilled, or recorded in the shrink-only undrilled-containers.baseline.json. A new one fails; a stale row fails. - the gate now counts and prints the inheritance population every run, and its success line no longer claims a completeness it does not have Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
…g loop on dashboards (#4956) - scripts/liveness/drill.test.ts: the tree is fully reconciled by construction, so a green check:liveness proves only that the rule is QUIET. These prove it fires — undeclared container, stale baseline row, malformed baseline. - packages/lint: register `dashboard` in TYPE_COLLECTIONS. Drilling widgets produced five authorWarn keys; without the collection the ledger would be newly correct and newly silent, which is the shape that lint exists to prevent. - README: document the third direction + regenerate the dashboard count row (18/2 -> 33/8; +21 classified, all 22 widget keys entering the count at once) - changeset: the five new build-time author warnings, with FROM -> TO per key Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
Conflict was `liveness/dashboard.json`'s `_note` only; both sides kept and reconciled. #4876/#4995 recorded that `widgets[].responsive` deliberately carries NO ledger row because one would be an ORPHAN — true only while `widgets` was undrilled. The retiredKey tombstone keeps the key in the walked shape, so with the drill landed the row is REQUIRED (omitting it reports UNCLASSIFIED); it is present with the dead verdict the #3896 sweep never got to record. check:liveness green on the merged tree is the arbiter. os-regen four-step done: generated artifacts taken wholesale from origin/main, rebuilt, check:generated clean on the merged source, and every #4995 sibling entry asserted present (authorable-surface [RETIRED], spec-changes, upgrade guide, conversions registry, tombstone, changeset). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
…4956) Five dead keys -> #5010 (ADR-0049 enforce-or-remove); compareTo's one-path-only liveness -> #5011. A dead verdict with no issue behind it is indistinguishable from never having looked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 107 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
…eness-widget-drill
…eness-widget-drill
) Caught by this PR's own --undrilled worklist: the first baseline draft recorded all 64 containers as 'classified NOWHERE', and for six of them that was FALSE — object.fields[] IS FieldSchema (the `field` ledger classifies all 66 keys), object.listViews[] is the ListView surface `view.list` already drills, and so on. 248 of the 540 keys, nearly half. Shipping that note would have been this PR's own version of the defect it fixes. So a container now declares one of THREE dispositions, all data: - drilled (`children`) - deferred ({ container, to }) — RESOLVED by the gate, never believed - recorded (the shrink-only `containers` list) A deferral's target must exist (a governed type root, or a drilled type/prop coordinate) and classify EXACTLY the container's child keys. Dangling fails; drifted fails (equality, not subset — a container that grows a key its target never classifies is #4956 one level down); declared in both lists fails rather than letting the gate silently pick which is true. This is the issue's own second option — 'let the checker PARSE such a reference and error when it dangles' — and it is what lets the first option be honest. Pointing a deferral at 'DashboardWidgetSchema' now produces a named build failure, where the same words in a `note` were believed for a release. Counts are honest for the first time: 58 containers / 292 keys classified nowhere, plus 6 deferrals covering 248. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
…eport (#4956) A coordinate that is BOTH double-declared and gone was reporting twice — as a stale row and as a contradiction. The single fix is to delete the row(s), and a second heading about a coordinate that no longer exists obscures it. Same rule orphans.mts already follows for its one deliberately-silent case. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #4956
台账
dashboard.widgets挂着一条{"status": "live"},外加一句断言:per-widget props「classified in the DashboardWidgetSchema subtree」。那个 subtree 从来不存在。走查只下钻一层、且只认显式children,而widgets一个也没声明 —— 于是 strictDashboardWidgetSchema的 22 个可授权键一个都没被分类过,也从不计入unclassified,而每次运行都照常打印「all governed-type properties are classified」。这不是「一个键漏了」,是仪器在自己的成功信息里说谎。
widgets[].responsive正是靠这个缺口活过了 #3896 那轮清扫 —— 同一轮里它的亲兄弟widgets[].performance和字面同名的view.responsive都被删了,因为view有children下钻,list.responsive被问到了、拿到 dead、出局;widgets从来没人问。它最后在 #4876 / PR #4995 手工退休,晚了四天。一、先证红(仪器修复的必要条件)
以下三段全部在当前合并树上重跑,输出为实测粘贴。
① 修复前:门禁全绿,而键在地图之外。 把
widgets还原成修复前形态(单一 blanket 裁决 + 那句假断言),用origin/main的门禁跑:用同一份
--json做探针,问「这些键在地图的哪一侧」:既不在 classified,也不在 unclassified,整份报告里没有任何坐标 —— 而门禁是绿的。这就是 issue 的证据形态。
② 同一份修复前台账,换成本 PR 的门禁 —— 转红,并点名 22 个键:
③ 补完分类后 —— 绿,且数字第一次诚实:
20 → 41:22 个 widget 键一次性进入计数(父条目自身不再单独计一条)。
二、结构性修复:三种处置,全部是数据
issue 给了两条路,并倾向「补
children」。补了 —— 但只补数据等于修掉这一处实例,留着这一类缺口。实测全仓库:刻意没有把 65 个全部下钻:那意味着凭空编造 562 条没有证据的裁决,而伪造一条
live比诚实的粗粒度更糟。继承本身是合法粒度,不合法的是默默继承。于是容器必须声明三种处置之一,且三种都是数据:children{ container, to }containers列表里的坐标三者皆无 → 红,并打印它正在无声覆盖的子键。baseline 里某行的容器已经下钻了 → 也红(与 orphan 行同一种腐烂,方向相反:虚报的债和瞒报的债一样误导人)。
deferred为什么必须被解析,而不是被相信这条是被本 PR 自己的
--undrilled工作清单抓出来的。 baseline 初稿把 64 个容器一律记作「classified NOWHERE」,而其中 6 个那是假的:object.fields[]就是FieldSchema,field台账把它 66 个键全分类了;object.listViews[]就是view.list已经下钻的那张 ListView 表。248 / 540,将近一半。 照初稿发出去,就是在一个专门修「无法兑现的断言」的 PR 里,写下本 PR 自己版本的同一个断言。区别从来不在那句话,而在谁来核。deferral 把目标写成数据,门禁去解析:目标必须存在(受管类型根,或一个已下钻的
type/prop坐标),且其已分类键集必须与容器子键集相等。悬空 → 红;漂移 → 红(用相等而非子集:容器多出一个目标从不分类的键,就是 #4956 下移一层);两个列表同时声明 → 也红,而不是让门禁悄悄选一个当真。实测,把 deferral 指向 issue 里那个不存在的名字:
目标存在但键集漂移:
这正是 issue 的第二条建议(「让 check-liveness.mts 能解析这种引用,并在引用悬空时报错」),而它也是第一条建议得以诚实的前提。当前 6 条已解析 deferral:
选「baseline 文件」而不是「每条加一个
subtree: inherits字段」,理由正是让 AI 写的东西难写错:加一行到一个以债务命名的文件里,是评审能看见的动作;在自己正在写的条目旁边多敲一个词,摩擦低到等于没有。逻辑放在纯函数模块scripts/liveness/drill.mts+ 单测,理由与orphans.mts一致:落地那一刻全树已自洽,绿灯只能证明规则安静,证明不了它会响。三、22 个键的裁决(证据工作,不是猜)
逐键手工闭合调用图,objectui@91757a7 + 本仓库 HEAD。16 live / 6 dead。本 PR 不退休任何键。
两个方向相反、都值得记住的结果:
requiresService是 live —— 但只在服务端。 在 objectui 里怎么量都是死的;真正的消费者是packages/rest/src/rest-server.ts的filterDashboardForUser(ADR-0057 D10):未注册的 kernel service 会让 widget 直接从 payload 里被剥掉,:3037/:3476有调用点,rest.test.ts:3271有测试。只看渲染器仓库就下 dead,会删掉一个正在生效的门。compareTo是 live,但只在一条路径上。 内联 object-provider 图表路径三个分支全实现;ADR-0021 dataset 路径(spec 自称「唯一面向作者的分析形态」)上,两个字符串分支被 DatasetWidget 显式丢弃,{ offset }被转发进一个要求{ kind, dimension }的契约 → 执行器抛错。照action.disabled先例记live并把边界写死在 note 里,另开 dashboard widgetcompareTo:三个声明分支在 ADR-0021 dataset 路径上全部无效(两个静默丢弃,一个抛错) #5011。dead 的 5 个(
responsive已由 #4995 退休,不计)colorVariantoptions.colorVariant能到达 MetricWidget。本仓库自带的system_overview.dashboard.ts写了 7 次actionUrlactionUrl全属于header.actions[](另一个 schema)。没有任何渲染器画 widget 级按钮actionTypeactionIconariawidget.aria消费者;与 17.0.0 删掉的看板级aria同一种假合规二阶成本:
validate-dashboard-action-refs.ts对widgets[].actionUrl做引用完整性强校验、解析不到就 ERROR,docblock 还称它为 "the per-widget button" 并声称 mirrors 运行时 dispatch —— 而那个按钮不存在。今天一个作者可能因为一个永远不会渲染的按钮指向不存在的 action 而被卡住构建。ADR-0049 enforce-or-remove 另开 #5010(未指派),本 PR 只记录裁决。
四、闭上作者侧的环
台账新增 5 条
authorWarn,但dashboard不在 CLI lint 的TYPE_COLLECTIONS里 —— 那条列表自己的注释写着这正是「ledger correct and silent」的形状。所以同时把dashboard注册进去;getNested本就支持数组层扇出,widgets.colorVariant会检查看板上的每一个 widget(测试里专门有一条:dead 键在第二个 widget 上)。五、验证(全部在合并后的树上重跑)
check:livenesscheck:generated(8 项)check:empty-statecheck:strictness-ledger@objectstack/spec test@objectstack/lint testtypecheck(spec + lint)tsc --noEmit无输出新增单测:
drill.test.ts28 条 —— 规则会响(未声明容器 / 陈旧 baseline 行 / 畸形 baseline / 悬空 deferral / 键集漂移(两个方向)/ 双列表声明),以及它必须保持安静的每一种情形;lint-liveness-properties.test.ts+5 条(dashboard 扇出、hint 内容、全 live widget 静默)。与 #4995 的共享面
PR #4995 在本单进行中落地。已
git merge origin/main(未 rebase、未 force-push),冲突只在_note一处,双边保留并重写:#4876 记录了「responsive故意不留台账行,否则是 ORPHAN」—— 这在widgets未下钻时是对的,而 retiredKey tombstone 会把键留在走查形状里,所以下钻落地后那一行是必须的(不留 → UNCLASSIFIED)。它现在带着 #3896 那轮没来得及记的 dead 裁决在位。合并后check:liveness绿,即两处改动可以正确合成的裁决证据。os-regen 四步已执行(含随后 #5003 / #5006 / #5008 落地后的第二、三次合并):generated artifacts 整体取自
origin/main→ 重装重建 →check:generated对合并后的源全绿 → 逐项断言 #4995 的兄弟条目存活(authorable-surface.json的[RETIRED]、spec-changes.json×2、升级指南、references、conversions registry ×2、tombstone)。不做的事
compareTo:三个声明分支在 ADR-0021 dataset 路径上全部无效(两个静默丢弃,一个抛错) #5011 各自跟进)dashboard.zod.ts(refactor(spec)!: retiredashboard.widgets[].responsive(#4876) #4995 的面)content/docs/releases/🤖 Generated with Claude Code
https://claude.ai/code/session_01Ehu85kbvMcrNTUJjwxvLJ9