16 Commits

Author SHA1 Message Date
771a053eea docs(CL-1F): restore A1 semantics and cite V7 logs for G1
A1: revert to the 06155d4 wording (recallBlocks stays synchronous, a new
async recallBlocksHybrid is added). The previous basis claimed the async
pre-step handler justified making recallBlocks async, which the code
contradicts: lib/recall.js:229 exports a sync recallBlocks with zero awaits,
and lib/recall.js:613 is the async wrapper.

G1: replace the stale count with the readings already present in the V7
evidence logs (/tmp/v7-order-20261101-173334-t1..t3.log = npm test,
225/225/0; c1..c3.log = npm run check, 12/12), noting that phase A is a
subset of the whole-suite run. No new test/check run was performed.
2026-11-01 17:52:52 +08:00
7fc3d46b9a docs(CL-1): revert V2 documentation changes 2026-11-01 17:41:52 +08:00
d57c9dc798 feat(memory-b): port V2 non-exact fusion weights
Also clear three stale counts the release train authorised: criteria A1
wording (sync + hybrid), criteria G1 counts (173/12), README test-site count.
2026-11-01 12:01:22 +08:00
99d0670ebe fix(memory-b): 返工 ③④ —— trigger 直判据 + skipped_dim 不再是假字段
③ trigger 补直写判据(此前只有间接覆盖)
你的复核准确:摘掉 trigger 的 WHEN 会让 F3/D3 红,所以「间接有守卫」——
但没有一条盯 raw-writer。而 trigger 存在的全部理由就是防 raw-writer
(修复脚本、迁移、运维手敲 sqlite3)。putBlock 自己就能把列写对,
所以真实风险是「trigger 全没了也没人发现」。

MIG5 三条:
  - raw UPDATE 把 vector 改成长度不符 ⇒ vector_dim 被拉回真值;清空则归 0
  - raw INSERT 带错误 vector_dim ⇒ 被 trigger 覆盖为真值
  - MIG5† 分别 DROP 两个 trigger,各自断言「谎言能存活」
真变异:从 DDL 里删掉整段 trigger ⇒ MIG5 前两条红(MIG5† 仍绿,因为它
主动 DROP,不依赖 DDL 是否存在 —— 这是两者分工正确的标志)。

④ skipped_dim 恒 0 是假字段,选 (a) 补维度过滤
你的诊断对,且比我的注释更狠:一个永远不能非零的字段,配合
`typeof === 'number'` 的断言,把 F4「维度失配与指纹失配分开计数」
做成了摆设。这与更早那处 assert.equal(skipped, 2) 是同一类。

实现:
- searchFts / 包含兜底都携带 dimension(来自 vector_dim 列)
- recallBlocks 新增 dimension 选项,在**空间过滤之后**做维度过滤
  ★ 顺序有意:先空间后维度,保证一块同时 stale 且窄时只计一次(记 space),
    否则两个计数会重复计同一行、谁也加不出「被丢弃的块数」
- skippedDim 与 skipped 分别挂在返回数组上(F4 要分开)
- recallBlocksHybrid 把 space.dimension 注入 scoped,六个降级分支自动继承
- lastRecall.skipped_dim 改为读真实值

判据 F4b 四条:
  - 同指纹不同宽度 ⇒ skippedDim=1 且 skipped=0(不串桶)
  - 同时存在旧空间块与窄向量块 ⇒ skipped=1、skippedDim=1,不双计
  - last_recall.skipped_dim 能非零
  - F4b† 变异:删维度过滤 ⇒ skippedDim 归 0、窄向量被召回 ⇒ 红
实测:hits=[C 正常],skipped=1,skippedDim=1,两者都能非零。

一处测试构造的调整:窄向量块用 raw 写入而非 commit —— F8(正确地)
拒绝显式传错的宽度,所以「库里存在窄向量行」在现实中只能来自
历史数据或修复脚本,与 trigger 那条判据的假设一致。

文档
- criteria: F4 补「两个都能非零」与恒 0 字段的教训
- dev-notes 增第 6 类「恒为 0 / 恒为真的字段」,含自查问句
  「这个字段的取值集合里,有没有一个能让我这条断言失败?」

验证
- npm test 162/162(155 + MIG5 三条 + F4b 四条)
- npm run check 10/10
- 干净副本实数见后续消息
2026-10-30 17:07:19 +08:00
160b5b5139 fix(memory-b): 三项验收整改 —— 去写死数字 / F8 硬报错 / blockStates 去 JSON.parse
一、skipped 断言不再写死数字
原 assert.equal(hits.skipped, 2) 属于「手抄数字」——与刚删掉的 README 计数同类:
fixture 一改就悄悄不再描述事实。
改为从构造库现算:expectedSkipped = counts.unknownSpace + counts.spaceMismatch,
并先断言 expectedSkipped > 0(防止变成「0 == 0」的假通过)。

