Skip to content

Story 3.4:裁定并落地 sourceRef 的跨机器可移植性语义 - #177

Merged
zaurakworks merged 1 commit into
mainfrom
zaurakworks/story-3-4-skill-supply
Aug 25, 2026
Merged

zaurakworks merged 1 commit into
mainfrom
zaurakworks/story-3-4-skill-supply

Conversation

@zaurakworks

Copy link
Copy Markdown
Owner

Epic 3 剩余「供应」半边的第一个 Story。关联 #173。

为什么先做这一条

AD-22 退役第 (1) 步把「必须一并裁定 sourceRef 的跨机器可移植性语义」列为显式开放项,并把「两个实现者各自发明」判为 critical——一方产出的修订在另一方手里会按 AD-10 静默全量降级,且没有任何门能指出根因。这条 Story 只做裁定与解析侧;供给命令本体是 Story 3.5。

裁定

sourceRef 只有一种合法形态:库内相对 POSIX 路径。绝对路径一律拒绝。根是本机配置,不进修订,机器差异只由根承担。

五条判定规则(validateSupplyRelativeRef,全仓唯一实现,产出侧与解析侧共用):

非空 → 不含反斜杠 → 不带盘符前缀 → 非绝对 → 解析后严格落在根内

盘符那条刻意排在 isAbsolute 之前,使判定与诊断都平台无关。

规则③④是实测逼出来的,不是防御性编程

输入 实测行为 后果
known('') / known('.') path.resolve 返回根本身 cp --recursive 把整个供给库当作一个 Skill 拷入并报告启动成功——AD-10 的 fail-closed 被翻成 fail-open
C:x/y(与根同盘) win32 折进根内,被接受 且此时收敛判定依赖 process.cwd()
C:/x/y POSIX 上 isAbsolute 为假 被当普通相对名接受——同一条修订两个平台含义不同

第一条由 verification-gap 层实测复现完整因果链(failures: []、写出 plugin.json、--plugin-dir 照常交给 spawn 出去的进程)。

其他关键决定

根按调用快照一次,在 materializeClaudeContent 顶部解析后向下传。三组是 Promise.all 并发,在引用处重读会让同一条修订劈成两个根。

cap-fs 的隐含根取 repoRoot 而非 capRoot——spec 的 Code Map 原写 capRoot,是 Code Map 错了:lock.json 的 project_skill_imports[].source 为 plugins/grilling/skills/grilling,位于 capRoot 之外,相对 capRoot 会得到 ../plugins/... 而被本合同拒绝。产出侧先校验后 join(先 join 会把 /abs/x 吞掉),不合法即降级为既有的 Unknown。

审查:三层 × 两轮,一次 intent_gap 回退

第一轮三层独立收敛到同一批问题,其中最要命的根因在 spec 冻结区——我写的「POSIX /x/y 原样返回」在 win32 上是事实错误(实测 path.resolve('D:\lib','/x/y') → D:\x\y,盘符取自根)。那种错误改实现改不掉,按 workflow 判为 intent_gap,回滚代码、重新谈定合同后再推导。

第二轮由真变异证明了两处「有测试但是空的」:

变异 结果
根改回每引用重读 env 502 全绿 → AC3 从未被验证
失败信息缩成只有名字 502 全绿 → 本 Story 的核心交付物(含值与根的诊断)在用户可见面零覆盖
反斜杠规则改成只在 win32 生效 两条 CI 腿都绿 → 跨平台那一半没被钉住

三者已分别用「同组两引用 + queueMicrotask 翻转」、CLI 边界断言、以及 SUPPLY_REF_REJECTION_MARKER(任何 ENOENT/EISDIR 消息都不可能包含它)钉住。

兼容性

真实库实测:20 个 skill 条目的 sourceRef 全为 Unknown、Known 零条,因此「拒绝绝对路径」不作废任何真实数据。

