@@ -52,6 +52,11 @@ const SEED = [
5252// Standard query set: each case is a query plus the ids that MUST appear in
5353// the top-K for the case to count as a hit. Covers the three recall paths —
5454// multi-term lexical (BM25's home turf), identifier lookup, and semantic.
55+ // Optional per case: `forbidden` — ids that must NOT appear in the top-K.
56+ // Anti-update probes (E6, #280 链;StatemenBench anti-update probes):锁的
57+ // 回归类是「supersede 后旧值复现」——被取代旧条靠 archived=1 从四路检索里
58+ // **排除**(不是降权)。未来谁把任一默认检索路径改成 archived 宽容的降权、
59+ // 或把归档行重新放回候选池,forbiddenHit 就会 >0,这里先红。
5560export const TEST_CASES = [
5661 { query : "rust 异步" , expected : [ "mem_async_pattern" , "mem_rust_switch" ] , note : "scattered terms — BM25 territory" } ,
5762 { query : "ZFS-4421 checksum" , expected : [ "mem_zfs_bug" ] , note : "identifier + keyword" } ,
@@ -67,7 +72,12 @@ export const TEST_CASES = [
6772 // leading token with a cross-topic memory that must rank below it) and an
6873 // exact-token case that leans on the BM25 path.
6974 { query : "zfs 磁盘 替换" , expected : [ "mem_zfs_bug" ] , note : "shared-token distractor" } ,
70- { query : "tokio spawn channel" , expected : [ "mem_async_pattern" ] , note : "exact async tokens" }
75+ { query : "tokio spawn channel" , expected : [ "mem_async_pattern" ] , note : "exact async tokens" } ,
76+ // Anti-update probes(配 seedService 里的 supersede pair):第二条 query 的
77+ // 措辞只与被取代的旧值(Go 选型)重叠——归档旧条若经任一默认路径复现,
78+ // forbiddenHit > 0;同时新值必须照常命中(取代后话题有活答案)。
79+ { query : "编译模块 语言 选型" , expected : [ "mem_rust_switch" ] , forbidden : [ "mem_rust_switch_old" ] , note : "anti-update: winner must answer the topic" } ,
80+ { query : "Go 部署 简单" , expected : [ "mem_rust_switch" ] , forbidden : [ "mem_rust_switch_old" ] , note : "stale-value bait: wording matches only the superseded value" }
7181] ;
7282
7383export function seedService ( overrides = { } ) {
@@ -95,6 +105,15 @@ export function seedService(overrides = {}) {
95105 const row = store . save ( { id : m . id , type : m . type , title : m . title , content : m . content , tags : m . tags , importance : m . importance , source : "seed" } ) ;
96106 store . setEmbedding ( row . id , hashVec ( `${ m . title } ${ m . content } ` ) ) ;
97107 }
108+ // Supersede pair(anti-update 探针的种子):复刻 dream/decisions.js
109+ // applySupersede 留下的确切落库状态——没有独立的取代列,取代关系完全由
110+ // loser 的 archived=1 + 正文 (superseded by: …) 注记表达。winner 就是
111+ // 上面的 mem_rust_switch 种子行,让「编译语言」话题始终有活答案,
112+ // 归档的 Go 旧决策必须保持不可见。
113+ const loserContent = "项目编译模块使用 Go 语言,选型理由是部署简单\n\n(superseded by: 语言迁移决策)" ;
114+ const loser = store . save ( { id : "mem_rust_switch_old" , type : "decision" , title : "编译模块语言(旧)" , content : loserContent , importance : 4 , tags : [ "rust" ] , source : "seed" } ) ;
115+ store . setEmbedding ( loser . id , hashVec ( `编译模块语言(旧) ${ loserContent } ` ) ) ;
116+ store . setArchived ( loser . id , true ) ;
98117 return service ;
99118}
100119
@@ -109,18 +128,31 @@ export async function runBenchmark({ topK = 5, mode = "auto" } = {}) {
109128 const rows = [ ] ;
110129 let hits = 0 ;
111130 let mrrSum = 0 ;
131+ let leaks = 0 ;
112132 for ( const tc of TEST_CASES ) {
113133 const results = await service . searchMemories ( tc . query , { mode, topK, useRerank : false } ) ;
114134 const ids = results . map ( ( r ) => r . id ) ;
115135 const metrics = service . computeRetrievalMetrics ( ids , tc . expected ) ;
116136 if ( metrics . recall === 1 ) hits ++ ;
117137 mrrSum += metrics . mrr ;
118- rows . push ( { query : tc . query , note : tc . note , expected : tc . expected , got : ids , ...metrics } ) ;
138+ // forbidden 命中数:anti-update 探针的判定(见 TEST_CASES 头注释)。
139+ const forbiddenHit = ( tc . forbidden ?? [ ] ) . filter ( ( id ) => ids . includes ( id ) ) . length ;
140+ if ( forbiddenHit > 0 ) leaks ++ ;
141+ rows . push ( {
142+ query : tc . query ,
143+ note : tc . note ,
144+ expected : tc . expected ,
145+ ...( tc . forbidden ? { forbidden : tc . forbidden } : { } ) ,
146+ forbiddenHit,
147+ got : ids ,
148+ ...metrics
149+ } ) ;
119150 }
120151 runs . push ( {
121152 config : cfg . name ,
122153 recallAtK : + ( hits / TEST_CASES . length ) . toFixed ( 3 ) ,
123154 avgMrr : + ( mrrSum / TEST_CASES . length ) . toFixed ( 3 ) ,
155+ forbiddenHits : leaks ,
124156 rows
125157 } ) ;
126158 }
@@ -141,18 +173,30 @@ export async function runFusionBenchmark({ topK = 5, mode = "auto" } = {}) {
141173 const rows = [ ] ;
142174 let hits = 0 ;
143175 let mrrSum = 0 ;
176+ let leaks = 0 ;
144177 for ( const tc of TEST_CASES ) {
145178 const results = await service . searchMemories ( tc . query , { mode, topK, useRerank : false } ) ;
146179 const ids = results . map ( ( r ) => r . id ) ;
147180 const metrics = service . computeRetrievalMetrics ( ids , tc . expected ) ;
148181 if ( metrics . recall === 1 ) hits ++ ;
149182 mrrSum += metrics . mrr ;
150- rows . push ( { query : tc . query , note : tc . note , expected : tc . expected , got : ids , ...metrics } ) ;
183+ const forbiddenHit = ( tc . forbidden ?? [ ] ) . filter ( ( id ) => ids . includes ( id ) ) . length ;
184+ if ( forbiddenHit > 0 ) leaks ++ ;
185+ rows . push ( {
186+ query : tc . query ,
187+ note : tc . note ,
188+ expected : tc . expected ,
189+ ...( tc . forbidden ? { forbidden : tc . forbidden } : { } ) ,
190+ forbiddenHit,
191+ got : ids ,
192+ ...metrics
193+ } ) ;
151194 }
152195 runs . push ( {
153196 config : recipe ,
154197 recallAtK : + ( hits / TEST_CASES . length ) . toFixed ( 3 ) ,
155198 avgMrr : + ( mrrSum / TEST_CASES . length ) . toFixed ( 3 ) ,
199+ forbiddenHits : leaks ,
156200 rows
157201 } ) ;
158202 }
@@ -164,10 +208,13 @@ function printFusionReport(report) {
164208 console . log ( `\n=== ${ run . config } (topK=${ report . topK } , mode=${ report . mode } ) ===` ) ;
165209 for ( const r of run . rows ) {
166210 const ok = r . recall === 1 ? "PASS" : "MISS" ;
167- console . log ( ` [${ ok } ] "${ r . query } " (${ r . note } ) recall=${ r . recall } mrr=${ r . mrr } ` ) ;
211+ const leak = r . forbiddenHit > 0 ? " LEAK" : "" ;
212+ console . log ( ` [${ ok } ${ leak } ] "${ r . query } " (${ r . note } ) recall=${ r . recall } mrr=${ r . mrr } ` ) ;
168213 if ( r . recall < 1 ) console . log ( ` expected ⊇ ${ r . expected . join ( ", " ) } got: ${ r . got . join ( ", " ) || "—" } ` ) ;
214+ if ( r . forbiddenHit > 0 ) console . log ( ` forbidden hit ×${ r . forbiddenHit } : ${ r . got . filter ( ( id ) => r . forbidden . includes ( id ) ) . join ( ", " ) } ` ) ;
169215 }
170- console . log ( ` → Recall@${ report . topK } : ${ ( run . recallAtK * 100 ) . toFixed ( 1 ) } % avg MRR: ${ run . avgMrr } ` ) ;
216+ console . log ( ` → Recall@${ report . topK } : ${ ( run . recallAtK * 100 ) . toFixed ( 1 ) } % avg MRR: ${ run . avgMrr } ` +
217+ ( run . forbiddenHits > 0 ? ` forbiddenHits: ${ run . forbiddenHits } ` : "" ) ) ;
171218 }
172219 const summary = report . runs . map ( ( r ) => `${ r . config } =${ ( r . recallAtK * 100 ) . toFixed ( 1 ) } %` ) . join ( " " ) ;
173220 console . log ( `\n融合配方 A/B (Recall@${ report . topK } , ${ report . mode } ): ${ summary } ` ) ;
@@ -178,10 +225,13 @@ function printReport(report) {
178225 console . log ( `\n=== ${ run . config } (topK=${ report . topK } , mode=${ report . mode } ) ===` ) ;
179226 for ( const r of run . rows ) {
180227 const ok = r . recall === 1 ? "PASS" : "MISS" ;
181- console . log ( ` [${ ok } ] "${ r . query } " (${ r . note } ) recall=${ r . recall } mrr=${ r . mrr } ` ) ;
228+ const leak = r . forbiddenHit > 0 ? " LEAK" : "" ;
229+ console . log ( ` [${ ok } ${ leak } ] "${ r . query } " (${ r . note } ) recall=${ r . recall } mrr=${ r . mrr } ` ) ;
182230 if ( r . recall < 1 ) console . log ( ` expected ⊇ ${ r . expected . join ( ", " ) } got: ${ r . got . join ( ", " ) || "—" } ` ) ;
231+ if ( r . forbiddenHit > 0 ) console . log ( ` forbidden hit ×${ r . forbiddenHit } : ${ r . got . filter ( ( id ) => r . forbidden . includes ( id ) ) . join ( ", " ) } ` ) ;
183232 }
184- console . log ( ` → Recall@${ report . topK } : ${ ( run . recallAtK * 100 ) . toFixed ( 1 ) } % avg MRR: ${ run . avgMrr } ` ) ;
233+ console . log ( ` → Recall@${ report . topK } : ${ ( run . recallAtK * 100 ) . toFixed ( 1 ) } % avg MRR: ${ run . avgMrr } ` +
234+ ( run . forbiddenHits > 0 ? ` forbiddenHits: ${ run . forbiddenHits } ` : "" ) ) ;
185235 }
186236 const [ legacy , fused ] = report . runs ;
187237 const lift = ( ( fused . recallAtK - legacy . recallAtK ) * 100 ) . toFixed ( 1 ) ;
0 commit comments