Files
gbrain/test/search/return-policy.test.ts
T
730aed77f2 v0.41.33.0 feat(search): intent-aware adaptive return-sizing + agent-facing query param (#1640)
* feat(search): intent-aware adaptive return-sizing (default-off)

New opt-in retrieval feature: trim the ranked result set to an intent-driven cap (entity -> tight, else -> recall-preserving) instead of always returning top-K. Pure module src/core/search/return-policy.ts (resolve + apply + config-read + at-least-minKeep failsafe); wired into hybridSearch after rerank, before slice, offset===0 only; decision stamped into HybridSearchMeta.adaptive_return; cache skipped when on (KNOBS_HASH fold is a follow-up). SearchOpts.adaptiveReturn per-call override. Default OFF — existing search behavior byte-identical. Mechanism is an intent cap, not a score-cliff detector (PrecisionMemBench data showed the cliff carries no signal). 19 unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: bump version and changelog (v0.41.30.0)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document adaptive return-sizing module in CLAUDE.md (v0.41.30.0)

Add a Key Files entry for src/core/search/return-policy.ts (intent-aware
adaptive return-sizing, default OFF) covering its exports, the four
search.adaptive_return* config knobs, the hybrid.ts wiring (post-rerank,
pre-slice, offset===0 only), and the cache-skip behavior. Regenerate
llms-full.txt to match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(query): agent-facing adaptive_return param on the query op

Expose adaptive_return (boolean) on the query MCP/CLI op so the AGENT — not the human config knob — decides per query whether to return a tight, intent-sized set. The param description teaches WHEN (single-answer questions on; breadth off; limit:1 for a hard single-answer cap), matching the salience/recency 'YOU (the agent) decide' pattern. Threaded into hybridSearchCached. End users never touch config; their agent serves them per query. Pinned by test/search/query-op-adaptive-return.test.ts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore: re-version to v0.41.33.0 + document agent surface

Re-version 0.41.30.0 -> 0.41.33.0 (VERSION/package.json/CHANGELOG/TODOS/CLAUDE.md). CHANGELOG + CLAUDE.md now document the agent-facing query-op adaptive_return param + when-to-use guidance. llms regenerated (build-llms test green).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(core): harden fence-strip + config parsing against non-string input

stripTakesFence/stripFactsFence crashed with "undefined is not an object"
when a read op returned a page with no compiled_truth (e.g. metadata-only
rows). The get_page untrusted-reader path calls both on page.compiled_truth,
which can be undefined. Guard both to no-op when body is not a string.

loadSearchModeConfig.safeGet trusted engine.getConfig to honor its
string|null contract; a non-string value (array/boolean) reached
loadOverridesFromConfig and crashed on ce.toLowerCase(). Treat any
non-string config value as "not set" so it falls through to the
mode-bundle default, matching missing-key behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 11:21:56 -07:00

130 lines
4.9 KiB
TypeScript

/**
* Unit tests for intent-aware adaptive return-sizing (v0.42).
* Pure logic — no engine, no LLM. Pins the failsafe (never-empty),
* intent→cap mapping, config resolution precedence, and the off-by-default
* contract.
*/
import { describe, expect, test } from 'bun:test';
import {
DEFAULT_ADAPTIVE_RETURN,
adaptiveReturnFromConfig,
resolveAdaptiveReturn,
adaptiveReturnEnabled,
applyAdaptiveReturn,
type AdaptiveReturnConfig,
} from '../../src/core/search/return-policy.ts';
const items = (n: number): string[] => Array.from({ length: n }, (_, i) => `r${i}`);
describe('resolveAdaptiveReturn precedence', () => {
test('default is OFF (no behavior change)', () => {
expect(resolveAdaptiveReturn(undefined).enabled).toBe(false);
expect(DEFAULT_ADAPTIVE_RETURN.enabled).toBe(false);
});
test('per-call true enables with default caps', () => {
const c = resolveAdaptiveReturn(true);
expect(c.enabled).toBe(true);
expect(c.entityMax).toBe(DEFAULT_ADAPTIVE_RETURN.entityMax);
});
test('per-call false wins over config-enabled', () => {
const c = resolveAdaptiveReturn(false, { enabled: true });
expect(c.enabled).toBe(false);
});
test('per-call object overrides caps but inherits enabled from config when omitted', () => {
const c = resolveAdaptiveReturn({ entityMax: 1 }, { enabled: true, otherMax: 9 });
expect(c.enabled).toBe(true); // from config
expect(c.entityMax).toBe(1); // per-call override
expect(c.otherMax).toBe(9); // from config
});
test('config plane enables', () => {
const c = resolveAdaptiveReturn(undefined, { enabled: true });
expect(c.enabled).toBe(true);
});
});
describe('adaptiveReturnFromConfig', () => {
test('reads nested search.* keys with clamping', () => {
const c = adaptiveReturnFromConfig({
search: {
adaptive_return: true,
adaptive_return_entity_max: 3,
adaptive_return_other_max: 8,
adaptive_return_min_keep: 2,
},
});
expect(c).toEqual({ enabled: true, entityMax: 3, otherMax: 8, minKeep: 2 });
});
test('rejects sub-1 / non-numeric caps (falls back to defaults)', () => {
const c = adaptiveReturnFromConfig({
search: { adaptive_return_entity_max: 0, adaptive_return_other_max: 'x' },
});
expect(c.entityMax).toBe(DEFAULT_ADAPTIVE_RETURN.entityMax);
expect(c.otherMax).toBe(DEFAULT_ADAPTIVE_RETURN.otherMax);
});
test('empty / null config → empty partial', () => {
expect(adaptiveReturnFromConfig(null)).toEqual({});
expect(adaptiveReturnFromConfig({})).toEqual({});
});
});
describe('adaptiveReturnEnabled', () => {
test('true when per-call true', () => {
expect(adaptiveReturnEnabled(true, null)).toBe(true);
});
test('true when config enables', () => {
expect(adaptiveReturnEnabled(undefined, { search: { adaptive_return: true } })).toBe(true);
});
test('false by default', () => {
expect(adaptiveReturnEnabled(undefined, null)).toBe(false);
});
});
const ON = (over: Partial<AdaptiveReturnConfig> = {}): AdaptiveReturnConfig => ({
...DEFAULT_ADAPTIVE_RETURN,
enabled: true,
...over,
});
describe('applyAdaptiveReturn', () => {
test('disabled → passthrough, decision.applied=false', () => {
const { kept, decision } = applyAdaptiveReturn(items(20), 'entity', DEFAULT_ADAPTIVE_RETURN);
expect(kept.length).toBe(20);
expect(decision.applied).toBe(false);
});
test('entity intent uses entityMax', () => {
const { kept, decision } = applyAdaptiveReturn(items(20), 'entity', ON({ entityMax: 2 }));
expect(kept.length).toBe(2);
expect(decision.cap).toBe(2);
expect(decision.applied).toBe(true);
});
test('non-entity intent uses otherMax', () => {
for (const intent of ['temporal', 'event', 'general'] as const) {
const { kept } = applyAdaptiveReturn(items(20), intent, ON({ otherMax: 5 }));
expect(kept.length).toBe(5);
}
});
test('FAILSAFE: never empty when candidates exist (cap floored to minKeep)', () => {
// minKeep defaults to 1; even a degenerate cap can't zero the result.
const { kept } = applyAdaptiveReturn(items(10), 'entity', ON({ entityMax: 1, minKeep: 1 }));
expect(kept.length).toBe(1);
});
test('empty input stays empty (no fabrication)', () => {
const { kept, decision } = applyAdaptiveReturn([], 'entity', ON());
expect(kept.length).toBe(0);
expect(decision.applied).toBe(false);
});
test('cap larger than result set keeps all', () => {
const { kept } = applyAdaptiveReturn(items(2), 'general', ON({ otherMax: 6 }));
expect(kept.length).toBe(2);
});
test('preserves order (returns the ranked prefix)', () => {
const { kept } = applyAdaptiveReturn(items(5), 'entity', ON({ entityMax: 3 }));
expect(kept).toEqual(['r0', 'r1', 'r2']);
});
test('minKeep > cap still respected (recall floor wins)', () => {
const { kept } = applyAdaptiveReturn(items(10), 'entity', ON({ entityMax: 1, minKeep: 3 }));
expect(kept.length).toBe(3);
});
});