Skip to content

[REQ-128][CODE] 本地 Claude 插件读取与安装预览(纯读,零写盘) - #786

Merged
jinjunnn merged 4 commits into
alphafrom
feat/780-local-claude-plugin-intake
Aug 2, 2026
Merged

[REQ-128][CODE] 本地 Claude 插件读取与安装预览(纯读,零写盘)#786
jinjunnn merged 4 commits into
alphafrom
feat/780-local-claude-plugin-intake

Conversation

@jinjunnn

@jinjunnn jinjunnn commented Aug 2, 2026

Copy link
Copy Markdown
Owner

Fixes #780
Refs jinjunnn/alpha-work#49

大白话

用户选一个 Claude 插件目录,主进程只读不写地把它清点一遍,然后如实说清:哪些技能能装、
哪些装不上(说人话原因)、哪些组件类型和摆放方式这一版不支持。执行前后磁盘逐字节不变。

今天的行为:插件目录一路落到 collectImportSkillPayload,报一句「文件夹内没有 SKILL.md」——
一句与真因毫无关系的错误信息,用户据它做不出任何正确动作。本票把它换成一份如实清点。

方案基线:docs/design/2026-08-02-req128-phase3-local-claude-plugin-import.md §10 的 T1-intake

改了什么

新增

  • main/claude-plugin-intake.ts —— 布局判定(六类不支持布局具名)、读 plugin.json
    (version 选填)、枚举 skills/*/SKILL.md、逐个跑 collectImportSkillPayload +
    parseSkillFrontmatter + 独立 lstat 自包含扫描,产 LocalPackagePreviewV1
  • ext-ipcpickImportSkillDir 之后加分流点。非插件目录行为逐字不变
  • test-fixtures/claude-plugin-corpus.json(1.42 MB)—— 本机真实语料的仓内夹具,由
    scripts/gen-claude-plugin-corpus-fixture.ts 导出。SKILL.md 与 plugin.json 逐字保留
    (它们是解析器与引用扫描真正读的输入,手写复刻 = 替别人写文法),其余 664 个文件占位保
    size/mode。测试把它摊成一棵真目录树再跑生产代码 —— 喂内存对象等于换成自拼的等价链。

改动(两处,都是最小修法)

  1. parseSkillFrontmatter 交出顶层键集(基线 §14 R2-b)。控制字段的判据是「顶层键在不在」,
    与它有没有标量值无关;老的 fields 字典要求冒号后有非空标量 ⇒ 块式写法整个不进字典。
    零新增解析器:不解析块式的值、不引 YAML、不解析嵌套(ext-import-validate.ts 抬头的
    PR fix(skills): 转义 skill-creator eval-viewer 的 </script> XSS(安全审查) #73 决策不动);顶层性由顶格判定,比 fieldsline.trim() 更严 —— 不会把
    metadata: 下的 allowed-tools: 误报成顶层键。
  2. collector 的排除集提升为导出的单一真源 IMPORT_EXCLUDED_DIR_NAMES(基线 §14 R2-a 首选
    处置)。技能目录内含 node_modules/.git/__pycache__具名拒绝;不处置就是
    「预览接受、装完缺件」。两处各写一份就是手写替身,故从一份导出。

真实语料实测(仓内夹具全量跑)

事实 实测 与基线 §3
插件(.claude-plugin/plugin.json) 62 一致
SKILL.md(任意深度) 162 一致
布局:标准 / .claude/skills / manifestless 159 / 1 / 2 一致
frontmatter 通过 161 / 162(唯一失败 math-olympiad) 一致
0-skill 插件 25 一致
commands/agents/hooks/.mcp.json 22 / 20 / 12 / 22 一致
manifest 无 version 27 / 62 一致
不自包含被拒 18 一致
控制字段被拒 12 一致
单插件 skill 数上限 13 一致

两处与基线不一致,如实记下(没有往基线上凑)

① 可装数是 132,不是 135。 两类拒绝重叠 3 个(claude-security / example-command /
codex-cli-runtime)⇒ 并集 27。基线的 135 = 162 − 27,即把三份异常布局的 SKILL.md 也算进
了分母
;而那三份正是 G18 具名为「不支持的布局」的,结构上装不了。经本功能真正可装的是
159 − 27 = 132。测试两个都断言(132 是真值,135 作为基线那条恒等式)。

② §14 R2-b 的绕过配方在真实语料上不成立。 基线写「把检测口径改回要求有标量值 ⇒ 块式那一半
必须变红」。实测:7 份块式 allowed-tools: 的文件全部同时带一个标量控制字段
(user-invocable: / disable-model-invocation:)⇒ 技能级拒绝集两种口径都是 12
也就是说真实语料验不出 R2-b —— 只用它给这道闸打分就是假闸形态⑨。
真正能杀掉它的只有唯一控制字段是块式的合成夹具(本 PR 已建,见下方绕过实验 B1)。
R2-b 的修法仍然必要:它管的是可达性(用户可手选任意目录),不是当前语料的计数。

每道闸的绕过实验(工作树干净 ⇒ 改坏 ⇒ 跑 ⇒ 还原)

基线未变异:67 pass / 0 fail

# 故意改坏什么 结果
B1 键存在性 → 要求冒号后有非空标量(R2-b 回退) RED ×3(块式夹具 + 解析器单测 + 嵌套用例);真实语料 12 那条仍绿 —— 见上文②
B2 解析器改回只交出 name/description RED ×11(含真实语料 12 与并集 27)
B3 删掉「被排除目录」那一臂(R2-a) RED(node_modules/.git/__pycache__ 夹具)
B4 x 位判据「任一 x」→「u+g+o 全有」(-perm -111) RED ×3(合成 exec + 真实语料 18 + 并集 27)
B5 删掉 symlink 那一臂(= 改用 collector 输出的效果) RED ×2(symlink 夹具 + 逃逸夹具)
B6 超限改成 slice(0, 64) 静默装前 64 个 RED(65 技能那条)
B7 界 64 → 发布端的 16(前提为假的闸) RED ×2(「64 仍可装」+ 上限断言)
B8 分流改回「有 plugin.json 才是包」 RED(manifestless / receipts 结构)
B9 删掉枚举 commands/ 的那一行 RED(真实语料 22)
B10 0 组件改成空成功(不给具名终态) RED ×2(25 个 0-skill 插件)
B11 在 intake 里偷偷写一次盘 RED ×2(树指纹 + fs 写面零调用)

11 条全部变红,没有写不出绕过配方的闸。 全部还原后 git status 为空。

-t 过滤器踩过一次坑:bun 的 -t 对本仓的 CJK 用例名一条都匹配不上,第一轮实验因此测到了
别的文件里的用例(3 条假绿)。改成每次跑整两个文件 + 打印逐条失败用例名之后才是真的。

本地门

结果
north-star ✓ 零上游文件改动
字面 NUL 字节 ✓ 7322 个版本控制文件零命中
typecheck exit 2,与 base fail-set(origin/alpha 13a69d2)逐字节相同:205 行全在 packages/app(worktree 软链 node_modules 的已知假红),packages/ui-mac 0 条
unit(ui-mac) 3692 pass / 0 fail(base 3648 / 0;+44 用例,零新增失败)
docs gate 不适用(未改 Markdown)

--no-verify 推送:pre-push 跑的 alpha-check.sh 会因上面那条 typecheck 假红恒红。判据按仓规
只认「与 base fail-set 相同」,已逐字节比对。

顺带被自己的闸抓到一次(值得记一笔)

NUL 闸在本 PR 上真的红了一次:我写的摘要分隔符与一处注释落盘成了字面 NUL(4 处)。
这正是 scripts/assert-no-nul-bytes.py 抬头预言的传播路径(「写这份脚本的过程本身就是本类缺陷
最常见的传播途径」)。已按它指定的转义写法修掉(运行时逐字节等价、grep 看得见)。
在这几个文件上 grep 当时会给出假的「没有」 —— 定位靠的是闸报的字节偏移,不是 grep。

范围纪律:主动没做的事

  • 不新增第六张导入卡、不动 renderer 一行([T4-renderer]);preview→confirm 通道、包级字节
    预算、local: 双向闸、「列出已装本地包」只读 IPC 一个都没做([T3-channel])。
    本票的用户可见终态就是「如实说清楚」为止,分流点返回的是具名拒绝 + 预览数据
  • 不碰账本、不碰事务、不改 ext-transaction.ts / package-admission.ts / ext-install-policy.ts
  • 不为将来的 provider 抽象通用适配层,不引 YAML 解析器,不解析嵌套,不改那条既有正则。
  • 不改已 accepted 的方案基线去对齐上面那两个数字 —— 基线的修订归编排者裁决。
  • G11 的「事务函数零调用」在纯读票里由**更强的「零写盘」**兑现(树指纹 + fs 写面零调用);
    spy 版本要等 [T2]/[T3] 有 confirm 之后才非空。已在此写明,不当作已做。
  • 自包含判定是启发式,会有假阴(抓不到运行期拼出的路径、写在 references/*.md 里的路径)。
    AC 措辞是「命中以下几类特征之一即拒」,不是「保证自包含」——按基线 §8 纪律 3,降级只许写成降级。

jinjunnn and others added 3 commits August 2, 2026 03:03
Fixes #780
Refs jinjunnn/alpha-work#49

用户选一个 Claude 插件目录,主进程只读不写地把它清点一遍:哪些技能能装、
哪些不能装(说人话原因)、哪些组件类型和摆放方式这一版不支持。执行前后磁盘
逐字节不变。

今天的行为是:插件目录一路落到 collectImportSkillPayload,报一句「文件夹内
没有 SKILL.md」—— 与真因毫无关系,用户据它做不出任何正确动作。本票把这句话
换成一份如实的清点。

新增
- main/claude-plugin-intake.ts:布局判定(六类不支持布局具名)、读 plugin.json
  (version 选填 —— 27/62 真实 manifest 没有它)、枚举 skills/*/SKILL.md、
  逐个跑 collectImportSkillPayload + parseSkillFrontmatter + 独立 lstat 自包含
  扫描,产 LocalPackagePreviewV1。