二、F8 从「事后统计」改成「写侧硬报错」
原状:写侧维度校验只降级成「不嵌入」,坏向量仍能落库,靠 blockStates
的 dimensionMismatch 事后兜住 —— 那是 accounting,不是 validation,
与 criteria:165 的「硬报错」不符。

实现:putBlock 新增可选 expectedDimension,声明了宽度且不符 ⇒ 抛错不落库。
commit 在「调用方显式传 vector」时传 expectedDimension。

★ 两个入口必须分清,否则会与 W4 打架:
  - 调用方 bug(显式传了宽度不对的向量)⇒ **必须抛**,否则它以
    「块在、召回不了」的形式沉默很久
  - provider 异常(embedding 返回宽度不对)⇒ 按 W4 降级为不嵌入,块仍写入
    (把上游抖动抛给用户 = 让一次 provider 故障变成写入失败)
判据含三例:显式传错宽度抛;显式传对宽度通过;provider 返回错宽度降级不抛。
F8† 变异:把宽度检查改成恒不触发 ⇒ 坏向量落库 ⇒ 判据红。

三、blockStates 不再解析向量(性能账)
实测 2000 块 × 2048 维:
  改前 354ms/次(其中 255ms 是纯 JSON.parse)
  改后 4ms/次(约 90×)
而 renderIndex 每轮都调它 —— 这是挂在 pre-step 热路径上的固定成本。

做法:memory_blocks 新增 vector_dim 列(维度是行事实,不是负载内容),
  - putBlock 写入时一并写
  - 迁移时为存量行回填一次(只此一次,不在读路径)
  - 加两个触发器保证 vector 与 vector_dim 永不脱节:
      ★ 理由:既然这一列的存在就是为了「不解析负载」,那每个写入者都背上了
        保持一致的义务。漏一个写入者(修复脚本、测试 fixture、未来迁移)
        就产生一行「维度是谎话」的数据 —— accounting 报它 active,
        而召回静默跳过它。用触发器让这个状态**不可表示**,而不是只劝阻。
  - 静态判据:blockStates 函数体内不得出现 JSON.parse(防以后「顺手简化」改回去)

四、embedFailure 记账
不进四态(它是单次调用事件,不是库状态),criteria 已补说明:
四态是库状态、从行派生;embedFailure 属于 recallBlocksHybrid 的 degraded,
由 W 组覆盖。塞进库状态会让「同一时刻读两次得到不同数字」,正是 D4 要避免的。

顺带修:F4† 的变异锚点因 blockStates 改写而失效(withMutatedCopy 显式报
「anchor not found」——这条守卫按设计响亮失败,没有静默变绿)。

验证
- npm test 148/148(143 + F8 四条 + 性能守卫一条)
- npm run check 10/10
- 性能实测见上
- 干净副本实数见后续消息
2026-10-30 16:29:05 +08:00
48c52497c2 feat(memory-b): F6 三段口径固化成判据 + A1b 降级记账
裁定:F6 选 (a),措辞改写,不留互斥读法
原措辞「仍可按文本调出、不参与向量召回」读起来像互斥。实际是三层:
  「按文本调出」= store 级无空间浏览,**不是生产召回路径**。

不改 F2 去迁就 F6 字面自洽的理由:F2 是 fusion.go:495 同款安全缺陷修复,
优先级高于 F6 的措辞。所以 F6 改成三段并列,每段一条断言,
**同一个用例内全断言**:

  数据层:指纹变更后旧块仍在库中 —— 块数不减、按 fp 分组两行都在
  浏览层:store 级不带空间参数的文本查询仍能取到旧块
  召回层:带 fingerprint 的生产召回路径必须把旧块过滤掉(F2 承担)

为什么必须一个用例:只断言「过滤掉了」会漏掉数据被误删
(删库在召回层看起来就是「过滤正确」);只断言「能调出」会把 F2 挡回去。

F6 判据 3 条
  F6          三段全断言 + 反向对照(当前空间块两层都能取到,
              防止断言因为「什么都找不到」而假成立)
  F6†data     变异:putBlock 顺手 purge 其它空间的行走道 ⇒ 数据层断言红
  F6†recall   变异:去掉空间过滤器 ⇒ 召回层断言红
两条变异各自只咬自己那一段,与 F6†data/†recall 的分工一致。

