Skip to content

Releases: MYCF711/dsh-GOkifu-compact

1.11.16

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 11:53

P2-1 · 同一份日志、换模型比:脚本 + 一组真实数据

问题:「哪个模型做摘要更好/更快」一直靠感觉。要比就必须喂同一份东西,否则比的是切片大小差异。

新增 scripts/compare-models.mjs(纯函数 + CLI,只读 DSH_HOME,唯一写出的是 --out)

固定住的变量 做法
同一折叠切片 取最近一条 foldMark 的 [seqStart, seqEnd],没有就全量;renderFacts(g,{range}) 逐字节相同 ⇒ 不同模型看到的不是不同大小的切片
同一 instruction buildChessInstruction,不按模型改写
同一 maxTokens 与 temperature: 0 命令行显式给,默认生产值
每模型 N 次(默认 3) 只对成功样本取中位;失败单独计数 + 保留错误原文(不把失败当 0 拉低中位)

记录:延迟中位 / usage / 失败数 / 摘要长度 / 四章节完整度(目标·死路·已验证事实·原始坐标)。
--dry-run 只读图不发请求;不给 --session 时自动挑「失败边最多」的真实会话。

真跑一组(evidence/compare-models.json,session session-d288e7a2…,9 条失败边、facts 1830 tok,maxTokens=8192,repeat 3)

模型 成功 延迟中位 摘要中位 四章节 usage 中位
group/deepseek-v4.1-flash1017 3/3 11451 ms 1769 字符 4/4 4808 tok
group/gokifu-compact 3/3 27076 ms 2211 字符 4/4 网关回 0(无真实 usage)
  • gokifu-compact 慢约 2.4×、长 25%,四章节一样齐;但它的 usage 全是 0(网关不回真实用量)
    ⇒ 这一组数据算不出成本对比,只能比延迟与长度。
  • 附带发现(重要):maxTokens=900 时 flash1017 0/3 —— 预算被 reasoning 吃光,
    finish_reason=length 且 content: null。maxTokens 给小了不是「摘要短一点」,是「完全拿不到摘要」。
    (该组原文留在 tmp/ 里,故意不进包。)

测试 +11(546 → 557):parseArgs(未知开关必须报错)/ median / pickFoldSlice / prompt 字节一致 /
切片外失败边不许进 prompt / 四章节口径 / parseChatResponse / 假 fetch 四态(成功·HTTP 500·网络异常·空摘要)/
中位只算成功 / 报表形状 / CLI --dry-run 端到端。
变异(逐条红):未知开关静默忽略 / 中位混入失败 / HTTP 失败当成功 / 忽略 range。

验证

  • 全量:557 passed / 0 skipped / 0 failed(46 个测试文件;546 + 11)
  • compare-models 11/0
  • npm pack --dry-run --json:77 文件(75 + scripts/compare-models.mjs + evidence/compare-models.json
    —— 证据写进 evidence/ 就会被 files 打进包)
  • verify-install:14/16 —— 仍是 live profile 环境漂移(本包在 live profile 里没有依赖声明、也没挂载),
    与 1.11.16 内容无关;没有动 live

未验证 / 已知问题

  • 对比只做了一组会话(9 条失败边、facts 1830 tok);换会话/换切片长度结论可能不同,不能当普遍结论。
  • 成本对比不可算:gokifu-compact 的 usage 全 0;flash1017 的 usage 是真实的,但没有可比对的一侧。
  • 「四章节完整度」是按标题/关键词命中判的,判的是「有没有写」,不判内容对不对。
  • verify-install 14/16 如实记录(同 1.11.15,环境问题,未动 live)。

1.11.15

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 11:15

P2-2 · fold 变成可观测的:每一次都能查到「走了哪条路由、多久、花了多少 token」

问题:fold 只有 count 和最后一次的 mode —— 想回答「上次压缩为什么这么慢 / 用的是哪个模型 /
是不是降级了」只能翻会话日志
,而日志会滚掉。

