Skip to content

[decision] the doc-snippet gate's specifier map reads dependencies only — and two packages were silently supplying React's types to the whole documentation corpus #8919

Description

@os-zhuang

由 domain:ui 派发席(session_01611D6ZaRaMmwTNQmSbk8MH)在 objectui#8303 的实现轮后立卡。全部读数由该卡实现席在两棵对齐的树(均为 origin/main 4fa0eb9c4)上实测,⛔ 非推断。Generated by Claude Code.

一、被测出来的结构事实

scripts/check-doc-snippet-types.mjs 的 third-party specifier map 只从 dependencies 构建(刻意如此,⛔ 不读 peerDependencies、⛔ 不读 devDependencies)。

而 packages/layout 与 packages/plugin-dashboard 是全仓仅有的两个在 dependencies 里声明 react 的已发布包。

⇒ ⭐ 这两个包一直在为整个文档语料供应 React 的类型解析。 一旦把 react 从它们的 dependencies 移走(objectui#8303 要修的正是这个缺陷 —— 一个库同时向宿主要 React 作为 peer、又自己 pin 一份,会把第二份 React 拉进消费者的树,是 Invalid hook call 的经典成因),这道门立刻拒收 34 个 block。

其中只有 2 个在那两个包自己的 README 里;另外 32 个属于 19 个文件、分属本卡根本不碰的包。

对齐两棵树的 A/B(都在 4fa0eb9c4,都先跑过门自己的 scoped build)

Doc Snippet Type Check 门自己的 pin 测试
main Root bound: no block imports a specifier that resolves only through the ROOT manifest. · 638 of 638 judged, 0 failed · exit 0 exit 0,101 passed
分支 Root bound: 34 block(s) … (react) … NOT semantically checked · 604 of 638 judged, 1 failed · exit 1 exit 1,1 failed / 100 passed

⭐ 机理由门自己打印,⛔ 非推断:main 上 89 specifier(s) mapped from the declared dependencies of 34 imported package(s),分支上 88。恰好一个 specifier 离开了 map:react。 门自身的全部对照(resolution / sentinel / positive / undeclared / root-declared)两侧读数完全相同。

⚠️ 两条红是同一个不变量的两个载体:Test (shard 1/4) 就是 scripts/__tests__/check-doc-snippet-types.test.ts 里断言同一个 root-bound 不变量的那条 pin,收到的是同一批 34 条。⇒ 修一次两边都动,⛔ 不是两个问题。

二、⛔ 为什么其它路线都已被实测关死

门自己推荐的解法对 react 不成立。 它的拒绝信息点名 declare const stand-in,并把 packages/layout/README.md 的 "Usage with React Router" 作为范例 —— ⚠️ 而那个范例本身就是被拒的 34 个之一(它 import ComponentType / ReactNode,恰恰因为该包在 dependencies 里声明了 react)。

带 trap 的一次性探针照做后:block 跳出 bound(Root bound: 33 · 605 of 638),直接撞出 6 个新的 TS2786 —— 一个当作 JSX 组件使用的 stand-in,必须让返回类型可赋给 React 真实的 ReactNode,⭐ 而真实的 React 类型正是刚离开 map 的那个。JSX 本身仍能编译(这就是另外 604 个还过的原因),因为 JSX runtime 类型经根解析、不走这张 map ⇒ 这类 block 既 import 不到 React 的类型,也无法自己手写出来。

量化:34 个里 27 个含 JSX,属探针实测失败的那一类;仅 7 个不含 JSX,其中数个 import useState / useEffect / forwardRef,stand-in 还得复刻它们真实的泛型签名才谈得上「检查了什么」。@types/react 在全仓零个包的 dependencies 里(30 个包的 devDependencies 里有),⇒ React 的类型词汇经这张 map 无路可达。

⛔ 也没有「记账接受」这条路。 那条 pin 是 expect(offenders).toEqual([]) 平铺断言 —— 无豁免表、无账本、无理由字段。⇒ 34 条不可能被记成已接受的债,即便有人想这么做;CI 会一直红,而不是安静降级。

三、决策块

① 待决问题

doc-snippet 门的 specifier map 该不该把「工作区自己能解析的 peerDependencies」也纳入?

② 选项 × 代价

做什么 代价
A 教 check-doc-snippet-types.mjs 在工作区自身能解析该 specifier 时,也映射 peerDependencies 里声明的它;并同笔放宽那条 pin ⭐ 唯一同时修好缺陷、又保住文档覆盖的选项,且门自己的头注就预见了它:dependencies-only「fails CLOSED, and widening it later is a visible edit with a reason, not a silent drift」。⚠️ 代价是对真正可选的 peer 削弱了 fail-closed;改门 + 改它的 pin ⇒ 另立卡另开 PR,#8303 标 Blocked-by
B 用 stand-in 改写那 34 个 block ⛔ 实测对 27/34 不可行;剩 7 个要手写 React hook 签名。且要改19 个属于本卡不碰的包的文件 —— 正是卡片明令禁止的范围扩张
C 把 34 个 block 声明成 fragment ⛔ 门自陈的最后手段:整个 block 连同该包自己的表面都不再编译 —— 覆盖损失比它掩盖的那个更大;且它是靠把 block 移出编译集来让 pin 闭嘴,不是修好任何东西
D 照现状落地,接受 34 个 block 不被检查,另立后续卡 ⛔ 不可用:pin 无账本 ⇒ CI 长期红,不是安静降级
E 回退 manifest 那一半,只留守卫断言与 changeset ⛔ 把已发布的缺陷放回去,并让新断言变红 —— 等于把 objectui#8303 判成 wontfix

③ 已测量、可据以决策的事实

  • 上面全部 A/B 读数与探针结果,均在两棵对齐的树上取得。
  • ⭐ 门排除 peer 的理由,与 react 在本仓的实际处境不匹配:门的理由是「a peer is a requirement ON the consumer that may be unmet」;⚠️ 但一个没装 React 的消费者,根本用不了这些包里的任何一个 —— React 在一个 React 组件库里不是「可能未满足的可选依赖」。
  • ⚠️ 2026-08-24 对 objectui#6120 的裁定按名否决了「在根 manifest 声明」这条路,⇒ 那条不在选项里。
  • ⚠️ 该 pin 自己的头注(objectui#8059)早已登记过这个形状:refusal「came to name two remedies, one of which reads as impossible on a peer (the package DOES declare it, in a field this map does not read)」。⭐ 本轮把那句「reads as impossible」量成了「对 34 个里的 27 个 IS impossible」。

④ 不决策的后果

objectui#8303 是一个已发布的缺陷(两个包同时向宿主要 React 又自己 pin 一份),修复已完成且正确,但无法落地:它一落地就让 34 个文档 block 失去语义检查,而那条 pin 不接受任何记账形式。⇒ 要么这个已发布缺陷继续留在库里,要么这道门变。没有第三种稳定状态。

四、⛔ 本卡不主张什么

  • ⛔ 不主张 A 的具体形状(「工作区自身能解析」该怎么判、要不要限定包名、pin 怎么放宽)——那是实现细节,裁完再定。
  • ⛔ 不主张这道门整体设计有问题。dependencies-only 的 fail-closed 是刻意的且有道理的;本卡问的只是 react 这一类(消费者不装就完全用不了的 peer)该不该在里面。
  • ⛔ 不主张 objectui#8303 的修复方向有误。那条修复是对的,且它暴露了这个耦合,⛔ 不是造成它。

五、⭐ 一条值得单独记下的方法论

实现席自陈:「我的『改了门/工具脚本就要跑它的测试』这条规则没有触发,因为这次 diff 改的是门所消费的 manifest 数据。」

⇒ ⭐ 一份被某道门消费的 manifest 数据,欠这道门的测试套件,而不只是欠它的可执行文件。

六、相关

objectui#8303(触发本卡的缺陷修复,PR #8911,因本卡而红)· objectui#8059(该 pin 的头注,已登记「one remedy reads as impossible on a peer」)· objectui#6120(2026-08-24 裁定,按名否决根 manifest 声明)· objectui#8300(同样两个 manifest 上的 react-dom peer 约定,仍开着)。

ⓘ 顺带两条,⛔ 不随本卡裁:门的范例本身(packages/layout/README.md "Usage with React Router")就是被拒的 34 个之一 ⇒ 无论怎么裁,那个范例都得重指,否则门的建议自我否定;另 packages/react-runtime/README.md 里 typeof import('react') 这种写法不被 bound 扫描器当作 import,⇒ 同一个 block 会因 import 语法不同而拿到两种判决。

Activity

  1. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    决策分析 —— 总监席第 21 场 · 批 #110 · 2/5(session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T08:2xZ)

    Governing text:scripts/check-doc-snippet-types.mjs 头注(dependencies-only「fails CLOSED, and widening it later is a visible edit with a reason, not a silent drift」);objectui#6120 裁定(2026-08-24,按名否决在根 manifest 声明);objectui#8059 pin 头注(已登记「one remedy reads as impossible on a peer」)。不改协议,改一道门禁的映射规则。

    一句话问题:文档示例的类型检查只认包的 dependencies;两个包在那里"顺手"装了 React,等于一直在替整个文档语料供应 React 的类型。把它们修对(React 应当是 peer——否则消费者树里出现第二份 React,Invalid hook call),34 个示例立刻失去类型检查、CI 长红,一个已发布的缺陷(objectui#8303)因此落不了地。

    选项 × 真实代价(卡面已用两棵对齐树测死 B / C / D / E)

    做什么 客户可感知的后果
    A 教门在 specifier 是工作区自身能解析的 peerDependencies 时也映射它,并同笔放宽那条 pin #8303 的修复落地;34 个示例继续被检查;对真正可选的 peer 削弱了一点 fail-closed
    A′ A 再收窄:只对显式白名单(react, react-dom)放行 同上,削弱面最窄;下一次撞到别的 peer 再来一次
    E 退回 manifest 那一半 已发布的双 React 缺陷继续发布

    业务含义直译:A=「一个 React 组件库的消费者不可能没装 React,把它当可选依赖是门在自欺」;E=「为了门绿,让已发布的缺陷继续发布」。

    四轴:① A 是可见、带理由的放宽,正是门自己预告的演化路径;缩小「React 是可选 peer」这个假特例。② 拉动具名:#8303 修复已完成,PR #8911 两次判词在案、红在这一道上。③ 削弱的只是对真正可选 peer 的 fail-closed;限定「工作区自身能解析」(A′ 再加白名单)把削弱收到最窄;失败方向仍是拒收(TS 报错),不静默。④ 一次门禁改动,不新增机制。

    推荐:A′(A 的形状 + 显式白名单 react / react-dom,白名单在门的头注里带理由)。回退:A。⛔ 不推荐 E。
    置信缺口:本席未复跑 34 个 block 的 A/B;未验证 packages/react-runtime/README.md 里 typeof import('react') 那条扫描器盲区(卡面顺带项);门自己的范例(packages/layout/README.md "Usage with React Router")无论如何要重指。

    裁后执行:devx@objectui pm:queue(改门 + 改 pin + 重指范例 + 头注写理由);#8303 加 Blocked-by: 指向本卡;PR #8911 待门落地后重跑。

    os-decision-facets


    Generated by Claude Code

  2. os-litant commented on Sep 10, 2026

    @os-litant
    Collaborator

    裁决落卡 —— 批 #110 第 2 项:A′ 的意图已由落地形状满足,本卡关闭(总监席第 21 场 session_01QVMnxyWBx8cAQMsV6akDV9,2026-09-10T08:4xZ)

    出处:维护者本场对话「其他同意」(对分析 5615377455 推荐 A′ 的回批)。鲜度门:本卡无新评论;但卡外事实先于回批:PR #8911 已于 2026-09-10T06:47Z 合并(objectui#8303 关闭 completed),其 diff 含 scripts/check-doc-snippet-types.mjs +173/−60 与该门的 pin +166/−25。origin/main 上该文件头注(:327-:344, :1285-:1298)现读:specifier map = dependencies 加上该包自己 peerDependencies 里、且 peerDependenciesMeta 未标 optional 的必需 peer,从该包目录解析。

    裁定:落地形状 = 「必需 peer 才映射,可选 peer 仍 fail-closed」。它比 A′ 的按名白名单更结构化:peerDependenciesMeta.optional 是机器可读的那条线,而 react 恰是必需 peer。⇒ A′ 的意图(削弱只及消费者不可能不装的 peer;失败方向仍是拒收)已被满足,⛔ 不再另开白名单。门自己的范例(packages/layout/README.md "Usage with React Router")是否已重指、typeof import('react') 的扫描盲区,两条卡面顺带项未由本席核,留给 devx 席在下一次触碰该门时顺手看。

    状态:关闭 completed(决定已做且已执行)。同笔修 #8303 的半状态(关闭卡仍带 pm:dispatched + needs-user-decision + assignee)。


    Generated by Claude Code

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    ci/cddomain:devxobjectui devx stream: fix lands on .github/, scripts/ or release pipeline — devx lane cross-repopriority:p2tooling

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions