mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 17:02:19 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ff32a8773 | ||
|
|
683b7665f2 |
@@ -1777,6 +1777,13 @@ export async function hybridSearchCached(
|
||||
// resolves) into the cache key so a row written under one exclude
|
||||
// policy can't be served to a lookup under another.
|
||||
hardExcludes: resolveHardExcludes(opts?.exclude_slug_prefixes, opts?.include_slug_prefixes),
|
||||
// #3515 — fold the EFFECTIVE detail level into the cache key. detail
|
||||
// gates dedup, chunk-source filtering, and the compiled_truth boost, so
|
||||
// a `--detail low` write (compiled-truth-only result set) must never be
|
||||
// served to a default `medium` lookup. Resolve auto-detect the same way
|
||||
// bare hybridSearch does (opts.detail ?? autoDetectDetail(query)) so an
|
||||
// auto-detected `high` query keys like an explicit `high` one.
|
||||
detail: opts?.detail ?? autoDetectDetail(query),
|
||||
});
|
||||
|
||||
// Cache decision: opts.useCache (explicit) wins over global config; global
|
||||
|
||||
+27
-1
@@ -766,7 +766,17 @@ export function attributeKnob<K extends keyof ModeBundle>(
|
||||
// written between the #3391 stale-fix (which changes which chunks count as
|
||||
// current) and the operator's migration run. Same one-time global cold-miss
|
||||
// pattern as the bumps above.
|
||||
export const KNOBS_HASH_VERSION = 13;
|
||||
//
|
||||
// bump 13→15 (#3515): `detail` folds into the key via ctx.detail (det=).
|
||||
// detail is result-affecting by design — it gates dedup, chunk-source
|
||||
// filtering, and the compiled_truth boost — but was absent from the key, so
|
||||
// a `--detail low` write (compiled-truth-only result set) was served to a
|
||||
// default `medium` lookup for the whole TTL. Same contamination class as
|
||||
// [CDX-4], floor_ratio (v=3), and relationalRetrieval (v=10). v=14 is
|
||||
// claimed by in-flight #3514 (compiled_truth boost scope, #3430), so this
|
||||
// lands as v=15 per the D8 sequencing convention (see the v=4/v=5 note
|
||||
// above). Same one-time global cold-miss pattern as the bumps above.
|
||||
export const KNOBS_HASH_VERSION = 15;
|
||||
|
||||
/**
|
||||
* v0.36 (D8 / CDX-2) — second-arg context for the cache key. The
|
||||
@@ -805,6 +815,17 @@ export interface KnobsHashContext {
|
||||
* 'none' for legacy callers that don't thread excludes.
|
||||
*/
|
||||
hardExcludes?: string[];
|
||||
/**
|
||||
* v=15 (#3515): the EFFECTIVE detail level for this call — per-call
|
||||
* SearchOpts.detail, or the auto-detected level when the caller didn't
|
||||
* specify (hybridSearchCached threads `opts.detail ?? autoDetectDetail(query)`,
|
||||
* matching what bare hybridSearch resolves). detail gates dedup,
|
||||
* chunk-source filtering, and the compiled_truth boost, so a detail=low
|
||||
* write must never be served to a detail=medium lookup. Lives in ctx (not
|
||||
* ResolvedSearchKnobs) because it's per-call, not a mode knob — same path
|
||||
* as col=/prov=. Undefined falls back to 'medium' (the documented default).
|
||||
*/
|
||||
detail?: 'low' | 'medium' | 'high';
|
||||
}
|
||||
|
||||
export function knobsHash(
|
||||
@@ -898,6 +919,11 @@ export function knobsHash(
|
||||
// across processes. Sorted copy so ['a/','b/'] and ['b/','a/'] hash
|
||||
// identically; undefined falls back to 'none' for legacy callers.
|
||||
`hx=${ctx?.hardExcludes ? [...ctx.hardExcludes].sort().join(',') : 'none'}`,
|
||||
// v=15 addition (#3515, append-only): effective detail level. detail
|
||||
// gates dedup, chunk-source filtering, and the compiled_truth boost, so
|
||||
// a low write (compiled-truth-only set) must never be served to a
|
||||
// medium/high lookup. Undefined falls back to 'medium' (the default).
|
||||
`det=${ctx?.detail ?? 'medium'}`,
|
||||
];
|
||||
const h = createHash('sha256');
|
||||
h.update(parts.join('|'));
|
||||
|
||||
@@ -136,7 +136,7 @@ describe('D2 — knobsHash differs across cross-modal knob values', () => {
|
||||
return resolveSearchMode({ mode: 'balanced' });
|
||||
}
|
||||
|
||||
test('KNOBS_HASH_VERSION is 13 (cross-modal still appended; 12→13 embedding-provider migration #3390)', () => {
|
||||
test('KNOBS_HASH_VERSION is 15 (cross-modal still appended; 13→15 detail fold #3515)', () => {
|
||||
// v0.35 ladder: 1→2 reranker, 2→3 floor_ratio. v0.36 piggybacks on v=3
|
||||
// with 7 cross-modal knobs + column/provider context. v0.40.4 (salem) +
|
||||
// v0.39 T21 (master) bump to v=4 for graph_signals + schema-pack fields.
|
||||
@@ -146,7 +146,8 @@ describe('D2 — knobsHash differs across cross-modal knob values', () => {
|
||||
// v0.43: 9→10 relational recall arm. #1400: 10→11 query-side input_type
|
||||
// finally reaches asymmetric providers — pre-fix rows were keyed on
|
||||
// document-side query vectors. #2825: 11→12 hard-exclude fold (hx=).
|
||||
expect(KNOBS_HASH_VERSION).toBe(13);
|
||||
// #3515: 13→15 detail fold (det=); v=14 claimed by in-flight #3514.
|
||||
expect(KNOBS_HASH_VERSION).toBe(15);
|
||||
});
|
||||
|
||||
test('flipping unified_multimodal changes the hash', () => {
|
||||
|
||||
@@ -277,3 +277,38 @@ describe('hard-exclude cache isolation (#2825)', () => {
|
||||
expect((await cache.lookup(emb, { knobsHash: envExcludeHash })).hit).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('detail cache isolation (#3515)', () => {
|
||||
// Hashes computed the way hybridSearchCached does: same resolved mode, ctx
|
||||
// carrying the effective detail level. A row written by a `--detail low`
|
||||
// call (compiled-truth-only result set) must not be served to a default
|
||||
// `medium` lookup, and vice versa.
|
||||
const lowHash = knobsHash(resolveSearchMode({ mode: 'balanced' }), { detail: 'low' });
|
||||
const mediumHash = knobsHash(resolveSearchMode({ mode: 'balanced' }), { detail: 'medium' });
|
||||
const unsetHash = knobsHash(resolveSearchMode({ mode: 'balanced' }));
|
||||
|
||||
test('detail=low write is NOT served to a default (medium) lookup', async () => {
|
||||
const cache = new SemanticQueryCache(engine);
|
||||
const emb = makeEmbedding(8);
|
||||
|
||||
// Simulate `query "X" --detail low` populating the cache with the
|
||||
// narrow compiled-truth-only result set.
|
||||
await cache.store('what is the deploy process', emb, makeResults('narrow', 2), {
|
||||
vector_enabled: true, detail_resolved: 'low', expansion_applied: false,
|
||||
}, { knobsHash: lowHash });
|
||||
|
||||
// Default-detail lookup inside the TTL → MISS (falls through to a
|
||||
// fresh, full search) instead of the narrow set.
|
||||
expect((await cache.lookup(emb, { knobsHash: mediumHash })).hit).toBe(false);
|
||||
|
||||
// The low-detail caller still hits its own row.
|
||||
const original = await cache.lookup(emb, { knobsHash: lowHash });
|
||||
expect(original.hit).toBe(true);
|
||||
expect(original.results?.length).toBe(2);
|
||||
});
|
||||
|
||||
test('undefined detail keys like the documented medium default', () => {
|
||||
expect(unsetHash).toBe(mediumHash);
|
||||
expect(unsetHash).not.toBe(lowHash);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('alias_resolved boost stage', () => {
|
||||
});
|
||||
|
||||
describe('KNOBS_HASH_VERSION', () => {
|
||||
it('is 13 (12→13 embedding-provider migration invalidates rows written against the prior embedding space, #3390)', () => {
|
||||
expect(KNOBS_HASH_VERSION).toBe(13);
|
||||
it('is 15 (13→15 detail fold makes detail-contaminated rows unreachable, #3515; v=14 claimed by in-flight #3514)', () => {
|
||||
expect(KNOBS_HASH_VERSION).toBe(15);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -413,7 +413,24 @@ describe('knobsHash determinism + cross-mode separation (CDX-4)', () => {
|
||||
// #3390/#3391: bumped 12→13 for the embedding-provider migration wave —
|
||||
// legacy callers hash prov=default before AND after a provider swap, so
|
||||
// pre-migration cache rows must become unreachable on upgrade.
|
||||
expect(KNOBS_HASH_VERSION).toBe(13);
|
||||
// #3515: bumped 13→15 to fold the effective detail level (det=) — a
|
||||
// detail=low write must not be served to a detail=medium lookup. v=14
|
||||
// is claimed by in-flight #3514 (#3430 compiled_truth boost scope).
|
||||
expect(KNOBS_HASH_VERSION).toBe(15);
|
||||
});
|
||||
|
||||
test('#3515: detail set vs unset produces DIFFERENT hashes (cache contamination prevention)', () => {
|
||||
const knobs = resolveSearchMode({ mode: 'balanced' });
|
||||
const low = knobsHash(knobs, { detail: 'low' });
|
||||
const medium = knobsHash(knobs, { detail: 'medium' });
|
||||
const high = knobsHash(knobs, { detail: 'high' });
|
||||
const unset = knobsHash(knobs);
|
||||
expect(low).not.toBe(medium);
|
||||
expect(medium).not.toBe(high);
|
||||
expect(low).not.toBe(high);
|
||||
// Undefined falls back to 'medium' — the documented default — so legacy
|
||||
// callers that don't thread detail share the default-detail rows.
|
||||
expect(unset).toBe(medium);
|
||||
});
|
||||
|
||||
test('T1 (codex): floor_ratio set vs unset produces DIFFERENT hashes (cache contamination prevention)', () => {
|
||||
@@ -578,8 +595,8 @@ describe('v0.40.4 — graph_signals knob', () => {
|
||||
});
|
||||
|
||||
describe('v0.42.3.0 — autocut knobs', () => {
|
||||
test('KNOBS_HASH_VERSION is 13 (12→13 embedding-migration wave, #3390/#3391)', () => {
|
||||
expect(KNOBS_HASH_VERSION).toBe(13);
|
||||
test('KNOBS_HASH_VERSION is 15 (13→15 detail fold #3515; v=14 claimed by in-flight #3514)', () => {
|
||||
expect(KNOBS_HASH_VERSION).toBe(15);
|
||||
});
|
||||
|
||||
test('bundle defaults: conservative off, balanced/tokenmax on @0.20', () => {
|
||||
|
||||
@@ -44,7 +44,7 @@ function baseKnobs(): ResolvedSearchKnobs {
|
||||
}
|
||||
|
||||
describe('KNOBS_HASH_VERSION + version invariants', () => {
|
||||
test('version is 13 (…; 10→11 asymmetric input_type #1400; 11→12 hard-excludes #2825; 12→13 embedding-provider migration #3390)', () => {
|
||||
test('version is 15 (…; 11→12 hard-excludes #2825; 12→13 embedding-provider migration #3390; 13→15 detail fold #3515)', () => {
|
||||
// v0.35.0.0: 1→2 to fold reranker fields. v0.35.6.0: 2→3 to fold
|
||||
// floor_ratio. v0.36 wave: piggybacks on v=3 with 7 cross-modal knobs
|
||||
// (D2) PLUS column + provider context (D8/CDX-2 cross-column isolation).
|
||||
@@ -64,7 +64,10 @@ describe('KNOBS_HASH_VERSION + version invariants', () => {
|
||||
// pre-fix document-side query vectors must not be served.
|
||||
// #2825: 11→12 to fold the resolved hard-exclude prefix list (hx=) —
|
||||
// cached rows leaked GBRAIN_SEARCH_EXCLUDE'd slugs across processes.
|
||||
expect(KNOBS_HASH_VERSION).toBe(13);
|
||||
// #3515: 13→15 to fold the effective detail level (det=) — a detail=low
|
||||
// write must not be served to a detail=medium lookup. v=14 claimed by
|
||||
// in-flight #3514 (#3430).
|
||||
expect(KNOBS_HASH_VERSION).toBe(15);
|
||||
});
|
||||
|
||||
test('hash is 16 hex chars regardless of reranker config', () => {
|
||||
|
||||
Reference in New Issue
Block a user