改法

  • foldMark 追加四个字段(外加 foldCount):
    字段 含义
    route provider/model —— 无条件记录(有的 provider 不回 usage,但路由永远可得;旧写法把 route 写在 usage 判断里,缺 usage 就丢)
    fallback 本轮摘要模式(rules / null)—— 与 S3 的降级标记同源
    ms summary + commit + flush 的端到端耗时
    usage 真实 usage 快照(provider 没回就是空)
  • 裁传统一:FOLD_MARKS_KEEP = 20,常规 fold 与 tier-2 共用同一个上限(旧版 tier-2 单独用 tier2After×2,
    两条路径各裁各的 ⇒ 保留条数不可预测)。要改只改这一个常量。
  • doctor 新增「最近 5 次 fold」:跨会话倒序 5 条;旧图缺字段显示 「—」(不猜);tier-2 记录单独标注。

测试 +4(542 → 546):foldMark 四字段 / 降级时 fallback=rules / 20 条裁剪 / doctor 的 5 条与「—」。
变异:不写四字段 / 去掉裁剪 / doctor 不显示 —— 三条逐条红。

S11 · 文档状态三处与事实对齐(代码零改动)

审计 §八 R9 指出三处文档与事实相反:

处 修复
README 版本行 曾写 1.7.1 ⇒ 现每次发版自动同步(README:3 与 package.json 一致,由 check-maintenance 与发布门禁共同盯着)
docs/计划表.md 首行标注 「已归档」,并注明「2026-10-29 起唯一持续任务表改为仓库根的 计划表.md」
HANDOFF.md 顶部 日期 2026-10-28 → 2026-10-31;删掉过期的「最紧急:v1.3.5 修 worldHash P0」三条;新增最新状态表(版本 / tag / npm shasum / 单测数 / verify / check / live 仍 1.11.2、落后多版 / 未发的 P2-2 / P6-C 真实 apply 仍欠);旧 1.9.1 块降级为「历史状态 …… 留作时间线」

diff 核对:与备份比 −5 行 / +19 行,尾部 6 行完全一致(旧内容一字未删)。

验证

  • 全量:546 passed / 0 skipped / 0 failed(45 个测试文件;542 + 4)
  • engine-harness 43/0 · tier2 7/0 · ledger 31/0 · release-hardening 22/0
  • bench 2000:351 tokens(与基线一致)
  • npm pack --dry-run --json:75 文件(数量不变)
  • verify-install:14/16 —— ⚠ 不是本版引入的缺陷,是 live profile 的环境漂移(见下)