★ F6†data 的变异写法踩了三次坑(据实记)
  1. 只写 form(没真改代码)—— 那不是变异,是自测
  2. 注入 SQL 里的转义引号把测试文件写成 SyntaxError
  3. 最终用 String.fromCharCode(39) 拼引号 + 数组 join 构造注入文本,
     避开嵌套模板字符串的转义地狱

裁定 2:A1b 降级要记账
A1b 原先逐字节锁死 recallBlocks 源码,F2 合法地要改它,已降级为
「不传 fingerprint 时行为不变」。但降级后的 A1b-behaviour 只跑 golden
**前 5 个** case,变异 A(非精确带分数微调)下它可能是绿的。
已在判据注释写明:**结论不由 A1b 单独给出,由 A2 兜**
(A2 走全部 45 例、比精确分数);A1b-behaviour 只是烟测,不是权威。

同步进 memo:docs/dev-notes-mutation-criteria.md 增第 5 条
「判据降级时,覆盖面必须一起交代」—— 这不是失效模式,是记账纪律。
自查问句:这条判据降级后,原本由它兜住的结论现在由谁兜?
答案必须是一个具体的、覆盖更广的判据名。

验证
- npm test 131/131、npm run check 10/10
- 干净副本(git archive + link-dsh)实数见后续消息
2026-10-30 15:39:03 +08:00
425c441470 feat(memory-b): F5 固化为 fail-closed 判据 + F2 符号路空间过滤
裁定 1(F5):fail-closed,写侧直接拒收
docs/phase-b-criteria.md §0.2 从「待拍板」改为「已定」:
  无名向量的块直接拒收,比放过再靠下游兜更能守住空间隔离;
  安全语义优先,**不为 HA 的疑似缺陷同构**
  (HA 的 block_recall.go:140 三个 && 让空 fp 被放行,这是要修正而非复刻的行为)。
落点分两层并写明「缺一层都不算落实」:
  写侧 putBlock 抛错(已实现)
  读侧存量行归 unknownSpace、不参与向量召回(F 组)

F5 判据(5 种输入形态 + 变异)
  省略 / 空串 / undefined / 数字 / null 一律抛错,且都不落库
  合法形态仍工作(无向量无 fp;有向量有 fp)
  F5† 变异:删掉写侧守卫 ⇒ 无名向量被接受 ⇒ F5 必红

顺带修一处:lib/store.js 的 vectorFP 加 String() 归一。
原先变异摘掉守卫后会以 SQLite bind 错误崩,掩盖了真正的断言失败
(变异因为错误的原因变红 = 不算证据)。

裁定 2(F2):先过滤,后计数
F2 —— 空间过滤必须覆盖符号路,不只向量路
- store.searchFts 现在返回 fingerprint / hasVector,
  两条 intake 路径(FTS 与包含兜底)都携带身份字段
  ★ 兜底路径第一版漏了这两个字段,导致过滤器看到 hasVector === undefined
    而放行 —— 正是「过滤器只覆盖两个入口中的一个」那种 bug
- recallBlocks 新增 fingerprint 选项,在 pool 构建后统一过滤
  刻意**不复刻** HA 的三连 &&:本仓 fail-closed,
  带向量但无法命名空间的块一律排除(§0.2 裁定)
  无向量的块不受影响(它们本来也不参与向量路,文本仍可检索)
- recallBlocksHybrid 在函数入口解析一次空间并注入 options,
  六个降级分支全部经 scoped 调用 —— 逐调用点设置会在加第七个分支时漏掉一条

F2 判据(4 条行为 + 2 条独立变异)
  F2   符号路命中旧空间块 → 被排除
  F2   不传 fingerprint → 过滤器不生效(phase A 行为不变)
  F2b  有向量但 fp 为空 → fail-closed 排除(HA 此处会放行)
  F2   hybrid 在所有降级路径上应用同一过滤
  F2†  删掉过滤器 → 旧空间块回来
  F2†b 让 searchFts 不携带身份 → 过滤器失明(过滤只剩一个入口)

A1b 的调整(必须说明)
A1b 原先逐字节锁死 recallBlocks 源码,而 F2 合法地需要改这个函数。
改为守它真正要守的东西:**不传 fingerprint 时行为不变**
  - 静态:过滤器必须 gated 在显式传入的 fingerprint 上
  - 行为:golden fixture 前 5 例逐字段一致
依据:golden 语料 12 个块全部没有 fingerprint,
所以 F2 对 phase A 是纯增量;A2 仍全绿即为证。

验证(干净副本:git archive → 空目录 → link-dsh → 自跑)
见下一条消息附实数。

未 amend / 未 force push。
2026-10-30 15:29:23 +08:00
0e65dfee64 fix(check): checkpoint 全废时 runner 仍报通过 + 补回丢失的覆盖
缺口(评审复现,我独立复现确认)
上一条替换旧 README 判据时,把它的第二条断言一起带走了。
旧 test 同时守两件事:
  1) README 计数与实现一致      已判定不值得守,删
  2) checkpoints.js 至少注册一条 值得守,跟着 1 一起消失
