[REQ-128][CODE] renderer 半场:分流 + 预览屏 + 已装扩展包区块(含启用开关)+ 移除 + 热重载接线 - #791
Conversation
ba6856e to
b17b46f
Compare
R1 五条 + owner 的显示名裁决,全部落地base 已从 owner 裁决:包显示名
Codex R1 五条(全部采纳,无一超纲)
顺带修掉一条夹具假绿
绕过实验(第二轮,12 条)全部实际跑过,记录在 本地门(真实输出)
🤖 Generated with Claude Code |
R2 Major 闭合:显示名不再能让安装失败先复现,不推断。修复前实测: 129 个 两条约束逐条落实
没有选截断 —— 截断会把作者写的名字改写成我们编的名字,而这个字段本来就是可选的: 三道闸的绕过实际输出绿基线 22/0(renderer 竖线)+ 52/0(真实语料 intake):
真实语料零误伤(反向检查):62 个插件全部拿得到显示名、没有一条告知、 一件我自己的错,连带修掉「真实语料零误伤」这条闸的第一版绕过配方(收到 30 字)没有变红。 处置:注释改成实测值并写明是实测;把这个数钉进测试(最长 27 且帽 > 27); 前三次没变红是配方改错了地方;这一次不同 —— 配方是照着一句假事实设计的。 本地门(真实输出)
复现脚本已删,场景由用例常驻覆盖:夹具 🤖 Generated with Claude Code |
第 1→9 跳里 renderer 拥有的那一半。在这一提交之前,main 侧全部铺好而**用户点不动**: preload 一条新通道都没暴露,`importSkillFolder` 的插件目录判别臂在 `use-extensions` 里被 折成一句「导入失败」,confirm 端口是 fail-closed 占位,已装的包在已安装页结构上看不见。 - 分流:`runImportFolder` 按 main 给的 `route` 走(renderer 不判目录形态、不传路径,`#255`)。 非插件目录逐字不变,回归用例钉死。 - 预览屏:逐条呈现装/不装/五类人话原因/不支持的组件类型/不支持的布局;重复导入、 超预算、一个都装不上三种终态各有具名呈现;确认键在无可装项时保留但禁用。 - 已装扩展包区块:每个技能一个开关(裁决 B 之下不给开关 = 用户装完拿不到任何东西), 包头挂整包移除;「账本读不出来」与「没装」永不折叠。 - 第 8 跳:包内单个技能被单独移除 ⇒ 行上明确拒绝 + 指向整包移除,不是假成功。 - G20 热重载接线:confirm 成功与整包卸载成功都走**既有** `refreshEngine()`,失败呈现待重载。 两条都收口在 `use-extensions` 一层 —— 逐调用点接一行的形态在本仓已栽三次。 - confirm 端口接到 `installLocalClaudePluginV1`,喂的是 **preview 期留下的字节** (不调 `collectLocalPackagePayloadsV1` 重采集:那会让 G19 变成一段写着闸的注释, 并重新引入 K15 否决的 confirm 期重扫)。 Fixes #784
绕过记录里诚实收了两条**第一轮没有变红**的配方,并写清它们为什么不算闸失效: 判据不是「我改了一行」,是「用户可观察的行为真的变了吗」。 Refs #784
owner 裁决:`PackageGraphV1` 加**可选** `displayName`,安装时存 `plugin.json` 自己声明的
`name`。存量图缺字段 ⇒ 回退 root 组件名,**绝不因为缺字段就拒载**。来源判定不变:
「是不是策展来的」仍然只有 child record 的 `origin` 一个答案,新字段只管显示。
摘要口径**在场才计入** —— 无字段图的 canonical JSON 与加字段之前逐字节相同,存量图的
digest 照旧过篡改闸(开发中真实红过一次:decoder 校验了却没把字段带出去 ⇒ 合法图被拒载)。
Codex R1 五条全部采纳:
- **[Blocker] 第 9 跳只读了允许集文件,没跑引擎的读。** 改成执行**生产 `AlphaExt` 的 config
hook**,断言 `cfg.skills.paths` 里只出现已开启技能的 live generation 目录;移除后归零。
读「引擎会读的那个文件」≠ 跑「引擎的读」。
- **[Major] SDK 的 HTTP 错误被当成 dispose 成功。** v2 client 默认 `throwOnError:false`,
503 会正常 resolve 成 `{error}`。`refreshEngine` 改为复用**既有**的 `connectOutcome`;
夹具补上「resolve 成 {error}」这条真实形状(旧夹具只会 throw)。
- **[Major] 非插件目录只覆盖了成功路径。** 补一条**导入失败**的非插件目录用例:
保持原行内错误、不进插件预览。`isLocalPluginRoute` 写成 `!result.ok` 于是杀得掉了。
- **[Major] 冷启动账本损坏会同时显示「读不出」和「尚未安装」。** 通用空态在 package
投影读不出时抑制;用例排在全文件**第一条**,那时 receipts 才真的是空的。
- **[Major] 整包移除后「留下了什么」随包卡一起消失。** retained 提到区块级结果上,
用两个共享同一 child 的包断言:卡消失后原因仍在。
顺带修掉一条**夹具假绿**:`waitFor` 不 await 断言 ⇒ 传 async 断言进去时它永远立刻成功。
Refs #784
D4 的第一版配方(条件展开改无条件)**没有变红** —— 真因是 `canonicalJson` 本来就丢弃
`undefined` 键,那个条件展开只是冗余防御。处置不是删断言,是把闸挪到前提本身
(`canonicalJson({a,b:undefined})` 必须等于 `canonicalJson({a})`),并把绕过配方换成
「给缺席值填占位」—— 那才是会让全部存量图集体拒载的改法,它当场变红。
这是同一形态第三次:**绕过配方本身也会是假的**。
Refs #784
复现(修复前,实测):插件名 129 个 `a` ⇒ `readManifest` 只要求非空、`mintPackageId` 把它 截成合法的 128 字符 ID ⇒ **照样进 installable 预览**;安装器把**原始 129 字符**写进 `displayName` ⇒ decoder 在 **receipt commit 那一刻**拒绝 ⇒ `tx-rolled-back`。 含控制字符的名字同理。一个**只管显示、而且明确可选**的字段,反向破坏了安装路径。 两条约束逐条落实: - **唯一真源**:`isValidPackageDisplayName` 出在 `ext-package-ledger-v3.ts`, decoder 与 intake **都消费它**,谁都不再抄一份。此前 intake 一份(只要求非空)、 decoder 一份(长度 + 控制字符),两份不一致就是这条缺陷的全部起因。 - **判定发生在预览期**:`LocalPackagePreviewV1` 新增 `displayName` / `displayNameNotice`, intake 定案,安装器**只透传**、不再从 `preview.name` 派生(派生就是第二份文法)。 名字存不下 ⇒ 预览期具名告知「装好后列表里会用它第一个技能的名字」,**安装照常成功**, 账本里那张图**根本没有** displayName 字段(缺席合法,不是存了个截断值)。 不偷偷截断:截断会把作者写的名字改写成我们编的名字。 - 安装器另加一条**事务前**的具名拒绝作兜底:万一有人绕过 intake 直接构造 preview, 失败也发生在动任何东西之前,而不是 commit 期回滚。 反向检查(「前提为假的闸门」的另一面):真实语料 62 个插件**零误伤** —— 每个都拿得到显示名、没有一条告知、显示名与 manifest 原样相等,可装总数仍是 132。 Refs #784
M3 的第一版配方(判据收到 30 字)**没有变红** —— 而这一次不是配方改错了地方,是 **配方照着一句假事实设计的**:我在注释里写「本机语料最长 31」,实测是 **27** (`claude-for-msft-365-install`)。那句话当时还被我用来论证「128 够长」。 处置:注释改成实测值并写明是实测;把这个数**钉进测试**(最长 27 且帽 > 27); 绕过配方换成 26 与 20,两条当场变红。 四次同一形态,前三次是配方错、这一次是**前提假**。能执行就别推断。 Refs #784
a8da23d to
aadb83e
Compare
Fixes #784
Refs #653
大白话
在这条 PR 之前,main 侧三票(
#780/#781/#782)已经全部铺好,而用户点不动任何东西:preload 一条新通道都没暴露、
importSkillFolder的「这是插件目录」判别臂在use-extensions里被折成一句「导入失败」、confirm 端口还是 fail-closed 占位、已装的扩展包在已安装页结构上
看不见。这一票补的就是用户真正看得见摸得着的那半场。
用户路径:点「导入 → 文件夹」挑一个 Claude 插件目录 → 弹一屏预览,逐条说清装什么、不装什么
以及为什么 → 确认 → 已安装页多出一张扩展包卡,里面的技能默认全关,卡上一行字说明「打开
开关才会生效」→ 打开任意一个,引擎当场重载,下一条消息里就能用 → 「移除此扩展包」一键连
文件带记录一起拿掉,引擎同样当场重载。
这一票为什么按「整条竖线」验,而不是按层验
Phase 1 漏掉 renderer 半场 ⇒ 全部票交付、全部过审计,最后才发现用户点不动;
Phase 2 漏掉三条跨仓竖线 ⇒ 同一形态高一层。按层拆票时,断掉的那一截不在任何一张票的边界内,
于是每一张票都能诚实地绿。
所以本票的主闸是一个把九跳跑完的端到端用例文件
(
test-component/local-package-renderer.cases.ts,15 条),里面只有一样东西是替身:引擎的 HTTP 客户端(
POST /global/dispose在无头进程里没有对端)——那是进程边界,而它恰好就是 G20 的断言对象。其余全是生产代码:真
ExtensionHub/ExtensionDetail、真
registerExtIpcHandlers、真claude-plugin-intake、真collectImportSkillPayload、真
local-package-preview、真claude-plugin-install、真runExtensionTransaction、真 V3 账本。第 9 跳「下一条消息里技能真可用」的判据 =
skills-enabled.json——引擎注入门(
packages/ext/src/gen-skill-paths.ts)实际读的那个文件,不是「我查了账本」。改了什么
runImportFolder按 main 给的route走。renderer 不判目录形态、也拿不到路径(
#255)。非插件目录逐字不变,回归用例钉死。原因文案全部来自 main 的
reasonCode → 人话表,renderer 一个字都不自己编、不折叠。重复导入、超预算、一个都装不上三种终态各有具名呈现;无可装项时确认键保留但禁用,
原因写在键面上。
local-package-install-port转调installLocalClaudePluginV1,喂的是 preview 期已经留在 main 的字节。刻意不调
collectLocalPackagePayloadsV1重新采集——那会让 G19 从一道闸变成一段写着「闸」的注释,并重新引入 K15 明令否决的
confirm 期重扫(第二个 TOCTOU 窗口 + 第二份摘要文法)。
拿不到任何东西),包头挂整包移除;开关走既有
runStateToggle,与平铺行同一条通道、同一个账本、同一次重载——不另造第二套「包内开关」。
判据是「main 拒了 且 这一条在某个包图里」,不解析 reason 的文字。
refreshEngine(),失败如实呈现「待重载」。两条都收口在
use-extensions一层——逐调用点接一行的形态在本仓已栽三次(
#765是同一个故事)。详情页的整包移除也改走这一层。每道闸都实施过一次绕过
12 条配方与结果全在
docs/verification/2026-08-02-req128-t4-gate-bypass-experiments.md。G20 的基线记录(票面 AC2 要求):把生产代码改回本票之前的原样
(整包移除只
refetchInstalled()不 refresh / 详情页直连 IPC),两条闸当场变红——也就是说 G20 钉的是一条这一票才接上的线,不是一段已经成立的行为。
记录里还诚实收了两条第一轮没有变红的配方,并写清它们为什么不算闸失效:
判据不是「我改了一行」,是「用户可观察的行为真的变了吗」。
本地门(真实输出)
ui-mac:3747 pass / 0 fail / 254 files。base(
feat/782-t3-preview-channel@e4004efa,同机新建 worktree 实测):3746 pass / 0 fail / 253 files。base fail-set = 空集,本 PR fail-set = 空集;
差 = +1 个测试文件 / +1 条用例(本票的九跳 runner)。
设计稿:按基线改了三处(稿未批,AC 级行为以已批基线为准)
rootComponentName(root 组件名),不是插件名。 稿里画的是tide。账本里没有「包显示名」这个事实——造一个不诚实;从
local:tide反推等于从 id 里读结构(
#737明令禁止)。代价:用户导入tide,卡上看到的是postmarket-review。这是一个真实的产品瑕疵,需要 owner 裁决要不要给 V3 图加一个显示名字段(那是 schema 改动,
不在本票范围)。
install-in-flight,用户必须有一条出路才能读到那句真话;禁掉按钮等于把真话藏起来,用户只会以为界面卡死。拒绝写在屏内、弹窗留在原地——绝不先关弹窗再飞一条 toast
(那样用户读到的第一件事就是「它取消了」)。
来自官方目录」两枚标区分,来源从 child record 的
origin读,不从 packageId 前缀读。稿的 Q1(包内技能照常出现在平铺列表 + 一枚「属于扩展包 X」标)、Q3(确认键保留但禁用)、
Q5(确认成功后自动切到已安装页)按稿实现——Q5 尤其对着裁决 B:不跳页的结果就是用户按完确认
什么都看不到,然后认为功能坏了。
顺带修好的一个既有缺陷
从目录装的扩展包,移除之后那些技能其实一直能用到下次重启
(
extension-detail.tsx的removePackage只refetchInstalled()、不refreshEngine())。本票把它收进同一条通道,一并修好。
主动没做的事(超出票范围,记下来不顺手改)
已安装页的 scope 分组一寸未动。
#783)的验证矩阵与真机 L2:本票只做到无头进程里可判的最强形式(
skills-enabled.json)。真机「下一条消息里技能真被引擎注入」归 T5。docs/design/current/customization-center/components.md的落地列:那份台账在设计稿分支(PR [#784][DESIGN] 本地插件包导入并入活稿:三块帧按上线实况画 + 台账落地列回填 #785)上、稿未批,回填是 design-loop 批准后并入活稿那一步的事。
local:包做「更新」:重导入前必须先整包移除(K19),预览屏在确认之前说清。🤖 Generated with Claude Code