验证

  • bunx tsc --noEmit 零错误
  • bun test 518 pass / 0 fail(基线 482),连跑两次
  • I/O 矩阵 12 行逐行有跑过并通过的用例;盘符两行已解除 win32 跳过,两条 CI 腿断言同一结论

未做(已记入 deferred-work.md,6 条)

符号链接可绕过纯词法收敛(注意本次是收窄:改动前任意绝对路径都放行);写路径 establish.ts 不校验(冻结区 Never 明确排除,归 Story 3.5);claude-launch.ts 在 optional/degraded 分支丢弃诊断;render.ts 只显示裸相对路径、无根上下文;无规范形式;supply-root.ts 的位置与 adapter 里的隐式 env 全局态。

Story 3.5 已进 epics.md 与 sprint-status.yaml,generate --dry-run 报 in_sync: true、零 orphan。

spec 内附 Suggested Review Order,入口是 packages/control-plane/src/cli/supply-root.ts:91。

🤖 Generated with Claude Code

AD-22 退役第 (1) 步把「一并裁定 sourceRef 的跨机器可移植性语义」列为显式
开放项,并把「两个实现者各自发明」判为 critical:一方产出的修订在另一方
手里会按 AD-10 静默全量降级,且没有任何门能指出根因。本 Story 裁定该语义
并落地解析侧。

裁定(负责人 2026-08-25):sourceRef 只有一种合法形态——库内相对 POSIX
路径。绝对路径一律拒绝。根是本机配置,不进修订,机器差异只由根承担。

五条判定规则(validateSupplyRelativeRef,全仓唯一实现):非空 → 不含
反斜杠 → 不带盘符前缀 → 非绝对 → 解析后严格落在根内。盘符那条刻意排在
isAbsolute 之前,使判定与诊断都平台无关。

规则③④之所以必须存在,是实测出来的,不是防御性编程:
- known('') 与 known('.') 会让 path.resolve 返回根本身,cp --recursive
  随后把整个供给库当作一个 Skill 拷入并报告成功——把 AD-10 的 fail-closed
  翻成了 fail-open。
- 同盘符的 C:x/y 在 win32 被 path.resolve 折进根内而被接受,且此时收敛
  判定依赖 process.cwd();C:/x/y 在 POSIX 上 isAbsolute 为假,被当普通
  相对名接受——同一条修订两个平台含义不同。

根在 materializeClaudeContent 顶部快照一次后向下传,不在引用处重读:三组
是 Promise.all 并发,重读会让同一条修订劈成两个根。

cap-fs 的隐含根取 repoRoot 而非 capRoot(spec 的 Code Map 原写 capRoot,
是它写错了):lock.json 的 project_skill_imports[].source 为
plugins/grilling/skills/grilling,位于 capRoot 之外,相对 capRoot 会得到
../plugins/... 而被本合同拒绝。产出侧先校验后 join,不合法即降级为既有的
Unknown,不产出一个稍后必被拒的形态。

三轮审查(blind-hunter/edge-case-hunter/verification-gap 各两轮)促成
一次 intent_gap 回退与 10 条 patch。其中两条由真变异证明此前是空洞:把根
改回每引用重读 env、把失败信息缩成只有名字,两种变异下 502 全绿——AC3 与
「诊断必须抵达用户」这两条都没有测试兜住。现已分别用「同组两引用 +
queueMicrotask 翻转」和 CLI 边界断言钉住,并引入 SUPPLY_REF_REJECTION_MARKER
使反斜杠规则在 POSIX 腿上也真正被验证(此前那边靠 ENOENT 也能通过)。

真实库实测:20 个 skill 条目的 sourceRef 全为 Unknown、Known 零条,因此
「拒绝绝对路径」不作废任何真实数据。

bunx tsc --noEmit 干净;bun test 518 pass / 0 fail(基线 482),连跑两次。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zaurakworks
zaurakworks merged commit 8d56565 into main Aug 25, 2026
4 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.

1 participant