grep 全仓:没有任何东西守 2。

实测后果(很安静)
把 helper 换成空操作、调用点全保留:
  npm run check  ->  0/0 checkpoints passed, exit=0
  npm test       ->  120 条全绿
即 checkpoint 全废、runner 报通过、测试一条不红。

修法:守结果,不守成因
scripts/checkpoints.js 结尾(if (failed > 0) 之前):
  if (results.length === 0) {
    console.error('no checkpoints were registered ...')
    process.exitCode = 1
  }
放 runner 而不是补回静态计数的理由:静态计数只能抓 helper 改名;
results.length === 0 抓的是结果,改名、被 stub、条件跳过都会红。

但 process.exit(0) 能绕过任何脚本内检查,所以另加一条外部断言
(test/phase-b-g-static.test.js)三层:
  - declared > 0                    helper 没被改名
  - 源码含 if (results.length === 0)
  - 该守卫确实设置 process.exitCode = 1
分工:runner 守结果,测试守 runner 还带着那道守卫。

实测
  0 条注册(stub)    -> no checkpoints were registered, exit=1
  条件跳过            -> 同上, exit=1
  正常态              -> 10/10 passed, exit=0
  变异:删 runner 守卫 -> not ok 6
  变异:helper 全改名  -> not ok 6

docs/dev-notes-mutation-criteria.md 增第 4 类
判据被替换时,旧判据的覆盖静默消失
- 形态:一条 test 守两件事
- 实测后果:0/0 passed / exit 0 / 120 全绿
- 守结果不守成因的四类成因对照表(含 process.exit(0) 是拦不住的那格)
- 自查问句:这条判据除了名字里那件事,还顺手守了什么
- 探针经验:不要用注释掉调用去验零注册,会变成 SyntaxError

验证
- npm test 120/120、npm run check 10/10
- npm test 3 次、npm run check 3 次(带 exit code 核对)全绿
- 无 .orig / .rej / /tmp/mut-* 残留
- 未 amend / 未 force push
2026-10-30 14:44:09 +08:00
fc5fe11590 docs: 去掉 README 计数并在判据侧禁止它 + 变异判据方法论 dev-notes
1. README 计数去掉(不留守卫)
去掉「119 tests」与「10 checkpoints / 10/10 checkpoints passed」。
理由按裁定:一个没有守卫、被手改过五次的数字是负资产,
它给人的确定感是假的;加守卫要翻倍套件时间,不值。

守卫反过来断言「README 不得出现计数」:
  /\b\d+\s+tests\b/i
  /\b\d+\s+checkpoints\b/i
  /\b\d+\s*\/\s*\d+\s+checkpoints passed/i
三种写法实测都能抓住(120 tests / 12 checkpoints / 12/12 passed)。

顺带处理一处同类:原文「(6 tests)」是手抄的测试数,删;
「12 blocks × 15 queries = 45 cases」是 fixture 自身内容量,也删并改为
指向 fixture 文件 —— 由文件决定的东西不该在文档里再写一份。
(README 现在的口径:表格说「验什么」,`npm test` / `npm run check` 说「多少」。)

