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 之后是新提交。
This commit is contained in:
@ -431,6 +431,7 @@ chain (not by calling the seam directly): **54 ms**.
|
||||
| **W11d** | degraded **and** zero hits still injects the notice (no entries) | restore either short-circuit ⇒ `W11d` red |
|
||||
| **W11e** | default config (no embedder) injects **nothing** on ordinary turns | make `no_embedder` speak again ⇒ `W11e` red |
|
||||
| **W12** | `aborted` and `embed_error` carry different labels and different wording | collapse the two labels ⇒ red |
|
||||
| **W11f** | no mode promises "matches below" when there is nothing below; every mode points at the entries when there are | drop the `aborted` split ⇒ red; drop the `default:` split ⇒ red |
|
||||
| **W11b** | a healthy recall renders no notice | — |
|
||||
| **W11c** | end-to-end: aborted pre-step text still carries the hit *and* the notice | remove the notice line ⇒ red |
|
||||
|
||||
@ -640,7 +641,7 @@ because they will bite anyone writing a second dsh service plugin.
|
||||
## Verify
|
||||
|
||||
```bash
|
||||
npm test # 115 tests: store, recall, wiring, loader, isolation, regression, G6/G6b/G7, phase-B A + G + H
|
||||
npm test # 118 tests: store, recall, wiring, loader, isolation, regression, G6/G6b/G7, phase-B A + G + H
|
||||
npm run check # 10 checkpoints with human-readable evidence (V1–V5 + V3c + MIG3 + mutations)
|
||||
```
|
||||
|
||||
|
||||
@ -290,6 +290,7 @@ phase B 必须把向量放进 **[0..1) 带**,不得越到 exact 带。
|
||||
| **W11d** | 降级 + 零命中仍要注入提示,且无条目 | **两处短路**(contextForStep 与 renderRecall)各自都能吃掉它 | 恢复任一短路 ⇒ `W11d` 红(两条独立变异 W11d† / W11d†ctx) |
|
||||
| **W11e** | 无 embedder(出货默认)+ 零命中 ⇒ 注入必须为 `''` | **P0**:每轮白灌 ~213 字符恒定无条目的记忆头,训练模型跳过该段,废掉 W11 用意 | `no_embedder` 改回返回文案 ⇒ 红 |
|
||||
| **W12** | `aborted` 与 `embed_error` 标签与文案必须分开 | 取消被描述成「搜索失败」,会诱导无谓重试 | 合并标签 ⇒ 红 |
|
||||
| **W11f** | 零命中时任何模式都不许说「matches below」;有条目时必须指向条目(表驱动,覆盖未来未知模式) | **`aborted` 与 `default:` 兜底在零命中时仍承诺条目**;`default` 是未来任何新模式唯一会走的路,最不该编 | 删 `aborted` 分叉 ⇒ 红;删 `default:` 分叉 ⇒ 红 |
|
||||
|
||||
★ 这组里 W9/W10/W11d 的教训是同一个:**判据用替身/短路/自造形状时,
|
||||
验的是替身而不是真代码**。W9 之前用假 embedder(它自己监听 abort),
|
||||
|
||||
@ -528,6 +528,10 @@ export function renderRecall(hits, sourceLabel = 'stablemem') {
|
||||
export function degradationNotice(hits) {
|
||||
const mode = hits?.degraded
|
||||
if (mode === undefined || mode === null) return ''
|
||||
// One question, asked once: are there entries below? Every branch shares it.
|
||||
// Wording contract (pinned by W11f, table-driven): nothing === true => no branch
|
||||
// may say "matches below"; nothing === false => the branch must point at them.
|
||||
const nothing = !Array.isArray(hits) || hits.length === 0
|
||||
switch (mode) {
|
||||
case 'no_embedder':
|
||||
// Permanent configuration state, not a per-turn failure. The constant index
|
||||
@ -542,21 +546,30 @@ export function degradationNotice(hits) {
|
||||
// Two wordings, because "matches below are …" is a lie when nothing was
|
||||
// found. The distinction matters: with zero entries the model must know the
|
||||
// search was incomplete AND empty, not merely incomplete.
|
||||
const nothing = !Array.isArray(hits) || hits.length === 0
|
||||
return nothing
|
||||
? '[Memory] Semantic search FAILED for this turn and the lexical fallback found nothing. Absence of a memory here does not prove it is unrecorded.'
|
||||
: '[Memory] Semantic search FAILED for this turn; matches below are lexical/symbolic only. Absence of a memory here does not prove it is unrecorded.'
|
||||
}
|
||||
case 'aborted':
|
||||
// Deliberate stop, not a fault: say so plainly and do NOT imply the memory
|
||||
// is incomplete, which would invite a pointless retry.
|
||||
return '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'
|
||||
// is incomplete, which would invite a pointless retry. Same two-wording rule
|
||||
// as embed_error — a cancelled turn that ALSO found nothing is the commonest
|
||||
// shape of an abort (the user stops, and their query matches nothing), so
|
||||
// "matches below are …" is a lie in exactly the case that happens most.
|
||||
return nothing
|
||||
? '[Memory] Semantic search was cancelled for this turn and the lexical fallback found nothing.'
|
||||
: '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'
|
||||
case 'vector_ready_unused':
|
||||
// Group A state: the vector was computed but does not take part in scoring
|
||||
// yet. Not something the model needs to hear about — but keeping the case
|
||||
// explicit means a future mode cannot silently fall through to ''.
|
||||
return ''
|
||||
default:
|
||||
return `[Memory] Recall degraded (${mode}); matches below may be incomplete.`
|
||||
// A mode nobody anticipated. "Not found" must not read as "not recorded":
|
||||
// this is the ONLY path a future mode can take, so it is the worst possible
|
||||
// place to start lying. Carries the same obligation as embed_error.
|
||||
return nothing
|
||||
? `[Memory] Recall degraded (${mode}); nothing was found, so treat this as "unknown", not "absent".`
|
||||
: `[Memory] Recall degraded (${mode}); matches below may be incomplete.`
|
||||
}
|
||||
}
|
||||
|
||||
@ -1704,3 +1704,89 @@ test('W11d†: restoring the renderRecall short-circuit turns W11d red', async (
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
// ── W11f: the notice must never promise entries it does not have ──────────
|
||||
//
|
||||
// Two wordings exist per degradation mode because "matches below are …" is a lie
|
||||
// when the lexical fallback found nothing. `embed_error` got that split first;
|
||||
// `aborted` and the `default:` fallback still said it unconditionally.
|
||||
//
|
||||
// `aborted` + zero hits is not an edge case, it is the COMMONEST shape of a
|
||||
// cancellation: the user hits stop, and their query matched nothing to begin
|
||||
// with. The `default:` branch is worse — it is the only path a FUTURE mode can
|
||||
// take, so it is the worst possible place to start lying.
|
||||
//
|
||||
// Table-driven on purpose: a per-mode assertion has to be extended every time a
|
||||
// mode is added, and the one that gets forgotten is always the fallback.
|
||||
test('W11f: no mode promises entries below when there are none, and all point at them when there are', () => {
|
||||
const MODES = [undefined, 'no_embedder', 'embed_error', 'aborted',
|
||||
'vector_ready_unused', 'future_mode_a', 'future_mode_b']
|
||||
|
||||
for (const mode of MODES) {
|
||||
const empty = []
|
||||
if (mode !== undefined) empty.degraded = mode
|
||||
const noticeEmpty = degradationNotice(empty)
|
||||
if (noticeEmpty !== '') {
|
||||
assert.ok(
|
||||
!/matches below|entries below|below are/i.test(noticeEmpty),
|
||||
`${mode} + zero hits must not promise entries below; got: ${noticeEmpty}`,
|
||||
)
|
||||
}
|
||||
|
||||
const withHit = [{ text: '值班室分机号 4324', exact: true, score: 1.01 }]
|
||||
if (mode !== undefined) withHit.degraded = mode
|
||||
const noticeHit = degradationNotice(withHit)
|
||||
if (noticeHit !== '') {
|
||||
assert.ok(
|
||||
/matches below|below are|entry|entries/i.test(noticeHit),
|
||||
`${mode} + one hit must point at the entries; got: ${noticeHit}`,
|
||||
)
|
||||
assert.ok(
|
||||
!/found nothing/i.test(noticeHit),
|
||||
`${mode} + one hit must not claim nothing was found; got: ${noticeHit}`,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('W11f†: dropping the aborted split makes W11f red', async () => {
|
||||
await withMutatedCopy(
|
||||
'recall.js',
|
||||
[[
|
||||
` return nothing
|
||||
? '[Memory] Semantic search was cancelled for this turn and the lexical fallback found nothing.'
|
||||
: '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'`,
|
||||
` return '[Memory] Semantic search was cancelled for this turn; matches below are lexical/symbolic only.'`,
|
||||
]],
|
||||
async (mod) => {
|
||||
const empty = []
|
||||
empty.degraded = 'aborted'
|
||||
assert.match(
|
||||
mod.degradationNotice(empty),
|
||||
/matches below/i,
|
||||
'the mutation restores unconditional wording, so W11f must go red on aborted+zero',
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
test('W11f†b: dropping the default-branch split makes W11f red', async () => {
|
||||
await withMutatedCopy(
|
||||
'recall.js',
|
||||
[[
|
||||
` return nothing
|
||||
? \`[Memory] Recall degraded (\${mode}); nothing was found, so treat this as "unknown", not "absent".\`
|
||||
: \`[Memory] Recall degraded (\${mode}); matches below may be incomplete.\``,
|
||||
` return \`[Memory] Recall degraded (\${mode}); matches below may be incomplete.\``,
|
||||
]],
|
||||
async (mod) => {
|
||||
const empty = []
|
||||
empty.degraded = 'future_mode_a'
|
||||
assert.match(
|
||||
mod.degradationNotice(empty),
|
||||
/matches below/i,
|
||||
'without the split the fallback lies again, so W11f must go red',
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user