Files
HologramAgent-DSH-Memory-Port/test/phase-b-c4-centering.test.js
jianf f3ee2d1cb0 test(memory): pin that the WRITE path marks stale, not the test (C4 follow-up)
C4j asserted that a stale-marked centroid falls back to uncorrected vectors — but it
called `store.markCentroidStale(fp)` **itself** to set up the state. That proves the
recall side honours the flag; it says nothing about whether production ever sets it.

Measured: deleting the one line in `#ensureCentroid` that persists the verdict

    if (status?.stale === true) this.store.markCentroidStale(fingerprint)

left **all 306 criteria green**. The wiring had no guard at all.

This is the fifth instance of one shape in this branch:

    W8   saveSpaceCentroid defined, zero production callers
    C4d  centroid built by the test's own rebuildCentroid call
    P1   vectorScores hand-built in the fixture
    D2   counters computed, never printed
    C4j  stale flag set by the criterion, not by the write path

A hand-fed input cannot prove a production path. So C4k drives only the production
entry points — `apply()` + `commit()` — and marks nothing itself:

    after a settled rebuild   stats.stale === undefined
    after 3 more commits      stats.stale === 1     (drift 3/8 = 37.5% > 20%)

Mutation — deleting that line in `#ensureCentroid`:

    not ok 12 - C4k: the write path itself marks a drifted centroid stale
    error: C4k: the WRITE path must persist the stale verdict ... undefined !== 1

## Why the drift is generated rather than forced

3 commits on top of a settled 8 puts the delta at 37.5%, past the 20% ratio — the
same path an ordinary growing space takes. Forcing `markCentroidStale` directly is
exactly what C4j does and exactly what this criterion exists to complement.

Gates: npm test 307/307 (0 skipped), npm run check 12/12, frozen faces untouched.
2026-11-05 12:39:06 +08:00

