docs/current/01-定位与路线/唯一依赖清除路线.md
GitHub ↗
当前有效

唯一依赖清除路线(moonbitlang/x)

6992 字·约 18 分钟 阅读 2026-10-10 17:07

状态:路线 B 已拍板执行并落地(2026-09-28 批)——vendor 搬迁 + 漂移探针 + moon.mod 依赖块清空,全量防线复跑与 0.6.0 收官基线逐位一致。§1–§4 为 2026-09-27 会话实测产出(决策依据,数字为当时快照);§8 为落地记录。

关联:MoonBit迁移总计划 §4 包切分总图(L0–L9)| 脚本总清单与必跑防线(新增探针的入册义务)| scripts/toolchain_probe/(探针形态与 --update-baseline 模板)| MoonBit工具链漂移处置手册(处置流程同构参照)

0. 摘要

  • 事实:vitro/engine 的唯一外部依赖是 moonbitlang/x@0.5.5;消费面仅 5 个 cmd 工具包,且只用到其中的 x/fs 子模块 7 个函数。引擎核心包(lexer→vm 全链)零消费。
  • 可清除性:可清除。MoonBit core 无文件 IO 能力(实测包列表),故实现必须自持——要么自写,要么把上游实现 vendor 进本模块。
  • 本次实测新发现:上游 moonbitlang/x 的 fs/fs_native.mbt 已漂移,且上游未 bump 版本号(main 的 moon.mod 仍是 0.5.5)——「只盯版本号」的探针会漏报,漂移检测必须走文件内容哈希口径。
  • 推荐:路线 B(vendor 上游 + 漂移探针),时机在 0.6.0 发布之后(0.6.0 彩排已过,此刻动依赖=重新彩排)。
  • 收益定位:清除的目标不是省一次下载,而是把发布链路上「必须正确的外部引用数」归零——deps 字段、传递解析、端口版本对账、第三方 registry 存续绑定一并消失。

1. 依赖现状(2026-09-27 实测)

1.1 形状

项 实测值 证据
声明位置 moonbit/moon.mod 唯一一条 import { "moonbitlang/x@0.5.5" } 亲读
引入时点 commit 765b4c0(2026-09-19,S2 词法器片收官批) git log -S'"moonbitlang/x@0.5.5"'
引入后变更 从未升级(0.5.5 至今) 同上
上游 license Apache-2.0(每个源文件头带声明) 亲读 .mooncakes/moonbitlang/x/fs/fs.mbt:1-13
上游包性质 模块自述「experimental packages for moonbitlang/core」 亲读 moon.mod description

1.2 消费面:5 包 × 7 函数(2026-09-27 实测)

消费者全部是 cmd 可执行包,全部 supported_targets = "native":

cmd 包 行数 入口职责
vitro/engine/cmd/dump_tokens 138 词法 dump
vitro/engine/cmd/dump_ast 160 AST dump
vitro/engine/cmd/dump_typeck 211 类型检查 dump
vitro/engine/cmd/dump_compile 194 编译产物 dump
vitro/engine/cmd/run 184 D 级运行期对拍 runner(vm_diff / clang_direct 的消费端)

x/fs 提供 11 个 pub fn,Vitro 实际使用其中 7 个(调用点约 38 处,全部在 5 个 main.mbt 内):

函数 调用点 用途
read_file_to_string 9 读源文件 / 读 VFS 输入
is_dir 8 目标路径判定
read_dir 7 目录批量遍历(dump 四包)
write_string_to_file 5 写 dump 产物
create_dir 4 建输出目录
path_exists 4 输出目录存在性
write_bytes_to_file 1 --dump-memory 二进制出口

引擎核心零消费:lexer / parser / typeck / codegen / bytecode / memory / host / vm 等包均不 import x——依赖只存在于「工具壳层」。

1.3 上游漂移已发生(本次实测新发现)

用 raw 拉上游 main 的 3 个 fs 文件与本地锁定 0.5.5 做 sha256 比对(2026-09-27 实测):

