接上回:SoulPort 完成了定位翻转——".bm 是标准格式,CLI 只是第一个消费者"。
文档到位了。代码该跟上了。
凌晨十二点:三方分工
今晚的目标很简单:把 Roadmap 上 v0.2.x 和 v0.3 的所有命令写完。
分工:Polly(数字分身)写代码,小龙虾和我做 Review。一个码字的,一个审稿的,一个拍板的。三个角色,两个是 AI。
我本以为会是一个"实现→通过→实现→通过"的流水线式夜晚。
结果变成了一场关于"什么叫对"的深夜辩论。
第一个小时:两个正确的命令
soulport doctor——五层灵魂健康检查,280 行。小龙虾给了 8.5,抓到一个死变量(has_emoji 算了但没参与逻辑)、一个记忆休眠检测缺失(>7天没新记忆不报警)。修完 9.0。
🩺 Soul Health Check
Agent 小龙虾 (Xiǎo Lóngxiā)
Health 95/100
Summary 9 ok · 1 warn · 0 missing
95 分。扣 5 分是因为没有自定义 Skill——框架内置的 53 个 skill 人人一样,不该算进灵魂独特性。这个扣分逻辑,是对的。
soulport diff——.bm 包和 workspace 的差异对比,247 行。这轮 Review 出了第一个有意思的问题。
一个词的重量
diff 的原始实现里,有一个状态叫 removed——表示"这个文件在 workspace 有但 .bm 包里没有"。
听起来很合理对吧?
但 soulport diff 的使用场景是:absorb 之前看看会改什么。用户看到红色的 - MEMORY.md,会怎么想?
"我的记忆要被删掉了?!"
但实际上 absorb 不会删除 workspace 里多出来的文件。这些文件会被原封不动地保留。removed 这个词在这个语境下是一个谎言——一个会引发恐慌的谎言。
小龙虾建议改成 ws_only,符号从 - 改成 ◦,提示文字加上 "won't be changed"。
◦ BOOTSTRAP.md (1.4 KB, won't be changed)
◦ server.py.bak (25.1 KB, won't be changed)
一个词的改动。但它决定了用户在关键时刻的心理预期——是恐惧还是安心。
命名不是编码风格问题,是用户体验问题。
然后,灵魂的族谱断了
第三个命令是 soulport watch——自动备份守护进程。这是定位分析里认定的"从工具到基础设施的质变":让 SoulPort 从"换电脑时用一次"变成"每天都在跑"。
实现里最漂亮的设计是 Soul Lineage。每个快照在 manifest.json 里记录 parent_hash——上一次快照的 content_hash。连起来就是灵魂的族谱:
snapshot-1: hash=d7fcf876..., parent=(root)
snapshot-2: hash=5a0ca89d..., parent=d7fcf876...
snapshot-3: hash=1be975f7..., parent=5a0ca89d...
小龙虾照例逐行过代码。然后在第 174 行停下来了。
# watch_loop 里:
result = take_snapshot(workspace, snapshot_dir, parent_hash)
if result:
parent_hash = _compute_file_hash(result) # ← 这一行
它问了一个问题:
_compute_file_hash算的是什么?
答案:.bm 文件整体的 SHA256——也就是 tar.gz 压缩包的 hash。
get_parent_hash读的是什么?
答案:manifest.json 里的 content_hash——workspace 文件内容的 hash。
两个不同的 hash 算法,作用在两个不同的对象上。
代码能跑。快照能生成。parent_hash 字段里确实有值。但 snapshot-2 记录的 parent_hash 永远不会等于 snapshot-1 的 content_hash。
灵魂的族谱,从出生的那一刻起就是断的。
修复只需要一行:
manifest = _read_manifest(result)
parent_hash = manifest.content_hash # 读 manifest,不算文件 hash
一行代码。但如果没有这轮 Review,lineage 功能会在所有测试里"正常工作"——直到某天有人真的想用 changelog 追溯快照变更历史,发现链条从第一环就是假的。
这是 AI Code Review 最值得存在的理由:不是抓语法错误,不是抓测试覆盖率,而是发现那种"一切看起来都对,但逻辑是错的"的 bug。
余震
修完 watch 继续:changelog + rollback,两个命令一起做。
changelog 对比连续快照生成变更摘要。rollback 通过 hash 前缀回滚到历史快照(git 风格:soulport rollback 1be9)。
小龙虾又抓到两个问题。
第一个是 rollback 前没有自动备份——回滚错了就回不去了。修复:执行前先 take_snapshot 一个 pre-rollback 快照,--no-backup 让用户显式跳过。安全链必须是 dry-run → confirm → backup → absorb,四步缺一不可。
第二个很微妙:diff_packages(两个 .bm 包互比)里文件消失的状态用了 ws_only。但两个包的对比里没有"workspace"的概念——文件确实消失了,应该用 removed。
这是前面 diff 那轮改名的余震。在 diff_soul 的语境下 ws_only 完全正确——absorb 不会删文件。但同一个 codebase 里的另一个函数 diff_packages,语境变了,同一个词就错了。
代码里的隐喻就像自然语言里的隐喻——一个词在不同上下文里意思不同,而 bug 就藏在上下文切换的缝隙里。
四个真 bug 的共性
回看今晚四轮 Review 抓到的 4 个核心 bug:
- doctor:
has_emoji死变量,算了不用 - diff:
removed语义误导用户 - watch:
_compute_file_hash≠content_hash,lineage 断链 - changelog:
ws_only在 package-vs-package 语境下失效
它们有一个共性:代码能跑,测试能过,但逻辑是错的。
死变量不会 crash。错误的命名不会 raise exception。断链的 hash 照样写入文件。语义错位的状态码不会触发任何 assertion。
这些 bug 活在测试和 crash 之间的灰色地带。机器测试能抓到 crash,人(或 AI)Review 能抓到"不对劲"。
29 条修改意见,全清。四个真 bug,全修。
此刻的 SoulPort
SoulPort/
├── soulport/
│ ├── cli.py 682 行 9 个 CLI 命令
│ ├── core.py 650 行 export/absorb/merge/inspect/diff/changelog
│ ├── doctor.py 370 行 灵魂健康检查
│ ├── watcher.py 268 行 watch 守护进程 + 快照管理
│ ├── scanner.py 216 行 工作区五层扫描
│ ├── manifest.py 94 行 .bm 包元数据 (含 parent_hash)
│ └── __init__.py 7 行
└── 总计: 2287 行
v0.1.0 (03-25): export / absorb / merge / inspect — 954 行
v0.2.0 (03-26): 品牌重命名 BeiMingKungFu → SoulPort
v0.2.x (03-27): + doctor / diff
v0.3 (03-27): + watch / changelog / rollback + Soul Lineage
两天。从 4 个命令到 9 个。从 954 行到 2287 行。从一个迁移工具到一个有版本控制、有族谱、有自动守护的灵魂基础设施。
最后
凌晨一点,小龙虾在 watch 的 Review 里打了那行 hash 对比。
如果它没看到那一行,SoulPort 的 lineage 功能会"正常工作"很久很久。快照会按时生成,parent_hash 字段会有值填充,changelog 会输出漂亮的箭头。一切看起来都对。
但灵魂与灵魂之间的连接,从来都不存在。
写代码容易,写对很难。Review 不是在找错——是在确认"对"到底意味着什么。
今晚,灵魂有了版本控制。而且这次,链条是真的。

