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.
752 lines
38 KiB
JavaScript
752 lines
38 KiB
JavaScript
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 })
|
||
}
|
||
})
|