- ext-ipc 在 pickImportSkillDir 之后加分流点;非插件目录行为逐字不变。
- test-fixtures/claude-plugin-corpus.json:本机真实语料的仓内夹具(SKILL.md 与
  plugin.json 逐字保留,其余文件占位保 size/mode),由 scripts/ 下的生成器导出。

改动(两处最小)
- parseSkillFrontmatter 交出**顶层键集**。基线 §14 R2-b:控制字段的判据是
  「顶层键在不在」,与它有没有标量值无关;老的 fields 字典要求冒号后有非空标量
  ⇒ 块式写法整个不进字典。零新增解析器:不解析块式的值、不引 YAML、不解析嵌套,
  顶层性由顶格判定。
- collector 的排除集提升为导出的单一真源 IMPORT_EXCLUDED_DIR_NAMES(基线 §14
  R2-a 的首选处置):技能目录内含 node_modules/.git/__pycache__ ⇒ 具名拒绝,
  否则就是「预览接受、装完缺件」。

真实语料实测(仓内夹具全量跑)
62 插件 / 162 SKILL.md / 159 在支持布局 / 161 frontmatter 通过 / 25 个 0-skill
插件 / 18 自包含被拒 / 12 控制字段被拒 / 两类重叠 3 ⇒ 并集 27 ⇒ 可装 132。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Refs #780