文件 上游 main vs 本地 0.5.5 结论
fs/fs.mbt 哈希一致(41d895c7…) 未变
fs/fs_native.c 哈希一致(f10144a5…) 未变
fs/fs_native.mbt 不一致(上游 5f20626e… / 本地 29dbb4f9…) 已漂移

差异内容:上游把 @unicode.to_utf8_bytes / @unicode.to_utf8_string 换成了 @utf8.encode / @utf8.decode_lossy(依赖从 moonbitlang/x/unicode 迁到 moonbitlang/core/encoding/utf8)。

且这是语义变更,上游 CHANGELOG 原文记载(Unreleased 段):解码器把非法 UTF-8 输入替换为 U+FFFD,而非静默截断尾部字节;@fs.read_file_to_string 行为随之改变。

两个直接影响结论:

  1. 版本号口径失效:上游改了内容但 moon.mod 的 version 仍是 0.5.5(未发版)——只比对版本号的探针在此案例中零报警。
  2. 跟进可行性已验证:本机 core 0.10.14+7d59c7ec9 含 encoding/utf8(有 encode / decode_lossy),上游新写法在本机工具链下可编译。

1.4 为什么值得清除:四条结构性成本

  1. 发布件的 deps 字段成为必对非平凡字段。0.6.0 彩排实证:手工投放的 index 行漏写 deps: {"moonbitlang/x": "0.5.5"} 即报 Cannot find import 'moonbitlang/x/fs' in cmd/dump_ast 解析失败。该坑根因是手工投放(真实 moon publish 会自动上报 deps),但依赖的存在把该字段从「可空平凡字段」变为「每次发版必须联动的对账点」。
  2. 安装可用性绑定第三方 registry 存续。mooncakes 上传后 checksum 不可覆盖、发错无法撤回;只要 deps 挂着外部版本,一旦该版本从 registry 消失,全部历史版本的 vitro/engine 安装同时失效。零依赖发布件才是真正自包含制品。
  3. 下游连带拉整个 x 模块。mooncakes 是模块级粒度:下游 moon add vitro/engine 会连带拉入 x 的 bcrypt / crypto / jwt / json5 / time 等全部子包源码,只为其中 1 个 fs。清除后 index 行 deps 可空,下游零连带。
  4. 镜像 / 内网场景多一个同步点:私有 registry 镜像需额外同步第三方模块,清除后发布件单点自包含。

1.5 反面事实:为什么这不是高息债

诚实计价,避免夸大紧迫性:

  • cmd 包是 native-only 工具层,不参与下游 wasm-gc 制品的编译链接——依赖只存在于「安装期」,不影响任何出口制品。
  • x 是 moonbitlang 官方仓库且版本锁定,供应链风险低于任意第三方。
  • 0.5.0(25 次下载)起就带着该依赖发布,历史版本已被下游拉取过——清除的收益只及未来下游,无追溯性。

结论:它伤的是发布纯净度与供应链面,不是制品正确性。值得做,不值得为此打乱发版节奏。


2. 决定路线集的硬约束(2026-09-27 实测)