752 lines
38 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict'
import test from 'node:test'
import { BlockStore } from '../lib/store.js'
import { recallBlocksHybrid } from '../lib/recall.js'
// ══════════════════════════════════════════════════════════════════════════
// C4 · 中心化校正(graph_centroid)
// ══════════════════════════════════════════════════════════════════════════
//
// 判据来源 docs/phase-b-criteria.md:253
// C4 校正时查询向量与块向量都去均值,不能只处理一边
// 依据 centerBoth(centroid_store.go:443-451)
// + 注释「只处理一边等于混两种量纲」
//
// ★ 为什么必须自建空间,不能复用 P1 的语料
// P1 的假 embedder 是 8 族正交 one-hot:每条块独占一维,均值向量趋近 0
// (实测 mean norm = 0.3536 < MEAN_THRESHOLD 0.6)⇒ isAnomalous = false
// ⇒ 正确门控的 C4 会自动关闭去均值 ⇒ 判据全绿但什么都没验到。
// 所以这里手工构造各向异性空间:所有块共享一个强背景方向,
// 各自只叠一个小 offset,均值范数才会高到触发校正。
//
// ★ 三条判据的分工(别混)
// C4a born-RED 未实现时红(裸余弦 top1 = hub,去均值后应为 target)
// C4b mutation-GUARD 未实现时绿(裸余弦下同值对本来就 = 1),
// 只有「只减一边」能让它红 —— 抓 C4 的病根。
// 实测:只减 query 时 C4a 的 top1 仍然是 target,
// 所以 C4a 抓不到这个错误,必须靠 C4b。
// C4c guard 无中心时照常返回结果,只是不校正
//
// ★ What this does NOT establish
// 中心化改变的是向量间的夹角,因此能改变排序。
// 它不能让一个没校准的绝对分数变得可用:去均值之后余弦仍然是余弦,
// 而 DEFAULT_MIN_SCORE = 0.5 从来不是从中心化后的分布推出来的。
// 阈值校准仍是 phase-b-p1-vector-wiring.test.js:51-58 记录的已知未解问题。
const DIM = 8
const SPACE = 'c4:centering:test'
// ── 空间常数(钉死;改一个就必须重测翻转窗口)────────────────────────────
const HUB_EPS = 0.02
const TARGET_EPS = 0.30
const QUERY_WEIGHT = 0.40
const MEAN_THRESHOLD = 0.6
const norm = (v) => Math.sqrt(v.reduce((s, x) => s + x * x, 0))
const unit = (v) => { const n = norm(v); return n === 0 ? v : v.map((x) => x / n) }
const axis = (i) => unit(Array.from({ length: DIM }, (_, k) => (k === i ? 1 : 0)))
// 共同背景方向:所有块都携带它,这是各向异性的来源
const BACKBONE = unit([1.0, 0.55, 0.42, 0.30, 0.22, 0.16, 0.11, 0.07])
const TARGET_AXIS = axis(7)
const mk = (eps, ax) => unit(BACKBONE.map((x, i) => x + eps * ax[i]))
const HUB_VEC = mk(HUB_EPS, axis(1))
const TARGET_VEC = mk(TARGET_EPS, TARGET_AXIS)
const DISTRACTOR_VECS = [0, 2, 3, 4, 5, 6].map((i) => mk(TARGET_EPS, axis(i)))
const QUERY_TEXT = '上次说的那个四位数是什么来着?'
const QUERY_VEC = unit(BACKBONE.map((x, i) => x + TARGET_EPS * QUERY_WEIGHT * TARGET_AXIS[i]))
const HUB = { key: 'hub', text: '这段时间一切照旧,没有什么特别的事情。', vec: HUB_VEC }
const TARGET = { key: 'target', text: '第七号仓库的备用门禁码是 QP47-812。', vec: TARGET_VEC }
const DISTRACTORS = DISTRACTOR_VECS.map((vec, i) => ({
key: `d${i}`, text: `干扰块 ${i}:与本问无关的日常事项。`, vec,
}))
const CORPUS = [HUB, TARGET, ...DISTRACTORS]
const byText = new Map(CORPUS.map((b) => [b.text, b.vec]))
function makeEmbedder() {
return {
async embedText(text) {
if (byText.has(text)) return byText.get(text)
if (text === QUERY_TEXT) return QUERY_VEC
return new Array(DIM).fill(0)
},
info: () => ({ fingerprint: SPACE, dimension: DIM }),
}
}
/** 建一个装满 CORPUS 的库,返回 store 与 id→key 的映射。 */
function seedStore({ withCentroid = true } = {}) {
const store = new BlockStore({ path: ':memory:' }).open()
const keyOfId = new Map()
for (const block of CORPUS) {
const written = store.putBlock({ text: block.text, vector: block.vec, fingerprint: SPACE })
keyOfId.set(written.id, block.key)
}
if (withCentroid) store.rebuildCentroid(SPACE)
return { store, keyOfId }
}
/** 召回并按 key 呈现,便于断言。 */
async function recallByKey(store, keyOfId, options = {}) {
const hits = await recallBlocksHybrid(store, makeEmbedder(), QUERY_TEXT, { topK: 8, ...options })
return hits.map((hit) => ({ key: keyOfId.get(hit.id) ?? hit.id, score: hit.score, vectorHit: hit.vectorHit }))
}
const fmt = (hits) => hits.map((h) => `${h.key}=${h.score.toFixed(4)}(v=${Number(h.vectorHit ?? 0).toFixed(4)})`).join(' ')
// ── 前置:空间真的会触发校正 ────────────────────────────────────────────
//
// 没有这三条,后面任何断言都可能因为「校正压根没启用」而通过,那是假绿。
test('C4-0: the fixture space is anisotropic enough to enable centering', () => {
const { store } = seedStore()
try {
const centroid = store.loadCentroid(SPACE)
assert.ok(centroid !== undefined, 'precondition: a centroid must exist after rebuildCentroid')
assert.equal(centroid.isAnomalous, true, 'precondition: the space must be judged anisotropic')
assert.ok(
norm(centroid.mean) >= MEAN_THRESHOLD,
`precondition: mean norm ${norm(centroid.mean).toFixed(6)} must clear MEAN_THRESHOLD ${MEAN_THRESHOLD}`,
)
assert.equal(centroid.dim, DIM, 'precondition: centroid dimension must match the space')
assert.equal(centroid.vectorCount, CORPUS.length, 'precondition: every block contributed')
} finally {
store.close()
}
})
// ── C4a: 校正把 top-1 从 hub 翻到 target ────────────────────────────────
//
// born-RED:在有中心但不去均值的构建上,裸余弦会让 hub 赢
// (实测 hub 0.9928 > target 0.9859),因为 hub 几乎就是背景本身,
// 而背景是 query 与所有块共有的最大分量。
test('C4a: centering flips top-1 from the hub to the target', async (t) => {
const { store, keyOfId } = seedStore()
try {
const hits = await recallByKey(store, keyOfId)
t.diagnostic(`C4a ranking: ${fmt(hits)}`)
assert.ok(hits.length > 0, 'precondition: the recall must return something')
assert.equal(
hits[0].key,
'target',
'C4a: after centering the target must be top-1. ' +
'If this is "hub", the space mean is still dominating the score — either ' +
'centering is not wired at all, or only one side was centered. ' +
`got ${fmt(hits)}`,
)
// 分数也要过闸,否则「top1 对了」但整轮被 minScore 清空就成了空断言
assert.ok(
hits[0].score > 0.5,
`C4a: the winning score must clear DEFAULT_MIN_SCORE 0.5; got ${hits[0].score}`,
)
} finally {
store.close()
}
})
// ── C4b: 同值对的余弦恒为 1(mutation-GUARD)──────────────────────────────
//
// 这是唯一一条**不依赖语料构造、纯代数必然成立**的性质:
// 若块向量与查询向量完全相同,余弦必须恰好是 1 —— 不论是否中心化。
// · 两侧都减同一个均值 ⇒ (v-m) 与 (v-m) 的余弦 = 1 ✓
// · 只减一边 ⇒ (v-m) 与 v 的余弦 ≠ 1 ✗ ← M-C4 正是这个
//
// ★ 它在未实现的构建上是绿的(裸余弦下同值对本来就 = 1),
// 所以它不是 born-red 判据,而是抓 M-C4 的那张网。别因为它「一开始就绿」删掉它:
// 实测「只减 query」时 C4a 的 top-1 仍然是 target,C4a 抓不到这个错误。
test('C4b: a block identical to the query always scores a cosine of 1', async (t) => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
// 一条与 query 完全同值的块,外加两条背景块,凑出会触发校正的空间
const twin = store.putBlock({ text: '与查询同值的那条', vector: QUERY_VEC, fingerprint: SPACE })
for (const vec of [HUB_VEC, ...DISTRACTOR_VECS.slice(0, 3)]) {
store.putBlock({ text: `背景块 ${Math.random()}`, vector: vec, fingerprint: SPACE })
}
store.rebuildCentroid(SPACE)
const hits = await recallBlocksHybrid(store, makeEmbedder(), QUERY_TEXT, { topK: 8, minScore: 0 })
const twinHit = hits.find((hit) => hit.id === twin.id)
assert.ok(twinHit !== undefined, `C4b: precondition — the twin block must be scored; got ${hits.length} hits`)
t.diagnostic(`C4b twin vectorHit = ${twinHit.vectorHit}`)
assert.ok(
Math.abs(twinHit.vectorHit - 1) < 1e-9,
'C4b: an identical vector pair must score exactly 1. ' +
'A value near 0.1897 on this 5-block fixture is the signature of centering ONE '
+ 'side only ' +
'(M-C4) — subtracting the mean from the query but not the block mixes ' +
'two coordinate systems and the cosine stops being an angle. ' +
`got ${twinHit.vectorHit}`,
)
} finally {
store.close()
}
})
// ── C4c: 没有中心时照常返回结果,只是不校正 ──────────────────────────────
//
// criteria 里写死了:C3 判 stale 的那一轮「仍返回结果(用未校正向量)」。
// 这条守的是「拿不到中心就让召回失败」这个错误,不是「校正对不对」。
test('C4c: without a centroid the recall still returns results, only uncorrected', async (t) => {
const { store, keyOfId } = seedStore({ withCentroid: false })
try {
assert.equal(store.loadCentroid(SPACE), undefined, 'precondition: no centroid exists in this fixture')
const hits = await recallByKey(store, keyOfId)
t.diagnostic(`C4c uncorrected ranking: ${fmt(hits)}`)
assert.ok(hits.length > 0, 'C4c: a missing centroid must not empty the result set')
assert.equal(
hits[0].key,
'hub',
'C4c: without a centroid the ranking stays uncorrected, so the hub still wins. ' +
'If this is "target", something is centering without a centroid to center on. ' +
`got ${fmt(hits)}`,
)
} finally {
store.close()
}
})
// ── C4d: 生产路径必须自己把中心建出来 ────────────────────────────────────
//
// ★ 这条是 C4 的第一层,也是最容易被漏掉的一半。
// C4a/b/c 都用 `seedStore()` 手工调 `rebuildCentroid`,所以它们只能证明
// 「去均值的算术是对的」,证明不了「生产上中心真的会出现」。
// 而生产上恰恰是后者不存在:`saveSpaceCentroid` / `centroidStatus` /
// `centroidRecallState` 在 lib/ 里都没有生产调用者,`rebuildCentroid`
// 只有测试在调 —— `graph_centroid` 曾经永远是空表。
//
// 所以这条判据走真 `apply()` + 真 `commit()`,不碰 store 内部:
// 写完之后 `loadCentroid` 必须已经能读到中心,且召回必须已经用上校正。
test('C4d: the production commit path builds the centroid it later reads', async () => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { apply: applyMemory } = await import('../lib/index.js')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'c4d-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = applyMemory(ctx, { path: join(dir, 'm.db'), embedder: makeEmbedder() })
// 前置:还没写东西时不该有中心
assert.equal(
mem.store.loadCentroid(SPACE),
undefined,
'precondition: a fresh database has no centroid',
)
// 全部走生产入口,不手工调 rebuildCentroid
const keyOfId = new Map()
for (const block of CORPUS) {
const written = await mem.commit({ text: block.text })
keyOfId.set(written.id, block.key)
assert.equal(written.embedded, true, `precondition: ${block.key} must actually be embedded`)
}
// ★ The first commit builds a centroid from the blocks it has seen SO FAR,
// so right after a burst of writes the centroid still describes only the
// first one — and with MIN_SAMPLE it is not even allowed to claim a direction
// yet. The write path schedules a drift rebuild; wait for it before asserting
// on the centroid's CONTENTS rather than merely on its existence.
//
// Measured without this wait: vectorCount=1, meanLength=1.0000 on the same
// fixture, and a 1-block "mean" is that block itself.
await new Promise((resolve) => setTimeout(resolve, 3000))
const centroid = mem.store.loadCentroid(SPACE)
assert.ok(
centroid !== undefined,
'C4d: after commits through the production path a centroid must exist. ' +
'If this is undefined, nothing in lib/ ever builds one and centering can ' +
'never apply in real use — the arithmetic tests above would still pass ' +
'because they build the centroid by hand.',
)
assert.equal(centroid.isAnomalous, true, 'C4d: and it must be judged anisotropic for this fixture')
assert.equal(centroid.dim, DIM, 'C4d: its width must match the space')
// ★ The existence check above passes even when the centroid describes a
// single block — which is exactly the state right after the first commit.
// A 1-block mean is that block itself (norm 1.0, so it clears the 0.6
// threshold by construction), and centering by it drops that block as a zero
// vector. Pin the coverage, not just the presence.
assert.equal(
centroid.vectorCount,
CORPUS.length,
'C4d: the centroid must describe every block in the space, not only the ones '
+ 'present at the first commit. A smaller count means the drift rebuild '
+ 'never caught up and centering is subtracting the wrong mean.',
)
// 端到端:同一个库,召回必须已经用上校正
const hits = await recallBlocksHybrid(mem.store, makeEmbedder(), QUERY_TEXT, { topK: 8 })
assert.ok(hits.length > 0, 'C4d: the recall must return something')
const top = hits[0]
assert.equal(
keyOfId.get(top.id),
'target',
'C4d: the whole point — production write + production read must together ' +
'produce the corrected ranking. A "hub" winner here means the two halves ' +
'are not connected even though each one works on its own. ' +
`got ${hits.map((h) => keyOfId.get(h.id) ?? h.id).join(',')}`,
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})
// ── C4e: 块数漂移超 20% 后,写入侧必须让中心重新跟上 ──────────────────────
//
// ★ 这条守的是我在 0a6adff 里自己漏掉的那一半。
// 那时的 `#ensureCentroid` 是「没有中心就建一个,有就 return」——
// 于是中心永远停在第一次 commit 的快照上,块数翻多少倍它都不动。
// 这正是这一整轮反复抓的同一个病:**逻辑写了,但没有被调到**。
// `centroidStatus` 里那套 stale 判定(漂移 > 20% + 10 分钟节流 + 异步重建)
// 原本在 lib/ 里零生产调用者,不接上的话它只是文件里的一段死代码。
//
// C4a/b/c 都手工 `rebuildCentroid`,C4d 只验证「首次建」,三条都照不到这里:
// 中心存在且过期,与中心不存在,是两个不同的失效形态。
test('C4e: a drifted centroid is refreshed by the write path, not left stale forever', async (t) => {
// 异步重建靠 setTimeout,用假时钟把 2s 拨过去,免得测试真的睡两秒。
t.mock.timers.enable({ apis: ['setTimeout'] })
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { apply: applyMemory } = await import('../lib/index.js')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const dir = mkdtempSync(join(tmpdir(), 'c4e-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = applyMemory(ctx, { path: join(dir, 'm.db'), embedder: makeEmbedder() })
// 第一条:写入侧顺手建出中心,此时它只见过 1 条块
await mem.commit({ text: CORPUS[0].text })
const born = mem.store.loadCentroid(SPACE)
assert.ok(born !== undefined, 'precondition: the first commit must build a centroid')
const bornCount = born.vectorCount
assert.ok(bornCount >= 1, `precondition: the centroid must have counted blocks, got ${bornCount}`)
// 继续写,直到块数相对建中心时漂移超过 20%
for (const block of CORPUS.slice(1)) await mem.commit({ text: block.text })
// ★ 这里刻意**不**调 centroidStatus。
// 它自己就会调度重建 —— 无论写入侧接没接,只要测试里调了它,
// 重建就会发生,这条判据就变成了自己证明自己。第一次写这条时
// 就是这么写的,M4 变异(退回 0a6adff 的「有中心就 return」)
// 居然 6/6 全绿,才把这个问题缴出来。
const drift = (CORPUS.length - bornCount) / bornCount
assert.ok(
drift > 0.2,
`precondition: this fixture must drift past the 20% ratio; got ${(drift * 100).toFixed(0)}%`,
)
// 假时钟把重建的 2s 拨过去。
// 只有写入侧真的调过 centroidStatus,这里才会有回调可跑。
t.mock.timers.tick(2000)
const healed = mem.store.loadCentroid(SPACE)
assert.ok(healed !== undefined, 'C4e: the centroid must still exist after the scheduled rebuild')
assert.ok(
healed.vectorCount > bornCount,
'C4e: a centroid that drifted past the 20% ratio must be rebuilt by the write path. ' +
`It is still describing ${healed.vectorCount} blocks while ${CORPUS.length} carry vectors — ` +
'that means nothing in lib/ ever consults the stale judgement, so the mean stays frozen ' +
'on whatever the first commit happened to see, and centering slowly stops describing ' +
'the space it is supposed to correct.',
)
assert.equal(
healed.vectorCount,
CORPUS.length,
'C4e: the rebuilt centroid must cover every vectored block in this space',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})
// ── C4g: 各向同性空间必须完全不校正(isAnomalous 门控)────────────────────
//
// ★ 这条守的是「门控本身」,不是「校正算得对不对」。
// C4a/b/c/d/e 五条用的全是有各向异性的 fixture(C4-0 与 C4d 还专门断言
// isAnomalous === true),C4c 是「没有中心行」—— 三条都照不到「有中心、但
// 中心判定为各向同性」这个分支。实测把 lib/recall.js 里的
// `centroid?.isAnomalous === true &&` 删掉,上述五条加 phase-b-h、phase-b-a
// 全部全绿:门控是裸的。
//
// 数字来源:同一份探针分别打「门控在」与「门控被拆」两个 lib,实测对照,
// 不是纸上推的。各向同性空间(8 条正交 one-hot):
// meanLength = 0.353553(= 1/sqrt(8)),isAnomalous = false
// query = unit(ones) 门控在 → 8 条命中,每条 cosine = 0.3536
// 门控拆 → 0 条命中(去均值后各块余弦转负,被 V5 的
// 结构性门 `vectorScore <= 0 && symbolHit === 0` 全丢)
// 为什么用这条 query:它的方向就是均值方向,去均值后与所有块的夹角都变成钝角,
// 落差是二值化的(8 → 0),不依赖阈值也不依赖浮点容差。
test('C4g: an isotropic space is left uncorrected — the isAnomalous gate', async (t) => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { apply: applyMemory } = await import('../lib/index.js')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const ISO_DIM = 8
const ISO_SPACE = 'c4g:isotropic:test'
const ones = new Array(ISO_DIM).fill(1)
const queryA = unit(ones) // ≈ 均值方向
const queryB = unit([0.5, 0.3, 0.2, 0, 0, 0, 0, 0]) // 三个方向轴的混合
const texts = Array.from({ length: ISO_DIM }, (_, i) => `正交块 ${i}`)
const vecOf = new Map(texts.map((text, i) => [text, axis(i)]))
vecOf.set('Q-A', queryA)
vecOf.set('Q-B', queryB)
const isoEmbedder = {
async embedText(text) { return vecOf.get(text) ?? new Array(ISO_DIM).fill(0) },
info: () => ({ fingerprint: ISO_SPACE, dimension: ISO_DIM }),
}
const dir = mkdtempSync(join(tmpdir(), 'c4g-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = applyMemory(ctx, { path: join(dir, 'm.db'), embedder: isoEmbedder })
const keyOf = new Map()
for (const text of texts) {
const written = await mem.commit({ text })
keyOf.set(written.id, text)
}
// 让写入侧的漂移重建跑完,断言的是「生产路径建出的中心」而不是半成品
await new Promise((resolve) => setTimeout(resolve, 3000))
const centroid = mem.store.loadCentroid(mem.space().fingerprint)
assert.ok(centroid !== undefined, 'C4g precondition: the production path must still build a centroid')
assert.equal(centroid.isAnomalous, false, 'C4g precondition: 8 orthogonal axes are isotropic')
assert.ok(
Math.abs((centroid.stats?.meanLength ?? 0) - 1 / Math.sqrt(ISO_DIM)) < 1e-6,
`C4g precondition: meanLength must be 1/sqrt(8); got ${centroid.stats?.meanLength}`,
)
assert.equal(centroid.vectorCount, ISO_DIM, 'C4g precondition: every block contributed')
// ① 方向接近均值的 query:门控在 ⇒ 全部命中,且余弦就是 1/sqrt(8)
const hitsA = await recallBlocksHybrid(mem.store, isoEmbedder, 'Q-A', { topK: ISO_DIM, minScore: 0 })
t.diagnostic(`C4g mean-like query: n=${hitsA.length} ${hitsA.map((h) => `${Number(h.vectorHit ?? 0).toFixed(4)}`).join(' ')}`)
assert.equal(
hitsA.length,
ISO_DIM,
'C4g: an isotropic space must NOT be centred. Removing the isAnomalous gate turns '
+ 'every cosine negative here and V5 drops the lot — the count collapses from 8 to 0.',
)
for (const hit of hitsA) {
assert.ok(
Math.abs(Number(hit.vectorHit) - 1 / Math.sqrt(ISO_DIM)) < 1e-4,
`C4g: each block keeps its raw cosine 1/sqrt(8); got ${hit.vectorHit}`,
)
}
// ② 三轴混合的 query:钉死「完全没做去均值」的具体数值
const hitsB = await recallBlocksHybrid(mem.store, isoEmbedder, 'Q-B', { topK: 3, minScore: 0 })
const byKey = new Map(hitsB.map((h) => [keyOf.get(h.id), Number(h.vectorHit)]))
t.diagnostic(`C4g three-axis query: ${[...byKey].map(([k, v]) => `${k}=${v.toFixed(4)}`).join(' ')}`)
assert.ok(Math.abs((byKey.get('正交块 0') ?? 0) - 0.8111) < 1e-4, 'C4g: block 0 keeps cosine 0.8111')
assert.ok(Math.abs((byKey.get('正交块 1') ?? 0) - 0.4867) < 1e-4, 'C4g: block 1 keeps cosine 0.4867')
assert.ok(Math.abs((byKey.get('正交块 2') ?? 0) - 0.3244) < 1e-4, 'C4g: block 2 keeps cosine 0.3244')
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})
// ── C3-k: 召回热路径不得出现计数查询 ────────────────────────────────────────
//
// `centroidStatus` 的第一行是 `countVectored`(一条 COUNT),而它每轮 pre-step
// 都会跑 —— C3-k 明令该查询只能出现在写入侧。实测往 lib/recall.js 里塞一行
// `store.countVectored(fingerprint)`,c4 6/6、phase-b-h、phase-b-a 全绿:
// 没有判据在守这条边界。所以这里用静态扫描把它钉住。
//
// ★ 扫描必须自证有效:同一段逻辑对 lib/index.js 跑一遍,必须能找到
// `centroidStatus(` —— 否则正则写错时,上面两条「不包含」会永远为真。
test('C3-k: the recall hot path must not run a COUNT query', async () => {
const { readFileSync } = await import('node:fs')
const strip = (relative) => readFileSync(new URL(relative, import.meta.url), 'utf8')
.replace(/\/\*[\s\S]*?\*\//g, ' ')
.split('\n')
.map((line) => line.replace(/\/\/.*$/, ''))
.join('\n')
const recall = strip('../lib/recall.js')
assert.ok(
!/\bcountVectored\s*\(/.test(recall),
'C3-k: lib/recall.js must not call countVectored — that COUNT belongs to the write path',
)
assert.ok(
!/\bcentroidStatus\s*\(/.test(recall),
'C3-k: lib/recall.js must not call centroidStatus — it runs countVectored internally, '
+ 'and the recall path may only read the centroid (loadCentroid)',
)
// 反向自检:同样的扫描必须能在写入侧看见那次调用,证明扫描不是空转
const index = strip('../lib/index.js')
assert.ok(
/\bcentroidStatus\s*\(/.test(index),
'C3-k self-check: the scan must be able to SEE a centroidStatus call — lib/index.js '
+ 'calls it on the write path. If this fails the two assertions above prove nothing.',
)
assert.ok(
/\bloadCentroid\s*\(/.test(recall),
'C3-k: the recall path reads the centroid through loadCentroid — and only that',
)
})
// ── C4h: 单块空间不得被判为各向异性(MIN_SAMPLE)──────────────────────────
//
// 一条向量的「均值」就是它自己,模长恒为 1.0,必然越过 0.6 阈值 —— 于是
// 「只有一条块的空间」会被判成各向异性,然后拿这条块自己去均值。减完是零
// 向量,被 recall.js 的 `scoringVector === null` 跳过,唯一那条块就此从
// 召回里消失。实测(真时钟、真 commit):第 8 次 commit 之前 centroidCount=1、
// meanLength=1.0000,同一库有中心时命中 1 条、无中心时命中 3 条。
//
// 判据落在两处:判定本身(isAnomalous 必须 false),以及后果(唯一那条块
// 必须仍然可召回)。
test('C4h: a single-block space is not anisotropic, and its only block stays recallable', async () => {
const store = new BlockStore({ path: ':memory:' }).open()
try {
const fp = 'c4h:single:test'
const only = axis(0)
const written = store.putBlock({ text: '唯一的一条块', vector: only, fingerprint: fp })
const built = store.rebuildCentroid(fp)
assert.equal(built.vectorCount, 1, 'C4h precondition: exactly one vector contributed')
assert.ok(Math.abs(built.meanLength - 1) < 1e-9, 'C4h precondition: a one-vector mean has norm 1')
assert.equal(
built.isAnomalous,
false,
'C4h: a one-vector mean IS that vector (norm 1.0), so the 0.6 test alone always says '
+ '"anisotropic". MIN_SAMPLE must refuse to call a single sample a direction — otherwise '
+ 'centering subtracts the block from itself and the zero vector is dropped from recall.',
)
const embedder = {
async embedText() { return only },
info: () => ({ fingerprint: fp, dimension: DIM }),
}
const hits = await recallBlocksHybrid(store, embedder, '与那条块方向相同的问题', { topK: 3, minScore: 0 })
assert.equal(
hits.length,
1,
'C4h: the only block in the space must still be recallable. A count of 0 means the '
+ 'single-block "centroid" was applied and its own block was projected onto the zero '
+ 'vector.',
)
assert.equal(hits[0].id, written.id, 'C4h: and it must be the block that was written')
} finally {
store.close()
}
})
// ── C4i: 浮点尘埃不得被当成方向 ─────────────────────────────────────────────
//
// 一个等于空间均值的块,与均值的差只有浮点残差(~1e-17)。如果把它归一化,
// 这堆垃圾会变成一个「单位向量」,再与 query 算出 0.2183 这样的分数 —— 看起来
// 像信号,实际是噪声。实测(2 条完全相同的块,query 与块方向不同):
//
// 原始余弦(无中心) 0.8608
// `norm > 0` 时代的输出 0.2183 ← 尘埃被重新归一化
// 现在的输出 无命中 ← 尘埃被判定为「没有方向」
//
// ★ 这条判据钉的是**不变式**,不是某个具体策略:
// 「任何返回的命中,其 vectorHit 必须是真实余弦(0.8608),绝不能是尘埃
// 推导出来的 0.22 量级」。因此它同时容得下两种未来做法 —— 丢弃这些块
// (当前),或对无方差的空间干脆不校正(若以后加上界保护)。
// 它唯一不允许的,就是继续拿尘埃打分。
test('C4i: floating-point dust is never scored as a direction', async (t) => {
const DUP_DIM = 8
const fp = 'c4i:duplicate:test'
const blockVec = unit([1, 0.2, 0.1, 0, 0, 0, 0, 0])
const queryVec = unit([1, 0.9, 0.1, 0, 0, 0, 0, 0])
const RAW_COSINE = 0.8608 // measured; see the header
const store = new BlockStore({ path: ':memory:' }).open()
try {
for (let i = 0; i < 2; i += 1) {
store.putBlock({ text: `重复的块 ${i}`, vector: blockVec, fingerprint: fp })
}
const built = store.rebuildCentroid(fp)
assert.ok(built.meanLength > 0.999, `C4i precondition: identical blocks have a unit mean; got ${built.meanLength}`)
assert.equal(built.isAnomalous, true, 'C4i precondition: the fixture must actually enable centring')
const embedder = {
async embedText() { return queryVec },
info: () => ({ fingerprint: fp, dimension: DUP_DIM }),
}
const hits = await recallBlocksHybrid(store, embedder, 'q', { topK: 10, minScore: 0 })
t.diagnostic(`C4i duplicate-space hits=${hits.length} ${hits.map((h) => Number(h.vectorHit).toFixed(4)).join(',')}`)
for (const hit of hits) {
assert.ok(
Math.abs(Number(hit.vectorHit) - RAW_COSINE) < 1e-3,
'C4i: every hit must carry the RAW cosine 0.8608. A value near 0.22 is '
+ 'renormalised floating-point dust: the block differs from the space mean '
+ 'only by ~1e-17, and turning that into a unit vector manufactures a '
+ 'direction that does not exist. Treat it as "no direction" instead.',
)
}
} finally {
store.close()
}
})
// ── C4j: stale 时退回未校正(父片令原文)──────────────────────────────────
//
// 片令写得很清楚:「判 stale 的那一轮仍返回结果(用未校正向量)」。
// 召回侧不能自己判 stale —— `centroidStatus` 第一行是 countVectored,C3-k 明令
// 它不得进热路径。所以判定在写入侧做,结果落在 centroid 行的 `stats.stale` 上,
// 召回侧只读这个标记。
//
// ★ 这条判据是在修掉一个真 bug 之后补的:最初把标记判断写成
// `centroid.stats?.stale !== true`,而标记存的是 JSON 数字 1 —— JS 里
// `1 !== true` 恒为真,于是整个 guard 变成 no-op。**当时全套 305 条判据全绿。**
// 换成 `!centroid.stats?.stale` 才生效。
// 没有这条判据,"写了但永不生效"会一直躺在那儿。
//
// fixture 用 C4 那个 hub/target 空间:那里的差别是可观察的 ——
// 校正时 target 胜(0.9000),不校正时 hub 胜(0.9928,即裸余弦)。
test('C4j: a centroid marked stale falls back to uncorrected vectors', async (t) => {
const DIM = 8
const unit = (v) => { const n = Math.hypot(...v); return n === 0 ? v : v.map((x) => x / n) }
const axis = (i) => unit(Array.from({ length: DIM }, (_, k) => (k === i ? 1 : 0)))
const BACKBONE = unit([1.0, 0.55, 0.42, 0.30, 0.22, 0.16, 0.11, 0.07])
const mk = (eps, ax) => unit(BACKBONE.map((x, i) => x + eps * ax[i]))
const HUB = { key: 'hub', text: '这段时间一切照旧。', vec: mk(0.02, axis(1)) }
const TARGET = { key: 'target', text: '第七号仓库的备用门禁码是 QP47-812。', vec: mk(0.30, axis(7)) }
const DISTRACTORS = [0, 2, 3, 4, 5, 6].map((i) => ({ key: `d${i}`, text: `干扰块 ${i}。`, vec: mk(0.30, axis(i)) }))
const CORPUS = [HUB, TARGET, ...DISTRACTORS]
const QUERY_TEXT = '上次说的那个四位数是什么来着?'
const QUERY_VEC = unit(BACKBONE.map((x, i) => x + 0.30 * 0.40 * axis(7)[i]))
const byText = new Map(CORPUS.map((b) => [b.text, b.vec]))
const store = new BlockStore({ path: ':memory:' }).open()
try {
const fp = 'c4j:stale-fallback'
const keyOf = new Map()
for (const b of CORPUS) keyOf.set(store.putBlock({ text: b.text, vector: b.vec, fingerprint: fp }).id, b.key)
const built = store.rebuildCentroid(fp)
assert.equal(built.isAnomalous, true, 'C4j precondition: the fixture must enable centring')
const embedder = {
async embedText(text) { return text === QUERY_TEXT ? QUERY_VEC : (byText.get(text) ?? new Array(DIM).fill(0)) },
info: () => ({ fingerprint: fp, dimension: DIM }),
}
const top1 = async () => {
const hits = await recallBlocksHybrid(store, embedder, QUERY_TEXT, { topK: 3, minScore: 0 })
t.diagnostic(`C4j ranking: ${hits.map((h) => `${keyOf.get(h.id)}=${Number(h.vectorHit ?? 0).toFixed(4)}`).join(' ')}`)
return { key: keyOf.get(hits[0]?.id), score: Number(hits[0]?.vectorHit ?? 0) }
}
const fresh = await top1()
assert.equal(fresh.key, 'target', 'C4j: with a usable centroid the target wins')
// The write path's verdict, persisted on the row.
assert.equal(store.markCentroidStale(fp), true, 'C4j: the flag must actually be written')
const stale = await top1()
assert.equal(
stale.key,
'hub',
'C4j: a round that judges the centroid stale must fall back to UNCORRECTED vectors '
+ '(the raw cosine then puts the hub first). If this still says "target", the flag '
+ 'is being ignored — the exact bug that a strict `!== true` comparison produced '
+ 'while the whole suite stayed green.',
)
assert.ok(Math.abs(stale.score - 0.9928) < 1e-3, `C4j: and it must be the raw cosine; got ${stale.score}`)
store.rebuildCentroid(fp)
const healed = await top1()
assert.equal(healed.key, 'target', 'C4j: a rebuild clears the flag and centring returns')
} finally {
store.close()
}
})
// ── C4k: 落标记这件事必须是写入侧自己干的,不是测试替它干 ───────────────────
//
// C4j 里有一行 `store.markCentroidStale(fp)` —— 那是判据**手工**打的标记,它只
// 证明「召回侧会尊重标记」,不证明「生产路径会打这个标记」。
//
// 实测:把 `lib/index.js` 里那行 `if (status?.stale === true) this.store
// .markCentroidStale(...)` 整个删掉,**全套 306 条判据全绿**。也就是说这条接线
// 此前没有任何判据守着 —— 与 W8(saveSpaceCentroid 零生产调用点)、C4d(中心
// 靠测试手工 rebuildCentroid)是同一个病:手工喂进去的输入,证明不了生产路径。
//
// 这条判据只用生产入口:`apply()` + `commit()`,自己不打任何标记。
test('C4k: the write path itself marks a drifted centroid stale', async () => {
const { Context } = await import('@deepseek-ai/cordis')
const { SystemPrompt } = await import('@deepseek-ai/dsh-system-prompt')
const { ToolRuntime } = await import('@deepseek-ai/dsh-tools')
const { apply: applyMemory } = await import('../lib/index.js')
const { mkdtempSync, rmSync } = await import('node:fs')
const { tmpdir } = await import('node:os')
const { join } = await import('node:path')
const DIM = 4
const dir = mkdtempSync(join(tmpdir(), 'c4k-'))
const ctx = new Context()
new SystemPrompt(ctx, {})
new ToolRuntime(ctx, {})
try {
const mem = applyMemory(ctx, {
path: join(dir, 'm.db'),
embedder: {
async embedText() { return unit([1, 0.1, 0, 0]) },
info: () => ({ fingerprint: 'c4k:write-marks', dimension: DIM }),
},
})
const fp = mem.space().fingerprint
// Build a centroid, then let the scheduled rebuild land so the count is settled.
for (let i = 0; i < 8; i += 1) await mem.commit({ text: `块 ${i}` })
await new Promise((resolve) => setTimeout(resolve, 3000))
const settled = mem.store.loadCentroid(fp)
assert.ok(settled !== undefined, 'C4k precondition: a centroid must exist')
assert.equal(
settled.stats?.stale,
undefined,
'C4k precondition: a freshly rebuilt centroid carries no stale flag',
)
// Drift it past the 20% ratio through the production write path only.
for (let i = 8; i < 11; i += 1) await mem.commit({ text: `块 ${i}` })
const drifted = mem.store.loadCentroid(fp)
assert.equal(
drifted.stats?.stale,
1,
'C4k: the WRITE path must persist the stale verdict — the recall side is not '
+ 'allowed to judge it (C3-k), so if this flag is missing, recall keeps '
+ 'centring with a mean that no longer describes the space. Deleting that one '
+ 'line in #ensureCentroid left all 306 criteria green, which is why this '
+ 'criterion exists.',
)
} finally {
await ctx.fiber.dispose()
rmSync(dir, { recursive: true, force: true })
}
})