未验证 / 已知问题

  • verify-install 14/16:这是本机 live profile 与当前代码的漂移,与 1.11.15 的内容无关 ——
    没有动 live(未改 profiles/*/cordis.patch.yml、未重装)。如实记录:这版发布时该门禁是红的。
  • P2-2 的四个字段只在夹具与引擎级验证;没有在真机上让一次真实 fold 落后再从 doctor 读回去确认显示正确。
  • ms 是端到端墙钟,包含 flush 的磁盘时间 ⇒ 与机器负载相关,不能当模型的推理耗时。
  • S11 是文档项,代码零改动;docs/计划表.md 里更早的历史内容未逐条复核。

1.11.14

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 10:39

S3 · 降级要看得见:规则摘要会在热清单里明说「这次没有模型润色」

问题:模型调用失败时 summarize 会退到规则摘要(纯模板拼装)。这一步静默发生 ——
用户只看到「压缩过了」,不知道这次的摘要没有经过模型,遇到摘要质量差时会以为插件坏了。

改法

  • summarize 的四条路径(含 auto 与 manual)都写两个字段到图上:
    lastFoldMode("rules" / "hybrid" / "llm")与 lastFoldDegraded(同一事实的布尔别名,
    热清单/诊断读哪个都行)。写失败绝不影响摘要本身。
  • 热清单在 mode === "rules" 时固定显示一行:⚠ 上次摘要是规则生成(无模型润色);
    hybrid 与旧图(没有这个字段)不显示 —— 不制造假警报。
  • 这一行计入 prePathChars,500 token 预算仍然成立。

S4 · minFold 按窗口缩放:小窗口不再被 8000 卡住

问题:minFoldTokens 原来是固定 8000。窗口小的模型(例如 32K)永远够不到这个门槛 ⇒
永远不 fold,历史一直堆到溢出为止。

  • resolveSpec 返回 minFoldTokens = max(8000, 窗口 × 0.08),selectRange 用它;
    overflow 分支回落配置值(溢出自救不该被这个下限挡住)
  • 显式配置优先:8000 只作为默认哨兵(注释里写明了这点),用户写了值就按用户的来

测试 +9(533 → 542):ledger S3-1/S3-2(提示行出现与不出现)、
engine-harness S3-3…S3-5(四条路径写标记 / 旧图不显示 / 预算仍成立)+ S4-1…S4-4
(缩放公式 / 显式配置优先 / overflow 回落 / 边界)、manual-compact MC-4 补 manual 标记。
变异:删提示行 / rules 不写标记 / 取消缩放 —— 三条逐条红。

验证

  • 全量:542 passed / 0 skipped / 0 failed(45 个测试文件;533 + 9)
  • engine-harness 39/0 · ledger 31/0 · release-hardening 22/0
  • verify-install 16/16
  • bench 2000:351 tokens(与基线一致)
  • npm pack --dry-run --json:75 文件(数量不变)

未验证

  • S3 的提示行只在夹具与引擎级验证过「rules 才出现」;没有在真机上制造一次真实的模型失败去确认它在真实热清单里的位置与可读性。
  • S4 的缩放只经单测覆盖;没有在 32K 这类小窗口的真实会话里观察过 fold 频次变化。
  • 「500 预算仍成立」是按 token 估算的(夹具上的 prePathChars 统计),不是真机计费对账。

1.11.13

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 10:16

① 手动压缩失败说人话(新增房间 R16 manual-compact)

问题:compactNow() 失败时抛的是宿主底层错误 —— 用户看到 Error: cannot compact while turn is open
之类的原文,不知道是自己点早了、还是插件卡了、还是真出 bug。

  • 新增 src/rooms/manual-compact.mjs(零依赖):classify / describe / annotate
  • 分类九种:open-turn · idle · busy-lock · cancelled · summary · persistence · commit · changed · unknown;
    原始 code 与 cause 一字不改地保留(诊断加人话,不吞证据)
  • engine 覆盖 compactNow:成功路径原样返回;失败时把诊断挂到原错误上、换成人话 message,仍 rethrow 原错误
    (调用方拿到的仍是原来的 error 对象,只是多了 .diagnosis)
  • 测试 4 条:纯函数四态 + 真实 Basic 探针(开着的 turn ⇒ busy/open-turn;turn/end 之后不再命中)
  • rooms 登记 R16(房间集合 12 → 13);变异:不诊断 / open turn 分类失效 / annotate 改 code —— 三条逐条红

② TRACE 行带上「最近三个节点」(S5)

问题:TRACE 原来只说「细节在 sessions/<sid>/index.md」—— 模型不知道最近走了哪三步,想定位就得先读盘
(一次工具调用 + 一轮上下文)。

  • ledger 的 TRACE 行现在附带最近 3 个路径节点的 id + 一句话(短语取 agent 的 goal / user 的 text,按
    TRACE_PHRASE_CHARS 截断)
  • 不新增 IO:只用已经算好的路径节点对象与节点自带字段
  • 绝对路径照旧打码:TRACE 行不泄漏盘符/用户名的既有硬约束不变(goal 里完全可能写着路径)

③ P6-C 停窗预检与执行手册

  • 新增 scripts/p6c-preflight.mjs:迁移之前的只读预检 —— 端口可连接 / 同名进程在跑 ⇒ 拒绝 apply(exit 1);
    正常时打印会话数与 graph 哈希;--port=0 --proc-name= 可跳过外部探测(供夹具用)
  • 新增 docs/P6-C 停窗执行手册.md(190 行):只在主人明确给停窗授权后使用;未授权只做只读部分
    (预检 + dry-run);只归档 settled residue,candidate 一条不动
  • 测试 4 条:夹具就绪时 exit 0 且带会话数/哈希 · 端口可连必须 exit 1 · 同名进程必须 exit 1 ·
    只读保证(跑前跑后夹具整树 sha256 不变)

④ 发布脚本:拒绝脏树 + 发布物核对

  • 脏树硬门禁:git status --porcelain 非空即红、exit 1(含未跟踪文件;evidence/ 等噪声不豁免),
    打印前 20 行与恢复办法。旧实现只查 git diff,看不见未跟踪文件。
  • 新增 scripts/verify-release.mjs:git archive <tag> 的树 vs tarball 的 package/ 逐文件比对;
    比较键 = 去 CR 后的内容 sha256(Windows 工作树 CRLF / archive LF ⇒ 按原始 shasum 必假红),
    行尾差异只警告不红;tarball 多出文件 / 内容真变 ⇒ 红。
  • release.mjs 接上 --verify-tarball=:npm publish 成功后、建 Release 之前核对,不过就 abort。

验证

  • 全量:533 passed / 0 skipped / 0 failed(45 个测试文件)
  • manual-compact 4/0 · ledger 29/0 · p6c-preflight 4/0 · rooms 15/0 · release-hardening 22/0
  • verify-install 16/16
  • npm pack --dry-run --json:75 文件(71 → 75:manual-compact.mjs / p6c-preflight.mjs / P6-C 手册 / verify-release.mjs)

未验证

  • 手动压缩诊断是分类正确性的证据(夹具 + 真实 Basic 探针),没有在真机上制造过一次真实失败去验 message 是否好读。
  • P6-C 的真实 apply 仍未执行(需主人授权 + 停窗窗口);预检与手册只是把「怎么安全做」准备好。
  • --verify-tarball 的 abort 分支没有被真发版触发过(本版发布时未传该参数)。

1.11.12

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 09:42

S8 首折削峰:第一次 fold 提前到 30%,之后回到 45%

问题:thresholdRatio(默认 0.45)对每一次 fold 都一样。但第一次 fold 的代价最高 ——
它要一次性压掉整段历史(没有 checkpoint 可续),而那正是用户最可能撞上长时间停顿的时刻。

改法

项 变化
新配置 firstFoldRatio preset 里的键(PRESET_KEYS 16 → 17);范围 0 – 0.95,默认 0.30
生效范围 只作用于本会话第一次 fold:首折阈值 = min(窗口 × 0.30, pressureBudget);之后回到 thresholdRatio(0.45)
不受影响 overflow 分支照旧(它有自己的触发条件);若首折阈值 ≤ retainTokens ⇒ 回退正常阈值(不会压到留不住尾巴)
preset 块格式 engineBlock 增 firstFoldRatio 行(21 → 22 行 / 15 → 16 键);rooms 的 preset 块 golden 已同步

测试 +7(505 → 512,engine-harness 32/0):首折触发 / 第二次回到正常阈值 / 配 0 即关闭 /
边界(0.95、≤ retainTokens 回退)/ fold 次数最多 +1 / retain 守卫 / overflow 不受影响。
变异自检:首折分支失效 / 首折比例用到每一次 / 默认改成 0 —— 三条逐条红。

验收口径的更正(同版内,4dd5f0b):原写「fold 次数不增加」,这条本身不成立 ——
首折提前意味着同一会话可能多一次 fold。测试名改为「fold 次数最多 +1(首折提前一次)」,断言未动;
docs/方案-持续优化.md 的 S8 节写明了为什么「不增加」不可能成立。代码零改动。

连带调整:test/real-fold-trigger.mjs 的 ① 改用 50K(首折阈值已降到 60K,旧的 60K 用例会直接触发 fold,判据随即失效)。

验证

  • 全量:512 passed / 0 skipped / 0 failed(43 个测试文件;505 + 7)
  • engine-harness 32/0 · config-schema 11/0 · ui 54/0 · rooms 15/0 · release-hardening 15/0
  • verify-install 16/16
  • npm pack --dry-run --json:71 文件(数量不变)

未验证

  • 首折削峰是引擎级/夹具级证据,没有在真机上量的收益 —— 「首折更早、单次更小」的方向性成立,
    但「用户实际感知的停顿降低了多少」没有观测数据。
  • firstFoldRatio 只经 preset 与单测覆盖,没有在真实会话里跑过非默认值。

1.11.11

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 09:26

修 check-maintenance 的一个假红:git status --porcelain 的首行空格被 .trim() 吃掉

现场:终审时 check-maintenance 报 8 通过 / 1 失败,而工作树其实只有 evidence/bench-2000.json 这一个
本该被豁免的噪声文件 —— 也就是说,它把「只改了 evidence」误判成「改了代码没 bump 版本」。

根因(一个字符的事):git status --porcelain 的首行形如 M evidence/x.json ——
状态码 X 位是一个空格。旧实现把整段 stdout .trim() 了,首行的那个空格被吃掉,再按固定列 slice(3):

" M evidence/x.json"  → trim → "M evidence/x.json" → slice(3) → "vidence/x.json"   ← 少了 v

于是 evidence/ 过滤失效 ⇒ 假红。(后续行没有行首空格问题,所以只有第一行会歪 —— 这类 bug 极难靠肉眼发现。)

改法

  • 新增 gitRaw():返回未 trim 的 stdout,专供解析用;git() 保持原来的 trim 行为(其余检查不变)。
  • 脏文件改用 git status --porcelain -z + 新增 parsePorcelainZ():按 NUL 切记录,
    每条记录是 XY <path>,逐条 slice(3) 取路径;rename/copy 记录后面紧跟一个「原路径」token
    (没有 XY 前缀),解析时跳过。
    用 -z 还顺带解决另一个坑:普通 porcelain 会给含空格 / 非 ASCII 的路径加引号并转义
    ("a b.txt" 之类),-z 给的是原始路径,不用反解义。

新增 test/check-maintenance.test.mjs(4 条回归,全部真跑脚本)

用例 钉住什么
基线 干净夹具必须 9/0
首行空格 脏 evidence/bench-2000.json 必须仍 9/0(这就是那个假红的原样复现)
不放过代码 脏 src/ 仍必须红(不能为了 evidence 把代码变更也放过)
可恢复 git checkout -- evidence/ 之后回到 9/0

验证

  • 全量:505 passed / 0 skipped / 0 failed(43 个测试文件;501 + 4)
  • check-maintenance 专项 4/0 · release-hardening 15/0 · verify-install 16/16
  • npm pack --dry-run --json:71 文件(数量不变;test/ 不进包)

未验证

  • 假红只在 check-maintenance 里修了;别的脚本若也用 .trim() + 固定列解析 --porcelain,可能有同样的歪一字符问题 ——
    本次没有全仓排查(grep 过一遍,只有这一处按列切)。

1.11.10

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 09:08

修一个会悄悄吞掉备份的真 bug:回灌备份按会话分子目录

现场:migrate-dangling --merge-sidecar --apply 的备份目录原本是每次运行一个(merge-<iso>/),
同一次 mergeMany 里的多个会话共用同一个目录 ⇒ 后一个会话的 graph.json / MERGE.json
把前一个覆盖掉。真实规模演练时 48 个会话最后只剩 1 份备份,而且没有任何报错。

改法

项 变化
mergeBackupDir() 新增 session 参数:备份写到 <run>/<session>/(会话名做 [^A-Za-z0-9._-] → _ 清洗);不传 session 时仍是原来的 <run>
mergeMany().backupDirs 用 uniqueBackupDirs() 去重 —— 同一次运行里 N 个会话共用同一个 run 目录,汇总列表只留一条(perSession[].backupDir 仍是各自的精确路径,不受影响)
mergeSidecar / mergeMany 的 JSDoc 明确写出:merge 不是字节级还原 —— updatedAt 会被刷新、stateIndex 可能与原图不同。要逐字节恢复,请用 apply 的整目录备份 + rollback

测试 +3(498 → 501)

  • B1 uniqueBackupDirs 去重:同一 run 目录只出现一次,顺序保留
  • B2 同一 root 的两份备份都不丢(回归上面那个「48 会话只剩 1 份」的 bug)
  • B3 非字节级还原:行为断言(updatedAt 刷新后与原图不同)+ 文档断言(JSDoc 里写了「不是字节级」)

变异自检:不去重 / 不分子目录 / 删掉「非字节级」说明 —— 三条逐条红。

验证

  • 全量:501 passed / 0 skipped / 0 failed(42 个测试文件;498 + 3)
  • migrate 24/0 · release-hardening 15/0 · verify-install 16/16
  • npm pack --dry-run --json:71 文件(数量不变;只改 src/ 与 test/)

未验证

  • 备份修复只在夹具上验证;真实库的 merge 仍未执行过(真实 sessions/** 的 apply / merge 都需主人授权 + 停插件窗口)。
  • 「非字节级还原」只是把差异写清楚,没有提供「让 merge 变成字节级」的选项 —— 需要逐字节就用 apply 备份 + rollback。

1.11.9

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 08:55

发布脚本新增门禁:CHANGELOG 写的「N 个测试文件」必须等于事实

这条门禁上线当天就抓到了真实漂移:CHANGELOG 从 1.11.5 起一直写着同一个旧数字(37),
实际早就是 42 —— 连续几个版本没人对过账(它不影响构建,所以从来不会自己叫)。

新增(scripts/release-lib.mjs,纯逻辑、可单测)

导出 行为
declaredTestFiles(section) 读「N 个测试文件」;没写返回 null;容错 **42** 个测试文件 / (42 个测试文件;493 + 7) / 42个测试文件
checkTestFileCount(section, actual) 三态:一致放行 / 不一致给出 {declared, actual, reason} / 无声明放行

scripts/release.mjs 体检 ②b:当前版本那一节的数字 ≠ test/*.test.mjs 实际数 ⇒ 红,--yes 也不动手;
没写这句不报错(允许不声明)。红文案同时给出两个数与「怎么数」(含历史版本数法):

· CHANGELOG 写的「N 个测试文件」与实际 42 个不符 —— 改了测试文件就必须同步这句(或删掉它,脚本不会因此报错)。
  实际值怎么数:ls test/*.test.mjs | wc -l(看历史版本:git ls-tree -r --name-only <tag>^{commit} test/ | grep -c '\.test\.mjs$')

顺带修掉两个「只在预演期才撞到」的发布脚本缺陷

写这条门禁时跑 dry-run,脚本直接崩了,顺藤摸出两个真缺陷(都不影响发版那一刻,但会让「已发版本上再跑一次预演」变成堆栈):

  1. tag 已存在时 dry-run 崩:git rev-parse -q --verify refs/tags/x^{commit} —— Windows 上 execSync 走 cmd.exe,
    ^ 是转义字符 ⇒ 被吃成 {commit},命令失败抛异常(手动在 Git Bash 里跑同一命令是好的,极难从表面看出)。
    改用 git rev-list -n1 refs/tags/<tag>(无 shell 元字符,annotated / lightweight 都剥到 commit)+ try/catch。
  2. 「tag 指向别的 commit」误报:给 ls-remote 传精确 pattern 时不会输出 ^{} 剥皮行 ⇒ 拿到的是 tag 对象 sha(≠ commit);
    而且判据原本是「≠ HEAD」,但「发过之后 main 又往前走了」是正常态。改为列全部 tag 再在 JS 里过滤剥皮行,
    判据改成「既不是 HEAD 也不是 HEAD 的祖先」(git merge-base --is-ancestor)才报。

验证

  • 全量:498 passed / 0 skipped / 0 failed(42 个测试文件;493 + 5 条 release-hardening)
  • release-hardening 15/0 · verify-install 16/16
  • npm pack --dry-run --json:71 文件(test/ 不进包,数量不变)
  • 三态端到端(临时克隆、版本 9.9.9):声明 37 / 实际 42 ⇒ exit 1;无声明 ⇒ exit 0(附跳过说明);声明 42 ⇒ exit 0
  • 仓库记录更正:1.11.8 节的「37」已改为「42」并在原处注明「发布时误写;npm 上已发内容无法改」

未验证

  • 门禁只校验当前 package.json 版本那一节;历史各节不追溯。
  • 没把「仓库自洽」做成单测:若在测试套件里断言「当前版本的数字 == 实际数」,每加一个测试文件都会红到下一版
    (已发布那一节是冻结的)⇒ 这条只在发布时才对得起来。

1.11.8

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 08:37

sidecar 现在能回灌:归档掉的悬空边可以放回图里

1.11.1 起 migrate-dangling --apply 把 settled residue 归档到 graph.dangling.json(sidecar),
但一直没有回灌工具 —— 报告里写「字段齐全、可回灌」,实际只能靠人手写 JSON。这一版把这条路补完。

新增(R14 src/rooms/migrate.mjs)

导出 作用
readSidecar(file) 读 + 校验:format / version / 非空 edges / 每条边必须有 id / 会话匹配;坏文件一律拒绝且不动图
mergeSidecar(graph, sidecar, opts) 按 edge.id 合并;同 id 跳过、不覆盖;保留 sidecar 原字段;原子写 graph.json + 重建 index.md
mergeMany(target, opts) 目标可以是文件或目录;批量
mergeBackupDir(opts) 回灌前先备份 graph.json / index.md 并落 MERGE.json
  • 默认 dry-run:只有 --apply 才写盘;幂等 —— 二次 merge 0 条命中且不写盘。
  • CLI:--merge-sidecar=<file|dir> [--apply] [--backup-dir=] [--json];有错误 exit 1。

CLI 摘要(scripts/migrate-dangling.mjs)

  • 修掉 main() 里 merge 分支的重复执行链(只留 mergeMany,错误 exit 1),人类摘要移到输出链。
  • 无 --json 时打印:标题 / target / sessions|merged|skipped / 逐会话行 / backup 目录 / errors / dry-run 或已回灌提示;
    --json 仍是纯 JSON(可 JSON.parse)。
  • mergeMany:目标不存在时直接报「目标不存在」(不再默默去读 ./graph.json)。

测试 +7(486 → 493)

  • M1 回灌与备份 · M2 幂等 · M3 五类拒绝(format/version/空 edges/边无 id/会话不匹配)· M4 同 id 不覆盖 · M5 mergeMany 批量
  • C1 真跑 CLI 六态(人类摘要 / --json / apply 备份 / 幂等 / 坏 version / 目标不存在)· C2 结构回归(merge 分支恰好 2 处)
  • 变异自检:覆盖已存在边 / dry-run 写盘 / 不校验 version / 执行链重复分支 / --json 走人类摘要 / 删人类摘要分支 —— 逐条红

验证

  • 全量:493 passed / 0 skipped / 0 failed(37 个测试文件)
  • migrate 21/0 · rooms 15/0 · verify-install 16/16
  • npm pack --dry-run --json:71 文件(数量不变;只改 src/ scripts/ test/)

未验证

  • 回灌没有对真实库跑过 —— 全部证据来自夹具;真实 sessions/** 的 apply 仍需主人授权 + 停插件窗口。
  • 回灌只按 edge.id 去重:同一个「边」若在 sidecar 里 id 不同(历史重复归档),会被当成两条各插一次。

1.11.7

Choose a tag to compare

@MYCF711 MYCF711 released this 31 Oct 08:12

真实测试与门禁都不再「靠输出猜」

问题:两个门禁的判据都在猜,而不是问退出码。

  1. test/all.mjs 用正则 /failed|✗/ 扫子进程输出判失败 —— 测试名里可能字面含 ✗
    (例如 ✓ S10c-3 manifestError 进 ✗ 详情…)⇒ 把绿当红;反过来,子进程真失败但输出没命中关键字 ⇒ 漏报。
  2. test/real-fold-trigger.mjs 无论 4/5 还是 5/5 都 exit 0 ⇒ 它作为门禁时几乎没有意义。

改法

文件 变更
test/all.mjs 删掉 /failed|✗/ 正则判据,只按子进程退出码;超时 / 起不来 / 无退出码一律算红(不许当成「没输出=没事」);导出 STEPS / stepPassed / runStep / allPassed 供单测直测
test/real-fold-trigger.mjs 加退出码:4/5 ⇒ exit 1;依赖未提取(跑不了)⇒ exit 2 表示「未运行」而不是「通过」
同上 · ⑤ 溢出自救 没有可压内容时记 N/A(不计失败)—— 先注入新内容让它有东西可压;⑤ 判红不再是「环境没给料」的假失败
同上 · usage 对账 打印模型返回的 usage(prompt / completion / total / calls)并累加落盘,真实调用的成本可对账
同上 · 预清理 跑前清空 graphs-trigger —— 旧状态会让 ② 的 growth gate 误拦,第二次跑必红

新增 test/test-gate.test.mjs(5 条):exit 0 但输出含 ✗ 不得误红 / 非 0 必红 /
起不来 · 超时 必红 / 4 通过 1 失败 必败 / 结构回归(STEPS 与导出面)。

变异自检:stepPassed 恒 true ⇒ TG-2/TG-3 红;判据退回正则 ⇒ TG-5 红。

验证

  • 全量:486 passed / 0 skipped / 0 failed(37 个测试文件;481 + 5)
  • node test/all.mjs → exit 0(5 个步骤全绿:单测 / 挂载校验 / 真实模块 / 真实事务 / 性能基准)
  • node test/real-fold-trigger.mjs → 5/5 exit 0,usage 已打印
  • verify-install 16/16 · bench 2000 351 tokens
  • npm pack --dry-run --json:71 文件(数量不变;test/ 不进包)

未验证

  • real-fold-trigger 依赖真实模型(走 magpie 127.0.0.1:3425)与已提取的模块目录;缺依赖时它会 exit 2(未运行),
    这是有意的语义 —— 但也就意味着它在别的机器上可能根本不跑,不能当「任何环境都能复现」的证据。
  • all.mjs 的「超时必红」用 300s 默认超时,没有真的等满 300s 去验证(用的是注入假 exec 的单测)。