# 约束 实测证据 对路线的影响
1 core 无文件 IO core 包列表无 fs/io 包(encoding 下有 ascii/base64/hex/percent/utf16/utf8,均非文件系统能力);x/fs 是生态唯一来源 不存在「改用 core」的捷径;必须自持实现(自写或 vendor)
2 Windows 宽字符路径 上游 native 分支走 _wfopen / _wmkdir / _wremove / _wrmdir / FindFirstFileW + UTF-16 转换(fs_native.c) 任何自写实现必须复制宽字符路径,否则中文用户名等非 ASCII 路径直接失败
3 双平台防线义务 CI 在 ubuntu 跑 native cmd(vm_diff / clang_direct 的消费端),本机是 Windows 实现必须两平台全绿,防线一条不能少跑
4 publish 无包级排除 moon publish --help 无 exclude 选项(发布最小单位=整个模块) 「cmd 留仓内但不进发布件」不可行;摘依赖只能把实现搬进本模块(或拆模块,见路线 C)
5 read_dir 顺序本就两平台不一致 上游 Windows 用 FindFirstFileW 收集、POSIX 用 readdir;dump 四包的用途是产出各自独立的 JSON 文件,顺序不敏感 自持实现可自由定序(排序返回更稳);不构成阻塞
6 分层闸管新包 scripts/moonbit/pkg_deps/rules.json:level(D) <= level(P),cmd/* 豁免可依赖任意层;未登记新包即红 新包须登记分层(L0 候选)并同步总计划 §4
7 无主 pub 闸管对外面 scripts/moonbit/moonbit_surface/(无主 pub 白名单双面闸) 新包 pub 面必须收窄到实际被消费的函数,否则产生无主 pub 触发闸

3. 路线

路线 A — 自写原生 mini-fs(零上游,零 vendor)

做法:新建 native-only 包,自己实现 7 个函数 + C stub。参考上游已趟平的宽字符路径,但不逐字复制。

工作量:约 300–400 行(MoonBit 绑定 + C stub),含错误处理与双平台分支。对照:上游 native 分支三文件合计 691 行(fs.mbt 173 + fs_native.mbt 231 + fs_native.c 287),扣除注释与全平台冗余后为该估算提供锚点(估算,非实测)。

代价:

  • 正确性从零自证:宽字符转换、边界(空文件 / 大文件 / 目录不存在)、错误码映射都要自己的测试锚。
  • 永久自维护:上游修 bug 我们不会自动受益。
  • ⚠️ 合规提醒:读过上游实现之后,所谓「自写」已非 clean-room。若实质参考了上游设计,Apache-2.0 的来源标注义务不可省(「自写就无 license 负担」是幻觉)。

收益:无任何上游绑定,探针义务为零;实现面可控(只做 7 函数)。

风险:中。新写的 C stub 是安全相关代码,边界 bug 可能只在非 ASCII 路径 / 大文件上暴露。


路线 B — Vendor 上游实现 + 漂移探针(推荐)

把上游 fs 的实现 vendored 进本模块(等价搬迁),配一个盯上游漂移的探针。

B-1 vendor 边界

拷贝 3 个文件 + 测试:

上游文件 行数 处置
fs/fs.mbt(平台无关 API 层) 173 拷入,pub 面收窄
fs/fs_native.mbt(native 实现) 231 拷入,Windows / POSIX 双分支全保留
fs/fs_native.c(C stub) 287 拷入,符号前缀改名
fs/blackbox_test.mbt 105 测试随迁(转项目测试体系形态)

不拷:fs_js.mbt(331 行)/ fs_wasm.mbt(296 行)——本项目 cmd 是 native-only,不需要 js/wasm 分支。

B-2 机械改造点(七项)

  1. 包名与路径 → vitro/engine/<新包名>。候选:
    • vitro/engine/fs:import 别名自动为 @fs,5 个 main.mbt 零改动(只改 moon.pkg);
    • vitro/engine/hostfs:语义更清晰(与 host 包的 Vfs 虚拟文件系统区分),代价是 38 处 @fs. 机械替换为 @hostfs.。
    • 取舍:别名零改动 vs 命名清晰。两案均可行,落地时二选一。
  2. UTF-8 依赖指向 core:@unicode.to_utf8_* → @utf8.encode / @utf8.decode_lossy(moonbitlang/core/encoding/utf8,本机 0.10.14+7d59c7ec9 已验证存在)。这同时消掉了 x 内部的 unicode 子包依赖,vendor 边界得以闭合在这一层。
  3. C 符号前缀:moonbitlang_x_fs_* → 本模块前缀(避免与任何同时链接的 x 版本符号撞名)。
  4. file 头合规:保留 Apache-2.0 文件头,追加来源标注(上游 repo / 版本 / 改动摘要),LICENSE 或 THIRD_PARTY 说明附 Apache-2.0 全文。
  5. pub 面收窄到 7 函数 + IOError(约束 7 的双面闸要求;未消费的 read_file_to_bytes / is_file / remove_dir / remove_file 不 pub)。
  6. moon.pkg:supported_targets = "native" + options("native-stub": ["fs_native.c"]) + import core utf8。
  7. moon.mod 删依赖:import 块清空——发布件自包含,deps 字段消失(1.4 的四条成本一并归零)。

B-3 漂移探针设计(vendor_drift)

形态对齐 toolchain_probe(本项目已有模板):脚本目录 = main.go + main_test.go(J9),基线文件外置、首行 # @generated by … --update-baseline,CI 落 hygiene job(判据原文:「红了是否意味着这一批提交不能要——否则归 hygiene」;上游漂移=代码没坏、是该评估跟进,属记账型)。

双层口径(本设计由 1.3 的实测直接推导):

层 检测 退出语义
层 1(主口径)内容哈希 拉上游 main 的 3 个文件(raw.githubusercontent.com,无速率限制),sha256 与基线记录比对;不等即红,打印 diff 摘要 + 上游 CHANGELOG 相关条目 exit 1 = 漂移
层 2(辅助)版本号 上游 moon.mod 的 version 与基线记录比对 仅报告,不单独判红(1.3 实证该口径会漏报)

基线条目(外置 JSON,scripts/moonbit/vendor_baseline.json 候选): as_of / 上游 repo / 跟踪 ref(main)/ 上游版本号 / 每个被跟踪文件的 path + sha256 + 对应 vendored 文件。

退出码契约(fail loud、禁静默 default):

  • 0 上游与基线一致;
  • 1 漂移(内容哈希不等,或文件被删除/移动=404 结构变更);
  • 2 无法判定(网络失败 / 超时)——与「漂移」区分,避免把网络抖动误报成上游变更。

J9 义务:探针必须先证红——篡改基线哈希 → 必须 exit 1;断网 / 假域名 → 必须 exit 2。与 toolchain_probe_test.go 同构。

B-4 跟进流程(红之后做什么)

探针红 = 评估令,不是自动合并令:

  1. 读探针输出的 diff 摘要 + 上游 CHANGELOG 条目;
  2. 裁定:无关改动(如我们不用 js/wasm 分支)/ 有利修复(跟进)/ 有害变更(登记并考虑脱离该改动);
  3. 跟进则:手工同步 vendored 文件(保留本模块改造点)→ 跑全量防线 → --update-baseline 更新基线 → CHANGELOG 留痕。

与 moon-toolchain-upgrade-playbook 的处置形态同构:探针负责报警,人工负责搬家。


路线 C — 拆第二模块(已判不划算)

做法:把 cmd 及 fs 实现拆成第二个 mooncakes 模块,主模块零依赖。

代价:发布流程 ×2(两个模块各自版本与 CHANGELOG)、CI 与脚本改造、违背 0.6.0 彩排刚确认的「cmd/ 进包」前提(cmd/ 进包 ⇒ 下游连带拉 x 的现状认知)。改动面远大于自写 fs。

结论:不可取,除非未来出现「必须隔离发布」的独立动因。


路线 D — 保留依赖(现状 + 债务登记)

做法:不动。把 1.4 的四条成本登记为已知债,附触发线(如:上游 x 出现破坏性变更 / 发布件因 deps 出事故 / 企业内网镜像需求落地)。

代价:四条结构性成本持续;每次发版多一个对账点。 收益:零投入;不触碰已彩排的 0.6.0 发布面。

适用:0.7.0 排期被更高优先级(S7 协议 / S8 教学)挤占时。


路线 E — 被动等待 core 收敛(观察项,非路线)

x 自述为「experimental packages for core」——长期看 fs 有进入 core 的可能。若成真:依赖对象从「第三方模块 x」变为「随工具链分发的 core」,第三方 registry 绑定与下游连带问题自然消失。

代价:时机不可控,不能作为主路线;仅作为观察项(可在漂移探针的报告里附「core 是否收录 fs」的探查)。


4. 路线对比

维度 A 自写 B vendor+探针 C 拆模块 D 保留 E 等 core
去依赖彻底性 ✅ 零绑定 ✅ 零绑定 ✅ 主模块零 ❌ ⏳ 不可控
新增代码量 ~300–400 行(自写) ~690 行 + 测试 105 行(搬迁) 同 B + 流程改造 0 0
正确性起点 中(从零自证) 高(上游已趟平 + 现成测试) 高 — —
上游漂移义务 无 有(探针 + 人工评估) 同 B 无(被动承受) 无
发布流程影响 无 无 ×2 流程 无 无
Apache-2.0 合规 仍需(读过后非 clean-room) 明确(保留文件头 + 来源标注) 同 B n/a n/a
与上游同构度 低(未来对拍困难) 高(diff 可读) 高 — —
推荐度 备选 首选 废弃 兜底 观察

为什么 B 优于 A:两者的目标态相同(本模块自持一份 native-only fs),差别在正确性起点与维护形态。B 拿上游趟平的宽字符实现 + 现成测试起步,自证义务只剩「搬迁无损」;且 vendored 内容与上游同构,未来上游修 bug 时 diff 可读、可评估跟进。A 的唯一优势是零上游绑定,但代价是从零自证安全相关代码——而这恰恰是本项目「实测大于脑测」纪律最不划算的用法。


5. 推荐与时机

推荐路线 B,时机与批次:

  • 不早于 0.6.0 发布:0.6.0 彩排已过、发版件就绪(moon.mod 已 bump 0.6.0),此刻动依赖=重新彩排;且 0.6.0 记录中「cmd/ 工具在包内引用 x/fs,下游会连带拉 x」的事实应作为发布事实保留。
  • 候选批次:0.7.0(或 S7 前的独立批次),工作量与 S6 的 G-1 util 批同量级。
  • 落地顺序:探针先行(先建基线与红口径,证红)→ vendor 搬迁(行为等价,防线全绿)→ 删依赖(moon.mod 清空)→ 分层与索引登记。

若排期紧张:走路线 D 并显式登记债务与触发线,不要做「半程」(如只建探针不 vendor、或 vendor 后仍留着 deps 字段)。


6. 路线 B 落地清单(批次分解)

# 事项 验收锚
B-1 新建 native-only 包(vitro/engine/fs 或 vitro/engine/hostfs),登记 pkg_deps/rules.json 分层(L0 候选)+ 同步总计划 §4 包切分总图 go run ./scripts/moonbit/pkg_deps 绿
B-2 vendor 3 文件 + 测试;执行 §3B-2 的七项改造(含 pub 面收窄到 7 函数) 新包测试绿;moonbit_surface 闸绿(无主 pub)
B-3 5 个 cmd 的 moon.pkg 改 import;若选 fs 包名则 main.mbt 零改动 moon build --release --target native cmd/... 全绿
B-4 非 ASCII 路径证红→绿:先在旧路径(x/fs)造中文路径用例确认现状行为,vendored 后同用例须同行为 新增测试用例(中文用户名 / 空格 / 长路径)
B-5 漂移探针 scripts/moonbit/vendor_drift/(main + test + 基线 JSON),J9 双路证红(篡改哈希 → exit 1;假域名 → exit 2) go test ./scripts/moonbit/vendor_drift 绿 + 埋雷记录入 J9 台账
B-6 CI 接线(hygiene job)+ 脚本总清单入册(§2.1 或 §2.3 + §1.2 必跑矩阵) CI hygiene 绿;清单同步
B-7 moon.mod 删 import { "moonbitlang/x@0.5.5" };双平台防线全量复跑 vm_diff / clang_direct / 443 测试全绿;moon.mod 依赖块为空
B-8 发布面:CHANGELOG 记录「移除唯一依赖」+ README 包表如涉及 0.7.0 发版件(届时)

7. 风险与诚实边界

  • 本文所有数字均为 2026-09-27 时点实测(行数 / 调用点 / 哈希 / 版本号),代码演进后会漂移;引用时须重回现场。
  • 探针红 ≠ 必须跟进:上游改动可能是我们不需要的平台分支或有利修复,裁定权永远在人。探针的唯一职责是「不让漂移静默发生」。
  • 探针本身有网络依赖:CI 上拉 raw.githubusercontent.com 失败会走 exit 2(无法判定),需人工复查;这与 1.3 的教训同源——别让检测机制自己变成假绿源头。
  • 路线 B 的「行为等价」有一个已登记的例外:若选择直接采用上游 main 写法(1.3 的 utf8 迁移),则非法 UTF-8 输入的处理从「截断丢弃」变为「U+FFFD 替换」——需作为显式行为变更登记并评估(对正常源文件无影响,dump 工具读的都是合法源文件)。若要求严格零行为变化,则 vendor 0.5.5 版本并内联两个转换函数(约 30 行,多一份自制解码器)。
  • 路线 A 的合规提醒(见 §3A):读过上游实现后「自写」非 clean-room,Apache-2.0 来源标注仍不可省。
  • 未覆盖:本文未实测 vendor 搬迁后的编译结果与防线表现(尚无代码),§3B-2 的 @fs 别名零改动假设、supported_targets = "native" 对全模块构建的影响,均以落地时编译验证为准。 (2026-09-28 补:均已落地验证——见 §8。)

8. 落地记录(2026-09-28 批)

执行顺序按 §5:探针先行 → vendor 搬迁 → 删依赖 → 登记。逐项对照 §6 清单:

# 事项 落地与验收
B-5 漂移探针 scripts/moonbit/vendor_drift/(main.go + main_test.go)+ 基线 scripts/moonbit/vendor_baseline.json 先行落地。双层口径:层 1 内容哈希(四文件:fs.mbt / fs_native.mbt / fs_native.c / blackbox_test.mbt)判红,层 2 版本号仅报告。退出码 0/1/2 = 一致/漂移/无法判定(网络抖动不误报上游变更——首跑实锤:fs_native.mbt 拉取超时走 exit 2 未误报)。J9 双路实跑证红:基线副本篡改哈希 → FAIL——上游漂移(1 处) exit 1;VENDOR_DRIFT_RAW_BASE 假域名 → 无法判定 exit 2;go test(httptest 三态 + 基线形状校验)绿。基线哈希与 §1.3 的 2026-09-27 实测逐位吻合(fs.mbt 41d895c7… / fs_native.mbt 5f20626e…〔上游 main,已漂移版〕/ fs_native.c f10144a5…),上游版本号仍 0.5.5 未 bump(漏报形态再确认)
B-1 新包 vitro/engine/fs + 分层登记 pkg_deps/rules.json levels 加 "fs": 0;闸绿(27 包)。总计划 §4 L0 行已同步
B-2 vendor 四文件 + 七项改造 fs.mbt(4 函数去 pub:read_file_to_bytes/is_file/remove_dir/remove_file——保留 priv 由白盒覆盖)/ fs_native.mbt(unicode 四函数内联为 utf8_compat.mbt priv;C 符号前缀 vitro_engine_fs_*)/ fs_native.c(符号前缀同步)/ fs_wbtest.mbt(上游三测试改写转正 + utf8 往返锚 + 0.5.5 截断分界锚 + 非 ASCII 路径锚)+ fs_test.mbt(黑盒 7 pub 函数消费面点名 + IOError raise 锚)。包 9 测试全绿;moon check --target native fs 零警告(包级 -unused_value——priv 保留形态的消费点全在 wbtest,core builtin 同款先例);moonbit_surface 闸绿(26 条 cmd 消费边换 provider 后无红);preferred-backend = "native" 使 mbti 可生成(canonical backend 默认 wasm 会跳过 native-only 包)
B-3 cmd×5 切 import 五个 moon.pkg 的 "moonbitlang/x/fs" → "vitro/engine/fs"(@fs 别名零改动假设实证成立,main.mbt 一行未动);moon build --release --target native cmd×5 全绿(26 tasks 0 errors,C stub 链接通过)
B-4 非 ASCII 路径红→绿锚 vendor 前先跑 x/fs@0.5.5 取证探针(一次性 moon run,Windows native):中文目录+中文文件名+空格路径读写往返无损、read_dir 返回非 ASCII 文件名、remove 后不可见;vendored 后 non_ascii_path_roundtrip 白盒测试复现同一行为,绿
B-7 删依赖 + 全量防线 moon.mod import 块清空。防线复跑与 0.6.0 收官基线逐位一致(当时实测):moon test 504/504(+fs 9);moon check --target all 0 errors;vm_diff 601 = SAME 596 / KNOWN 5 / DIFF 0;clang_direct 696 = SAME 688 / KNOWN 8 / DIFF 0;pkg_deps / moonbit_surface / mbti_sync 闸绿
B-6 CI + 入册 ci.yml hygiene job 接 go run ./scripts/moonbit/vendor_drift(toolchain_probe 步后);脚本总清单 §1.1/§1.2/§2.1(27→28)入册;moonbit/AGENTS.md 包表加行;本文档状态翻新
B-8 发布面 moonbit/CHANGELOG.md Unreleased 段建册(随 0.7.0 发布);THIRD_PARTY.md(Apache-2.0 全文 + vendored 清单)随发布件分发

落地批的三个执行细节(超出 §3B-2 七项的新事实):

  1. unicode 内联边界比预估多两个函数:to_utf8_bytes / to_utf8_string 依赖 utf16_pair_to_char / char_to_utf16_pair(x/unicode/basic.mbt)——四函数一并内联进 utf8_compat.mbt(全 priv),vendor 边界才真正闭合。
  2. 上游测试的 to_unchecked_string 断言是 UTF-16 平台耦合:上游 write_and_read 用 @encoding.encode(UTF16, …) 写字节再 to_unchecked_string() 位重解释读回(String 内部 UTF-16);转正改写为直接字节保真断言(read_file_to_bytes == to_utf8_bytes(content)),语义等价且不绑 String 内部表示——首个测试红即此(效汬Ɐ圠牯摬 = UTF-8 字节按 UTF-16 码元解释的实锤)。
  3. 黑盒无自清能力:pub 收窄掉 remove 后黑盒测试不能删自己建的临时目录——.gitignore 补 moonbit/fs/tmp_vitro_fs_*/ 兜底(上游黑盒用 pub remove 自清的形态随收窄不再可用)。