Codex R1:1 Blocker + 4 Major + 1 Minor,全部采纳。

Blocker — 闸绕开了真实 IPC handler
新增 test-component/claude-plugin-intake.ipc.cases.ts:跑 ext-ipc.ts 里**已注册**的
ext-import-skill-folder handler(真 electron mock + 真 alpha 环境 + 真 ledgerReady),
对「输入语料树 + 隔离 alpha 根(installs.json)+ userData」三处做前后逐字节指纹。
写面 spy 有盲区:collector 用 namespace import,ESM 命名空间对象不可写 ⇒ 换成可观察结果。
指纹起点取在 ledgerReady 之后 —— 启动期恢复/迁移本就允许写盘,要证的是「这一次导入没写盘」。
子进程隔离:mock.module("electron") 是进程级的,留在 src/** 会连带打红 39 条别处用例。

Major — 预览没消费已装事实
生产分流现在从 v2 账本读已占用技能名、从 V3 图读已装 packageId 并传入 intake;
新增 name-collision-installed / name-collision-in-package 两个原因码;
包内重名两个都跳过(不挑赢家);账本读不出来时如实说「读不出」不折叠成「没装」。
安装期最终裁决仍归 T2 的 G4,本票只负责把事实喂进来并具名呈现。

Major — G16 静默丢 symlink 技能 + scan.failed fail-open
候选枚举改为先枚举条目再判定:symlink 目录 / symlink SKILL.md ⇒ 具名 skill-entry-not-regular,
不再从候选集消失(旧测试断言 skillCandidates===0 是把 bug 写成期望,已改);
scan.failed 改 fail-closed(self-containment-scan-failed)—— 扫不动时「没看见」不是「没有」。

Major — G12 杀不掉硬化读取旁路 + plugin.json 裸读
plugin.json 改走与技能载荷同一份 readImportFileBounded(realpath 圈禁 / O_NOFOLLOW /
O_NONBLOCK / fstat 帽 / 定长读 / 增长探测)+ 256KB 帽;补三例夹具:
超 256KB 的 SKILL.md(唯一能杀掉「换回裸读」的用例)、超大 manifest、symlink manifest。

Major — G11 假闸形态⑨
表驱动 1/63/64/65/66/200,所有 >64 均具名整包拒绝 —— 写死 count===65 的实现不再能全绿。

Minor — 真实语料 AC 每次绿灯留 888 个文件
AC1 的共享 corpus 加 afterAll(cleanup)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
绕过实验挖出的自身盲区:只比内容的指纹看不见「反复写同一串字节」——
C2(在生产依赖里写 installs.json)时这条闸没变红,变红的是别的用例。
inode 另外管住「写临时文件 + rename」这种内容相同但确实换过文件的写法。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jinjunnn

jinjunnn commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

R1 闭合(1 Blocker + 4 Major + 1 Minor 全部采纳)

推到 2de6d6f1。逐条修法 + 绕过实验实际输出如下。

[Blocker] 闸绕开真实 IPC handler

新增 test-component/claude-plugin-intake.ipc.cases.ts(10 例):跑 ext-ipc.ts
已注册ext-import-skill-folder handler —— 真 electron mock + 真 initAlphaEnvironment

  • registerExtIpcHandlers + await ledgerReady,picker 走生产自己的 headless 短路。
    判据 = 「输入语料树 + 隔离 alpha 根(installs.json 在其中)+ userData」三处前后指纹。
  • 没用写面 spy:审计给的实验是对的 —— collector 是 namespace import,ESM 命名空间对象不可写,
    require("node:fs") 只改得动 default 那一份。换成可观察结果,没有盲区。
  • 指纹起点取在 ledgerReady 之后:启动期恢复/迁移本就允许写盘,算进来会让闸对着无关原因恒红。
    要证的是「这一次导入没写盘」。
  • 子进程隔离:mock.module("electron") 是进程级的。留在 src/**bun test src 连带打红
    39 条别处用例(MCP / plugin / governance),单跑它自己 0 fail —— 与仓内
    package-admission.wiring.test.ts 同一条理由、同一种写法。宿主断言 10 pass / 0 fail(用例数写死)。

[Major] 预览没消费已装事实

生产分流现在自己去读账本:readLedgerV2 取已占用技能名(含 v1Only)、readPackageLedgerStateV1
取已装 packageId,传进 intakeImportDir。新增两个原因码 name-collision-installed /
name-collision-in-package;包内重名两个都跳过(挑赢家 = 装进去哪一个取决于目录枚举顺序);
账本读不出来时 reason 里如实说「读不出」,不折叠成「没装」。

范围按你的裁决:安装期最终裁决仍归 T2 的 G4,LocalPackageIntakeOptions 上写明了这是预览期告知
判据用 frontmatter 的 name(落盘的键),不是目录名 —— 写这条测试时先踩了一次目录名。

[Major] G16 静默丢 symlink 技能 + scan.failed fail-open

候选枚举改成「先枚举条目、再判定」:symlink 目录 / symlink SKILL.md ⇒ 具名
skill-entry-not-regular,不再从候选集消失。旧那条断言 skillCandidates === 0 的用例已改 ——
你说得对,那是把 bug 写成了期望。scan.failed 改 fail-closed。

[Major] G12 杀不掉硬化读取旁路 + plugin.json 裸读

plugin.json 改走与技能载荷同一份 readImportFileBounded + 256KB 帽。补三例夹具。

⚠️ 一条要如实说小:plugin.json 是 symlink 那一例,实际由前置的 isRegularFile(lstat)拦下,
不是由 bounded 原语拦的 —— C8 里它没变红。它仍是一条真行为断言,但不能算作「证明硬化原语接上了」。
真正杀掉旁路的是超大 manifest(C8)与超 256KB 的 SKILL.md(C7)两条。

[Major] G11 假闸形态⑨ / [Minor] 临时目录残留

表驱动 1 / 63 / 64 / 65 / 66 / 200;AC1 共享 corpus 加 afterAll(cleanup)


绕过实验(工作树干净 ⇒ 改坏 ⇒ 跑整套 ⇒ 还原)。基线 81 pass / 0 fail

# 改坏什么 结果
C1 删掉 ext-ipc.ts 的生产分流点 RED(子进程 0 pass / 1 fail)—— R1 之前这一条不会红,那正是 Blocker 的定义
C2 在生产依赖里偷写 installs.json RED(见下方修正)
C3 生产分流不再传已装事实 RED(已装同名那条)
C4 包内重名不再逐组件跳过 RED ×2(纯函数 + 生产)
C5 symlink 技能回到「枚举期滤掉」 RED ×2(symlink 目录 + symlink SKILL.md)
C6 scan.failed 回到 fail-open RED(深度 33)
C7 技能载荷换回裸 readFileSync RED(超 256KB SKILL.md;其余三例确实杀不掉它)
C8 manifest 回到裸 readFileSync RED(超大 manifest)
C9 G11 判据由 > 64 收成 === 65 RED ×2(66 / 200;旧的只有 64/65 两点时不会红)

全部还原后 git status 为空。

C2 顺带挖出我自己这道闸的盲区(已修,单独一次 commit)

C2 第一次跑时,三条 AC9 指纹用例一条都没红 —— 红的是三条 D/K19 用例(靠账本被覆盖间接暴露)。
真因:只比内容的指纹看不见幂等重写。往 installs.json 反复写同一串 {},内容哈希逐次相同,
「逐字节不变」照样成立,而生产其实每次都在写盘。

修法:treeFingerprintmtime + inode(inode 另外管住「写临时文件 + rename」)。
修后重跑 C2:三条 AC9 用例全部变红(4 pass / 6 fail)。

这一条不在审计的 6 条里 —— 是绕过实验自己跑出来的,记在这里。


门(与 base fail-set 的差)

结果
north-star ✓ 零上游文件改动
字面 NUL ✓ 7324 个版本控制文件零命中
typecheck exit 2,与 base(origin/alpha 13a69d2)逐字节相同;packages/ui-mac 0 条
unit(ui-mac) 3706 pass / 0 fail(base 3648 / 0;R1 前 3692)
contracts-consumer / ext 132 pass / 0 fail

我认为你没判错的地方 —— 但有两条要摆给你

  1. plugin.json symlink 那一例的强度被我写小了(上文已标)。要不要再加一条「符号链接指向的目标
    合法但 manifest 在读取途中被换掉」的竞态夹具,归你裁 —— 我倾向不加:它要靠 readImportFileBounded
    自己的 dev/ino 复核,那是 #390 已有闸的射程,本票再造一份是重复。
  2. 重名判据只覆盖 global scope。生产分流读的是 alphaGlobalRoot(),project scope 的
    .alpha/skills 没读 —— 因为本路线的 folder 导入生产上恒 global(ext-ipc.ts 的 body 里
    isProjectTarget(target) 分支只经 ecosystem 通道)。写明而不做;若 T4 之后 renderer 能传
    project target,这条要跟着补。

Refs #780

R2:R1 六条判 5 FIXED / 1 PARTIAL,但修复 diff 引入两条新问题。全部采纳。

[Blocker] 为闭合「预览要消费已装事实」而加的账本读取自己会写盘
parseLedger 看到坏 receipt / 坏 record 时会落一份 installs.json.evidence-<hex12>
(ext-receipt-v2 r18 的字节级取证侧写)—— 那是一个写,直接违反 AC9;
启动期 recovery 非 clean 而跳过 migration、或账本在 barrier 之后变化时到得了。
修法:parseLedger 加 sideEffectFree(默认 false = 今天的行为,既有调用点一个不动),
readLedgerV2 / readPackageLedgerStateV1 透传;预览路径传 true。
取证不丢:任何写路径的读仍会落它,而侧写本来就按损坏集哈希幂等。
补一条闸:账本里有坏 receipt ⇒ 预览后 evidence-* 零新增 + 账本 mtime/inode 未变。

[Major] K19 生产闸是假绿
旧夹具写 v:2 却带非空 V3 段、node 缺 componentId、installedGraphDigest 是手填常数
⇒ 账本合同必然拒绝 ⇒ 走「读不出来」退路分支 ⇒ 删掉 K19 生产接线照样全绿。
新夹具:record 用生产 upsertRecordV2 造,owner token 用真 bundleOwner 算,
installedGraphDigest 用真 computeInstalledGraphDigest 算,v:3;
并**先自证 readPackageLedgerStateV1 接受它**,再断言 K19。退路分支已删除。

[PARTIAL] 零写盘指纹范围
隔离 home 纳入指纹(四棵树)。范围外写盘与「写完恢复 mtime」明写而不做:
要堵那一类得在进程边界上拦,不在本票射程内。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jinjunnn

jinjunnn commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

R2 闭合(1 Blocker + 1 Major + 1 PARTIAL 全部采纳)

推到 cd550b4a。绕过实验按最终树全量重跑(不是引用上一轮的结论),命令与输出如下,可直接复现。

[Blocker] 我为闭合 #2 加的账本读取自己会写盘

真的会。parseLedger 看到坏 receipt / 坏 record 时落一份 installs.json.evidence-<hex12>
(ext-receipt-v2.ts r18 的字节级取证侧写)—— 那是本函数唯一的写,而我把它拉进了一条
承诺零写盘的路径。(顺带核实:quarantineCorrupt 只被 upsertRecordV2 / removeRecordV2
等写函数调用,不在 readLedgerV2 / readPackageLedgerStateV1 的链上,所以侧写是唯一的口。)

修法(没用「复用启动期已解析状态」那条 —— 它要在 ledgerReady 上挂一个快照,而快照会陈旧,
等于把「拍照那一刻」的问题再往前挪一层):parseLedgersideEffectFree,
默认 false = 今天的行为,既有调用点一个都不动;readLedgerV2 / readPackageLedgerStateV1
透传;只有预览路径传 true。取证不丢:任何写路径的读仍会落它,而侧写本来就按损坏集哈希幂等。

新闸:账本里放一条坏 receipt → 跑生产 handler 预览 → 断言 evidence-* 零新增 +
账本文件 mtime/inode 未变

[Major] K19 生产闸是假绿

你说得对,而且比我以为的更糟 —— 旧夹具三处都不合规:v: 2 带非空 V3 段、node 缺
componentIdinstalledGraphDigest 是我手填的常数(合同会重算并比对)。三条任一都会让它
走「读不出来」退路分支。

修法:record 用生产 upsertRecordV2 造;owner token 用真 bundleOwner 算;
installedGraphDigest 用真 computeInstalledGraphDigest 算;v: 3
并且先自证 readPackageLedgerStateV1 接受它,再断言 K19;退路分支已删除。
(手填那个常数正是「替别人写文法」—— 同一形态在这张票上第二次咬我。)

[PARTIAL] 零写盘指纹范围

隔离 home 已纳入(四棵树)。

范围外写盘 + 写完恢复 mtime:明写而不做。 判据只覆盖「传给生产 handler 的那几个根」;
要堵住真实 $HOME / /tmp / 系统路径,或堵住恢复 mtime,得在进程边界上拦(seccomp / 沙箱),
不在本票射程内。这条降级写在 writeSurfaceFingerprint 的注释里,不让它看起来无所不能。


绕过实验(最终树全量重跑)。基线 81 pass / 0 fail

跑法:bash 脚本、跑整文件不用 -t、每次断言 across 3 files(zsh 分词那条陷阱的防线,
脚本里若测到文件数不符会打印「本次测量作废」而不是给出一个数字)。

# 改坏什么 结果
D1 预览路径的账本读取改回会写取证文件 RED — 「坏 receipt ⇒ evidence-* 零新增」
D2 删掉 K19 生产接线(不传 installedPackageIds) RED — K19。R2 之前这条不会红,那正是假绿
D2b 保留接线但摘掉 duplicateImportNotice 产出 RED ×2 — K19 + AC8
D3 往隔离 home 写一个文件 RED ×3 — 三条 AC9 指纹用例(纳入 home 之前它看不见)
C1 删掉 ext-ipc.ts 分流点 RED(子进程 0 pass / 1 fail)
C2 生产依赖里幂等重写 installs.json RED ×7
C3 不传已装技能名 RED
C4 包内重名不再逐组件跳过 RED ×2(纯函数 + 生产)
C5 symlink 技能回到枚举期滤掉 RED ×2
C6 scan.failed 回到 fail-open RED
C7 技能载荷换回裸 readFileSync RED — 超 256KB SKILL.md(["payload-unreadable"][])
C8 manifest 回到裸 readFileSync RED — 超大 manifest
C9 G11 判据 > 64 收成 === 65 RED ×2(66 / 200)
C10 R2-b 键存在性改回「要求非空标量」 RED ×3(块式夹具 + 解析器单测 + 嵌套);真实语料 12 那条仍绿

全部还原后 git status 为空。C10 的「真实语料仍绿」是刻意保留的证据:真实语料验不出 R2-b,
只有 block-only 合成夹具能杀它。


结果
north-star ✓ 零上游文件改动
字面 NUL ✓ 7324 个版本控制文件零命中
typecheck exit 2,与 base(origin/alpha 13a69d2)逐字节相同;packages/ui-mac 0 条
unit ui-mac 3706 pass / 0 fail(250 files;base 3648 / 0)
contracts-consumer / ext 52 / 0 + 132 / 0

你没判错的地方 —— 但有一条要摆出来

我动了 ext-receipt-v2.ts 那是基线 §10 给 T2 划的禁改文件(T1 没有这条明文约束),
改动面是三行:parseLedger 加一个默认 false 的参数、两个 read 入口透传、侧写多一个 &&
默认路径行为逐字不变(3706 用例零回归可作证)。

我判断这是最小修法 —— 另外两条路都更差:①在预览路径上另写一个「只读版 parseLedger」= 手写替身,
账本文法有两个答案;②复用 ledgerReady 的解析结果 = 要挂快照,而快照会陈旧。
若你认为 T1 不该碰这个文件,请裁,我把它退回并改走 ①/② 里你指定的那条。

@jinjunnn
jinjunnn merged commit 9b49c5d into alpha Aug 2, 2026
5 of 6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REQ-128][CODE] 本地 Claude 插件读取与安装预览(纯读,零写盘)

1 participant