2. docs/dev-notes-mutation-criteria.md(新)
把三轮踩出来的方法论落成仓库文档,README 的 Verify 段加指针。
三类失效模式,每条都给「形态 / 实测证据 / 正确形态 / 自查问句」:

  ① 自测型假变异(犯四次:W11† / W11d†a / W11d†b / W12†)
     变异在 test/ 里自己拼替身,一行 lib/ 没碰 ⇒ 真回退下保持绿
     实测:M-a / M-b 下 W11d†a / W11d†b 均绿,只有 W11d 红

  ② 清单写死(W11f)
     手写 MODES 漏掉新 case;future_mode_a/b 测的是「掉进 default 的」,
     永远测不到「有人新写了一个 case」
     实测:植入 brand_new_mode(零命中时撒谎)→ 写死版全绿,扫描版红
     并记下边界:自动推导不能替代「提取本身也要被守卫」,
     所以还需要 declared.length >= 4 + W11f-a(guards the guard)

  ③ 变异共享状态并发撞车(我自己引入的)
     就地改写 lib/*.js,而 node --test 并行跑文件 ⇒ 别的文件读到中间态
     实测:约 1/30 偶发失败,症状像 flaky 断言;A1b / G2b / G5 这类
     读源码的判据会读到被改坏的中间态
     修法:在 lib/ 副本上变异(复制 lib + package.json + 软链 node_modules),
     finally 清理,锚点失配显式失败

文末给通用自查清单 9 条 + 一句话记住三条。

验证
- npm test 119/119、npm run check 10/10
- 连跑 6 次全量 0 失败
- 无 .orig / .rej / /tmp/mut-* 残留
- 未 amend / 未 force push,199553f 之后是新提交
2026-10-30 14:36:41 +08:00
d972a2d135 fix(memory-b): W11f —— aborted / default 零命中时不再承诺条目
接你备好的四个 patch(ready-lib / ready-test / README / docs),
应用前逐个审过内容,dry-run 干净,应用后 md5 与你给的期望值一致:
  lib/recall.js          c8fdffd25d5340291fadf6cb26e1741f
  test/phase-b-h.test.js c06685abb7672fa806ca37e0d1cb8947
  README.md              3c183b045dd7de4a54338ed9d01b3738

问题(我上一轮修 embed_error 时只修了那一个分支)
  aborted 与 default: 在零命中时仍说「matches below are …」,而下面没有条目。
  aborted + 零命中不是边角 —— 用户点停止、且这条查询本来就查不到东西,
  是取消最常见的形态。
  default: 更糟:它是未来任何未知模式**唯一**会走的路,
  最不该在这里开始编。

修法(补丁采用)
  把「有没有条目」只问一次,四个分支共用:
    const nothing = !Array.isArray(hits) || hits.length === 0
  default 用「nothing was found, so treat this as "unknown", not "absent"」——
  把「没搜到 ≠ 没有」这条口径落在**兜底分支**上,
  而不是每加一个模式才记得补一次。

W11f 判据:表驱动、双向
  遍历 [undefined, no_embedder, embed_error, aborted, vector_ready_unused,
        future_mode_a, future_mode_b]
  零命中:notice 非空时不得含 matches below|below are|entries below
  有条目:notice 非空时必须指向条目,且不得说 found nothing
  第二条以前完全没人管 —— W11/W11c 只查「出现 FAILED / cancelled」,
  不查它是否指向了真实存在的条目。

两条变异各自独立(实测,不是「都红就算数」)
  M-A 只删 aborted 分叉
    not ok 57 - W11f
    not ok 58 - W11f†
    ok     59 - W11f†b        ← default 那半仍绿
  M-B 只删 default 分叉
    not ok 57 - W11f
    ok     58 - W11f†         ← aborted 那半仍绿
    not ok 59 - W11f†b
  若两条变异下红的是同一批,那就是重复计数(W11d†a/†b 的老毛病)。

验证
- npm test 118/118
- npm run check 10/10
- 连跑 8 次全量,0 失败(按你的要求超过 5 次)
- 变异 runner 零残留:/tmp/mut-?????? 前后均为 0
  (/tmp/mut-probe 是 10:17 的历史副本,与本轮无关,未动)

未 amend / 未 force push,80d188e 之后是新提交。
2026-10-30 14:00:07 +08:00
80d188efc1 fix(memory-b): P0 no_embedder 逐轮注入 + 变异改为真改 lib/(并修掉自引入竞态)
P0 —— no_embedder 每轮注入(随 82b7546 进了远端)
复现(真链路,config 不给 embed = phase A 出货默认,10 个普通回合)
  10/10 回合被注入,共 2130 字符,均值 213 字符/次
注入原文是恒定、无条目的两行记忆头。

根因:W11d 拆两处短路时把 no_embedder 一起放行了。
问题不在「该不该说」,在「不该每轮说」——它是一条永久配置事实,
renderIndex()(systemPrompt.context)已经每轮在讲。
代价不只是 token:每轮一段恒定无条目的记忆头,模型的正确反应是学会跳过
该段,等真降级出现时也不看,正好废掉 W11 的用意。

修法
- no_embedder 返回 ''(注释写明理由与被测数字)
- 顺手修文案矛盾:embed_error + 零命中原来会说「matches below are…」
  但下面没有条目。现在分两种措辞:
    零命中 -> "FAILED … and the lexical fallback found nothing"
    有命中 -> "FAILED …; matches below are lexical/symbolic only"
- aborted / embed_error 不动

判据 W11e:默认配置(无 embedder)+ 零命中 ⇒ 注入必须为 ''
修复后同一探针:10 个回合 0 个被注入,0 字符
W11e 与 W11d 互为反向守卫:真·本轮事件要说,持续状态不该每轮说。

变异纪律(你指出的第四次同型,我认)
W11d†a / W11d†b 原来是测试里自己拼的局部函数,不碰 lib/ ——
真回退下它们保持绿,只有 W11d 变红。已删除,不留作计数。
换成三条真变异:W11e† / W11d† / W11d†ctx,
都真改源码、import 变异版、断言行为翻转。

★ 改真变异时我自己引入了竞态,并被抓到
第一版 withMutatedSource 直接在磁盘上改写 lib/recall.js 再还原。
node --test 是并行跑测试文件的,别的文件正在同时 import 它。
实测 1/30 左右偶发失败,症状看起来像「断言 flaky」,其实是 harness 竞态。
已改为 withMutatedCopy:整体复制 lib/ + package.json + 软链 node_modules
到临时目录,在副本上改、在副本上 import,完全不碰共享树。
验证:连续 30 次全量 0 失败;变异锚点漂移时显式报错(不会静默通过)。

文档
- README:W11e 的理由与实测数字、变异纪律(必须改 lib/,改 test/ 不算)、
  以及上面这条「变异共享状态不能与代码并发跑」的竞态记录
- docs/phase-b-criteria.md:W11e 进表、W11d 的变异列改为两条独立真变异、
  ★★ 变异纪律段(同型四次:W11†/W11d†a/W11d†b/W12†)

验证
- npm test 115/115
- npm run check 10/10
- 连跑 3 次 test + 3 次 check 全绿
- 另跑 30 次全量确认竞态已除(0 失败)
2026-10-30 13:34:18 +08:00
82b754661d fix(memory-b): W11d 两处短路 + W12 abort/失败分流 + 文档订正
W11d —— 你说得对,不是一行。两处短路各能独立吃掉提示
  ① lib/index.js contextForStep: `if (hits.length === 0) return ''`
  ② lib/recall.js renderRecall:  `if (!Array.isArray(hits) || hits.length === 0) return ''`

修法(按你给的形态):「有没有条目」与「有没有话要说」分开判:
    const notice = degradationNotice(hits)
    if (lines.length === 0 && notice === '') return ''
不写成「notice 非空就总出 header」——那样健康路径零命中回合每轮灌一行记忆头,
白烧 token 还教模型忽略这一段。

四场景实测(真 Context/SystemPrompt/ToolRuntime/真 adapter/真 http)
  A 降级+命中   -> empty=false failed=true  entries=true   54ms
  B 降级+零命中 -> empty=false failed=true  entries=false  51ms   ← W11d
  C1 健康+命中  -> empty=false failed=false entries=true    7ms
  C2 健康+零命中-> empty=true  failed=false entries=false   2ms   ← 正确沉默
与你实测四条一致。

W11d 判据 + **两条独立变异**
  W11d†a 只把 renderRecall 短路改回去  -> not ok 50 - W11d
  W11d†b 只把 contextForStep 短路加回来 -> not ok 50 - W11d
两条分别实测,证明「只修一处无效」,也证明每处都有人守着。

W12 abort 与 embed_error 分流(你上一条四项里的 ③)
原来两者都报 embed_error ⇒ 用户主动取消时提示「搜索失败」,会诱导无谓重试。
现在:options.signal.aborted 或 AbortError ⇒ 'aborted',其余 ⇒ 'embed_error'。
degradationNotice 新增 aborted 分支,措辞是「was cancelled」而不是「FAILED」。
W12 判据:两种标签、两种文案、必须不同。
变异(合并成 'embed_error')→ W10b / W11c / W12 三条同时红。
已同步 W10b/W11c/W11d 的断言(它们原假设 embed_error)。

② README:224 订正
`recall('etcd 端口', 5)` 已失效——第二形参变成 options 后,数字不是 {topK},
默认值生效。改为 `recall(q, { topK: 5 })`,并补 await(commit/recall 都是 async),
另加一段说明这个静默失效,以及「不 await 也能写」正是 W6 守着的脆弱假设。

④ docs/phase-b-criteria.md 补 W 组(原 0 命中)
新增 §W 表:W1/W5/W4/W6/W7/W8/W9/W9b/W10/W11/W11d/W12,
每条写「逼出它的 bug」与变异,并记下这组的共同教训:
判据用替身/短路/自造形状时,验的是替身而不是真代码。
(W9 假 embedder、W11d 两处短路互相掩盖、V3b 自造 2 段 batch 是同一形态。)

★ 你那条附带发现我也记下了:探针把成功响应写成 {vector:[...]},
而适配器只认 {embedding} 或 {data[0].embedding}(lib/embed/http.js:199-202),
于是「健康路径」实际一直在走 embed_error,C1 误报 FAILED。
自造 mock 时形状必须照适配器真读的字段抄,否则验的是自己的 mock。

验证
- npm test 113/113(108 + W11d/W11d†a/W11d†b + W12/W12†)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
2026-10-30 11:24:17 +08:00
eac9f0fca6 docs: phase B 开工前决策建议 —— 异步化回归风险可结构性消除
回应四条裁定与一个交叉点。不含实现。

核心结论:草案 A1「recallBlocks 改 async」是我图省事的设计偷懒,
把「要拿向量」和「要打分」混在一个函数里。切分后风险归零:

  recallBlocks(store, query, opts)              保持同步,签名行为不变
    新增可选 opts.vectorScores = Map<blockId, number>
  recallBlocksHybrid(store, embedder, q, opts)  新的 async 包装

实测调用面:生产代码 3 行(lib/index.js:124/140/277),
测试代码 0 行(test/store.test.js 5 处 + scripts/checkpoints.js 4 处
全部继续调同步版)。已实测确认 test/plugin.test.js:119 那个
`contextForStep = () => ''` 的同步桩在 async 化后仍兼容(await 非
Promise 值即其本身),所以那条变异测试继续有效。

建议把 guard 组 A1 改成「recallBlocks 保持同步且实现不变」,
并新增 A3:hybrid 在 embedder 为 null 时返回值与同步版逐字段相同
(含 why 字符串)——切分后最大的新风险是两个入口漂移,A3 钉死它。

四条裁定核对
- §2.1 endpoint 规范化:建议固定四条(去尾斜杠 / host 小写 /
  省略默认端口 / 去空白),不做 query 重排与路径归一。
- §2.2 四态:可执行,我草案的三态漏了「向量化失败」,裁定并进来更准。
  新增 D7:stats 默认不解析向量 JSON(用 length(vector)),维度从
  graph_centroid 取。修正一处我原稿的说漏——HA 为算 Dimensions 必须
  逐行 decodeVector(block_recall.go:303/313),所以「维度统计」与
  「不解析」互斥,只能靠中心表绕开。
- §2.3 centroid 补 stats 列:已列为第一件事。
- §2.4 失败降级:同意,并建议 mode 三值而非 bool
  (no_embedder / embed_error / all_mismatched 对用户意义不同)。

§3 建议把 embedder 焊成单方法窄接口(embedText / info / close),
加 G5 源码 grep 守卫:除 lib/embed/http.js 外不得出现 http/endpoint/
fetch/Bearer 等 provider 专属标识——机器可查,比约定强。

§4 交叉点:建议投完整 phase B,按 §1 切分后 5.5-6.5 人天。
但建议你先确认一件事:是否有真实使用中的 dsh 会话在产生块?
若无,可先投 2.5 人天的中间态(向量写入 + 四态可见,不改召回主路径)。

§6 记录本轮两处自我修正。

参考仓未改动。
2026-10-30 09:52:06 +08:00
0dfa6dcb27 docs: phase B 判据与变异规则草案(待批,不含实现)
decision 1(http 转发、不背 ONNX)与 decision 3(跳过不报错但失配计数
必须暴露到工具面)已落实到判据;decision 2 未回,全文按草案状态。

开工前必须先定的 4 件事(各附我的建议与代价):

§0.1 ★ HA 的 http 指纹派生式漏了 endpoint
     http_embedder.go:99 只取 "http:"+model+":"+dim。
     换 endpoint(换机器/A-B 服务/换 region)而 model 同名同维时,
     指纹相同 ⇒ 静默混用两个向量空间。
     建议扩展为含 endpoint hash;代价是换 endpoint 触发一次全量重嵌。

§0.2 ★ 空 fingerprint 的带向量块会被静默放行
     block_recall.go:140 的跳过条件是三个 && ⇒ fp 为空时不跳过。
     phase A 的块全都无向量,phase B 回填后「无向量」与
     「有向量但指纹空」是两种不同状态,混在一起用户无法知道怎么修。
     建议三态归类 + fail-closed(no_vector / no_fingerprint / stale / active)。

§0.3 graph_centroid DDL 与 HA 不一致(我的疏忽)
     少了 stats 列,而 HA 靠 meta.Anomalous 决定要不要启用中心化
     (centroid_store.go:205-209)。现在表是空的,改列名零成本。

§0.4 embedding 调用失败是否一并进计数器(与 decision 3 同构,建议纳入)

判据 6 组共 34 条(A 前置重构 / F 指纹空间 / V 融合 / C 中心化 /
D 降级可见性 / H http provider / G 守卫),每条带变异。
变异规则汇总出 12 条必须能红的,其中 M-V1(精确串从布尔降成权重)
是 HA 真实踩过的 bug(0.300 输给 0.534)。

另外三项判断:
- 增加 golden fixture 固化 phase A 召回结果,作为「加向量没顺手改符号路」的守卫
- memory_blocks 要加 vector/fingerprint 两列,phase A 的 DDL 已发布 ⇒
  建议 phase B 第一件事补 PRAGMA user_version 迁移框架(现在 0→1 一步,
  一天成本;等 L2 落地再补就贵了)
- 工作量从原估 3-4 人天上修到 6-7 人天(原估漏了 schema 迁移、
  stablemem_stats 工具、异步化带来的全量测试改动)

不含任何实现。参考仓未改动。
2026-10-30 09:49:07 +08:00
533f801c24 docs: 影子上下文方案在 dsh 上的可行性核实
结论:路线②(注册独立 agent 模式)可行;路线①(劫持原有信息流)
只是看着可行。

路线②可行 —— agentPresets 是一等机制
- ctx.agentPresets.register({id, plugins}) 可由任意插件调用
  (dsh-agent-preset/lib/index.js:18-26)
- preset 子树是独立 scope + 可选 isolate realm
  (dsh-agent-preset-registry/lib/types/mount.js:183-198)
- 已有实证:standard.patch.yml:66-82 的 isolate:{compaction:true}
  group 已经在替换 compaction 服务
- 因此「压缩时机」「不压缩」「压缩前抓原文」全部可控

路线①为什么看着可行
- agent/pre-step 看着能注入,其实注入即落盘:
  loop 随后 session.append("user/message", ...)
  (dsh-agent-loop/lib/index.js:1067-1073),注入内容成为真实历史
- llm/stream 看着能改最终请求,其实 loop 请求深冻结
  (dsh-llm/lib/types/index.d.ts:37-42),mutation throws
- 「不装 compaction-basic」看着能旁路,其实 auto:false 会连
  溢出恢复一起关掉 (dsh-compaction-basic/lib/index.js:827)

关键机制核实
- compaction 自动触发只是两个 ctx.on 监听器
  (dsh-compaction-basic/lib/index.js:839/:861),受 config.auto 控制
- 服务名同 realm 撞名抛错 (cordis/lib/index.js:814),换实现必须 isolate
- surface 支持整段 replace,但被遮蔽节点必须全部列进 sourceEventSeqs
  (dsh-session/lib/index.js:314-332)
- 日志 seq 严格连续 (dsh-session/lib/index.js:426),影子副本只能活在
  dsh 之外
- MessageSourceMap 可 merge 扩展,自定义字段 lossless-JSON 存活但不进
  wire(adapter 只投影 role/content)
- setFactory 独占 (dsh-agent/lib/index.js:431 + agent-loop:1590),
  自建 runner 需弃用 2023 行的 agent-loop

三处与路线无关的硬阻塞:surface 不能插中间、llm/stream 深冻结、
前缀时序只能靠末尾追加。

人天:路线① 12-18(仅 60% 子集);路线② 18-26;混合 22-30。

附完整证据索引(6 组,全部 lib/*.js 实现级核对)。
原文只读,未修改 HologramAgent 仓库。
2026-10-30 09:29:37 +08:00
28d9d5e929 docs: 三层记忆移植 dsh 插件可行性评估
结论:只能做子集。

逐层结论
- L1 (context 热上下文) ≈20%:dsh 的上下文压缩是「摘要替换区间」
  (dsh-compaction-basic 的 compactSurfaceRegion),不是「按 query 相关性
  淘汰 + 迁出到冷层 + 按原时间戳迁回」。CompactionTrigger 接口拿不到
  query;MessageBase 无 timestamp 字段,surface 是 append-only + replace,
  无法语义插入。
- L2 (coldcontext 冷文档) ≈80%:存储/召回/注入可落,缺
  「Peek → 回灌 → Remove」的回收站语义。
- L3 (stablemem 图记忆) ≈85%:memory_blocks/memory_block_edges 结构
  完全可映射,node:sqlite 实测可用(含 FTS5);受限于无 embedding 服务。

关键发现
- dsh 无任何 embedding/vector service,需自建(推荐复用 HA 的 http
  provider HTTP 协议转发,2 人天)。
- systemPrompt.context() 的 AssembleContext 拿不到本步消息正文,
  「按本次输入召回后注入」必须走 agent/pre-step waterfall
  (参考 dsh-time-context 的用法)。
- dsh attachment 是 CAS,与 HA「媒体字节内联在块里、不做引用账本」
  的设计冲突 → 媒体块必须裁掉。

工作量(含判据与实测)
- L2+L3 子集:36-53 人天
- MVP(L3 + 工具 + 注入面):19-25 人天
- 完全等价(含迁出-迁回):不可行,不报价

报告含 7 章 + 两份附录(HA/dsh 双侧证据索引、环境实测结果)。
原文全程只读,未修改 HologramAgent 仓库。
2026-10-30 00:39:44 +08:00