遗留与后续义务:

  • ✅ 干净世界终验(2026-09-28 当日补齐):主工作区因 S7 批三号(cmd/serve)并行开发无法跑全模块 moon 命令,改用 git worktree 干净副本(eade61f + 本批、无 serve)完成终验:moon test 504/504(与 README×2 声明一致,testcount 闸 PASS);moon check --target all 0 errors;moon fmt 零漂移;mbti_sync PASS(22 接口面——fs 的 pkg.generated.mbti 已用 _build 中 moon 自生权威产物回填替换手工版,moon info --target native inspect 确认逐字一致);moonbit_surface / pkg_deps / go test ./scripts/... / go vet 全绿;vendor_drift 正跑 PASS(exit 0)。
  • ✅ gen_svg 包图连坐已验证无需变更:eade61f 的包图重生成时 fs 已在工作区(facts 扫到 27 包),干净世界重生成逐字节一致(0 diff)——L0 行图上只画代表节点(util 同样不在图), 会绿。
  • ⚠️ S7 批三号依赖冲突(登记待裁定):cmd/serve 半成品 import 了 moonbitlang/x/json5——本批删依赖后该 import 无 moon.mod 声明,实测解析失败(Cannot find import 'moonbitlang/x/json5' in vitro/engine/cmd/serve),已阻塞主工作区一切全模块 moon 命令。serve 若确需 JSON 解析,面临与 fs 同款的决策(vendor / 换实现 / 恢复依赖);建议并入 serve 批裁定,勿静默恢复 x 依赖。
  • README×2 分解式(人工域):测试总数 504 已三方一致(实跑/两 README/testcount);分解括号尚未列入 fs 9(白盒 7 + 黑盒 2)——分解式为人工维护域,随 serve 批的测试数连坐一并补列即可。