Files
gbrain/test/e2e/whoknows.test.ts
T
d71fcf6f65 v0.33.1.0 feat: eval-gated whoknows — expertise + relationship-proximity routing (#881)
* feat(v0.33): add SearchOpts.types multi-type filter to searchHybrid

Push the page-type filter into SQL via AND p.type = ANY(\$N::text[]) in
both engines' searchKeyword + searchVector + searchKeywordChunks paths.
Primary consumer is the upcoming gbrain whoknows command (filters to
['person','company']); the limit budget then goes to typed candidates
instead of being eaten by note/transcript/article pages. Future
entity-only search in v0.34+ reuses the parameter for free.

AND-applies alongside the existing single-value type filter (callers can
use either or both). HybridSearchOpts threads opts.types into the
underlying searchOpts so hybridSearch callers get the SQL-level filter
without any post-filter waste.

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

* feat(v0.33): whoknows core ranking function + 10 locked unit tests

Implements ENG-D1's locked spec: score = log(1 + raw_match) ×
max(0.1, exp(-days/180)) × (0.5 + 0.5 × salience). raw_match comes
from hybridSearch's RRF + source-boost-adjusted score; salience and
recency boosts in hybridSearch are intentionally disabled so the
formula applies on a clean signal.

rankCandidates() is the pure function the eval grades against;
findExperts() is the public entrypoint that wires hybrid search +
batch salience/effective_date fetches; runWhoknows() is the CLI.

Test/whoknows.test.ts covers the 10 ENG-D3 cases (zero results,
negative recency floor, NaN salience neutral default, NaN match
zeros gracefully, type preservation, --explain factor breakdown,
top-K limit clamping, recency-floor extreme-days safety, alphabetical
tie-break determinism, public-surface contract). Plus four sanity
asserts (higher-match outranks, more-recent outranks, higher-salience
outranks, all-zero candidate appears with score 0). Plus one factor
decomposition assertion that pins the exact formula numerically.
Plus a composite-key safety case (Codex F1).

22 expect calls across 16 tests. All passing.

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

* feat(v0.33): register find_experts MCP op + gbrain whoknows CLI

Wires both surfaces per ENG-D5: MCP op = find_experts (matches
find_anomalies naming convention; agent-facing); CLI command =
gbrain whoknows (memorable, user-facing). One findExperts() core
function backs both paths.

The op is scope:'read', localOnly:false — accessible over HTTP MCP
to read-scoped OAuth clients like the salience/anomalies family.
Op handler validates non-empty topic and dispatches to the same
findExperts() pure function the CLI uses.

CLI dispatch in src/cli.ts:case 'whoknows' calls runWhoknows; thin-
client routing happens inside runWhoknows via isThinClient(cfg) —
remote MCP installs route through the v0.31.1 routing seam to
callRemoteTool('find_experts', ...).

FIND_EXPERTS_DESCRIPTION in operations-descriptions.ts mirrors the
v0.29 redirect-hint style: leads with what the tool does, lists
explicit user-intent triggers ("who should I talk to about X",
"who knows about Y"), notes the type-filter behavior.

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

* feat(v0.33): gbrain eval whoknows — two-layer eval gate (ENG-D2)

Implements the locked spec: Layer 1 hand-labeled fixture (>=80% top-3
hit rate) is the primary ship-blocking gate; Layer 2 eval_candidates
replay (>=0.4 mean set-Jaccard@3) is the regression gate that
auto-skips when < 20 replay-eligible rows exist (CONTRIBUTOR_MODE
sparseness fallback).

Dispatch lands as `gbrain eval whoknows <fixture.jsonl>` sub-subcommand
in src/commands/eval.ts (mirrors v0.25.0 export/prune/replay and
v0.27.x cross-modal pattern). Exits 0/1/2 for pass/fail/usage so CI
gates can consume.

JSON output (--json) ships schema_version: 1 for stable consumer
contract (mirrors v0.25.0 eval-replay.ts). Human output groups by
layer + emits a per-miss diagnostic table so failures are
self-debugging.

Unit tests pin:
- jaccardAtK math (7 cases — identical, disjoint, partial, k cutoff,
  empty-empty vacuous-stable, empty-vs-non-empty, Set dedup)
- topKHit (7 cases — position 1, 3, 4, miss, multi-expected, empty
  actual, empty expected)
- readFixture (6 cases — well-formed, comments/blanks, missing file,
  malformed JSON, missing required fields, non-string filter)
- Locked thresholds (HIT_RATE=0.8, REGRESSION=0.4, MIN_REPLAY_ROWS=20)

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

* feat(v0.33): gbrain doctor adds whoknows_health check

Per CEO-D7 (substrate-conditional v0.33 doctor check, but the
fixture-presence sub-check ships in week 1 regardless — it's the
"did you do the assignment?" signal). When the eval fixture is
missing, empty, or undersized (< 5 rows), doctor warns with the
exact path the user should populate.

The check is intentionally lightweight: it does NOT run the eval
itself or measure hit-rate regression. That's the job of `gbrain
eval whoknows`, called from CI/ship time. This check is the cheap
always-runs signal that surfaces in `gbrain doctor` and on the
ship review dashboard.

5 unit cases pin the four-status behavior (missing/empty/undersized/
ok) plus the comment-and-blank-line filtering so users can comment
out queries during iteration without breaking the row count.

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

* feat(v0.33): synthetic whoknows eval fixture + E2E quality gate test

test/fixtures/whoknows-eval.jsonl ships as a 10-query placeholder
demonstrating the schema. Comments document the assignment for end
users: they replace these with their own real queries before
shipping their gbrain install. The placeholder uses obviously-
example slugs (wiki/people/example-alice, etc.) so nobody mistakes
it for production data.

test/e2e/whoknows.test.ts seeds a synthetic PGLite brain that
matches the placeholder fixture, then runs findExperts on every
fixture query and asserts >=80% top-3 hit rate per ENG-D2 quality
gate. Also exercises the typeFilter (concept-decoy pages filtered
out), empty-result graceful return, --explain factor breakdown, and
top-K limit honoring.

Basis-vector embeddings (no API key) follow the existing pattern from
test/e2e/search-quality.test.ts.

5 test cases, 23 expect calls, all passing against PGLite.

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

* docs(v0.33): VERSION bump + CHANGELOG + CLAUDE.md + llms regen

Bumps VERSION 0.31.11 → 0.33.0 and package.json to match. CHANGELOG
entry leads with the headline use ("ask gbrain who knows about X")
and the locked ENG-D1 ranking formula. "Numbers that matter" replaced
with a "what ships on which eval outcome" table — honest about the
eval-gated trajectory rather than fabricating benchmarks before the
release has been graded against a real brain.

CLAUDE.md Key Files annotations added for src/commands/whoknows.ts,
src/commands/eval-whoknows.ts, and test/fixtures/whoknows-eval.jsonl.
src/core/search/hybrid.ts entry extended with the new types parameter
documentation (push the type filter to SQL, no post-filter waste,
AND-applies alongside the existing single-value type field).

bun run build:llms ran the chaser; llms.txt + llms-full.txt
regenerated to match.

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

* test(v0.33): unit-test gap fill — engine typeFilter + find_experts op

Two new files filling the gaps Garry called out:

test/search-types-filter.test.ts — engine-level coverage on PGLite for
the new SearchOpts.types filter. Asserts the SQL-clause behavior
directly so a regression in the AND p.type = ANY(...) emission gets
caught here with a tight assertion rather than as part of a longer
findExperts pipeline. 9 cases across searchKeyword + searchVector +
chunk-grain documentation. Documents the pre-existing PGLite parity
gap (single-value `type` field is Postgres-only; `types` is the v0.33
multi-type filter that BOTH engines honor).

test/find-experts-op.test.ts — MCP-op contract test for find_experts.
Pins:
- Registered in the operations array + operationsByName
- scope: 'read', localOnly false (HTTP-MCP accessible per ENG-D5)
- Documented params (topic / limit / explain) with correct types
- cliHints.name === 'whoknows' (CLI surface bridge)
- Non-trivial description that references the use case
- Handler rejects empty / whitespace / missing topic with invalid_params
- Handler returns array shape on valid topic
- Handler honors limit param

11 op-contract cases + 9 engine-clause cases. All passing.

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

* chore: bump version to v0.33.1.0

Garry asked for v0.33.1 instead of v0.33.0 (queue collision with
unrelated 0.33.0 work). 4-digit format: 0.33.1.0. CHANGELOG header
and "To take advantage of" block updated. llms.txt regenerated.

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

* fix(v0.33.1.1): cliHints.positional on find_experts so CLI accepts <topic>

Without `cliHints.positional: ['topic']`, the op-dispatch path in
src/cli.ts couldn't parse `gbrain whoknows "ai agents"` and threw
`invalid_params: topic is required`. Found while testing the v0.33.1.0
build against a real brain. The op handler validates topic; the CLI
just needed to know the positional shape so the dispatcher could
hand it through.

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

* test(v0.33.1.2): real-brain whoknows-eval fixture from VC intro network

Replaces the synthetic 10-row placeholder with 10 real expertise-routing
queries mined from Garry's actual brain via thin-client connection to
Wintermute (v0.32.2). Source: reference/vc-intro-network ("Who Takes
Intros from Garry") + adjacent routing context. All 15 unique expected
person slugs verified against ~/git/brain/people/<slug>.md source
markdown:

  people/amit-kumar          Accel partner, 102 YC deals
  people/diana-hu            YC GP
  people/elad-gil            Angel, top-rated
  people/eric-vishria        Benchmark, healthtech
  people/gokul-rajaram       Angel, 57 YC deals
  people/joff-redfern        Menlo Ventures, ex-CPO Atlassian
  people/jon-xu              YC GP
  people/kristina-shen       Chemistry, healthtech
  people/lachy-groom         Angel, 43 YC deals
  people/lee-edwards         Quiet Capital, 52 YC deals
  people/nick-shalek         Ribbit Capital, fintech
  people/nina-achadian       Index Ventures, 69 YC deals (note: slug
                              uses 'achadian' not 'achadjian')
  people/parul-singh         645 Ventures
  people/rebecca-kaden       USV
  people/trae-stephens       Founders Fund, defense/deep-tech

Eval cannot run yet against Wintermute thin-client: server is v0.32.2,
find_experts MCP op was added in v0.33. Once Wintermute upgrades the
eval will run end-to-end via the v0.31.1 thin-client routing seam.
Local eval works once the brain is indexed with find_experts available.

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

* feat(v0.33.1.3): wire thin-client routing into eval-whoknows

`gbrain eval whoknows` now works against a thin-client install. When
isThinClient(cfg), each fixture query routes through the remote
find_experts MCP op via callRemoteTool — same v0.31.1 routing seam
runWhoknows already uses. Local mode unchanged: findExperts(engine, ...)
called directly.

Server prerequisite: the brain must be v0.33+ for find_experts to be
registered. Wintermute (currently v0.32.2) gets it on next upgrade and
then the eval runs end-to-end with zero client-side changes.

Mechanics:
- `WhoknowsFn` callable abstraction so the gates are impl-agnostic
- runEvalWhoknows(engine: BrainEngine | null, args) — null engine
  allowed in thin-client mode
- Regression gate auto-skips in thin-client mode (no DB access to
  eval_candidates; quality gate alone gates ship)
- cli.ts adds a thin-client bypass before connectEngine for
  `gbrain eval whoknows`, matching the longmemeval/cross-modal no-DB
  pattern

E2E test updated to use an inline synthetic fixture (the shipped
fixture is real-brain data now, doesn't match the seeded test brain).
Sanity-check the shipped fixture parses cleanly in a separate case.

Tests: 25 unit cases (+2 for null-engine signature contract) + 6 E2E
cases. Typecheck clean.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 14:33:29 -07:00

236 lines
9.7 KiB
TypeScript

/**
* v0.33 whoknows E2E — full pipeline against a seeded PGLite brain.
*
* Seeds a synthetic brain matching test/fixtures/whoknows-eval.jsonl,
* runs gbrain eval whoknows --skip-replay over the fixture, asserts
* the quality gate passes >= 80% top-3 hit rate. Also exercises:
*
* - findExperts() directly with --types filter
* - Person/company filtering excludes other types
* - Empty result returns empty array (not crash)
* - --explain output includes factor breakdown
*
* Mock embeddings via basis vectors (no OpenAI key needed). Uses the
* same pattern as test/e2e/search-quality.test.ts.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
import type { ChunkInput } from '../../src/core/types.ts';
import { findExperts } from '../../src/commands/whoknows.ts';
import { readFixture } from '../../src/commands/eval-whoknows.ts';
let engine: PGLiteEngine;
function basisEmbedding(idx: number, dim = 1536): Float32Array {
const emb = new Float32Array(dim);
emb[idx % dim] = 1.0;
return emb;
}
async function seedPerson(
slug: string,
title: string,
topic: string,
embeddingIdx: number,
) {
await engine.putPage(slug, {
type: 'person',
title,
compiled_truth: `${title} is an expert in ${topic}. Built career around ${topic}.`,
timeline: `2024-01-01: ${title} on ${topic} project.`,
});
const chunks: ChunkInput[] = [
{
chunk_index: 0,
chunk_text: `${title} is an expert in ${topic}. Built career around ${topic}.`,
chunk_source: 'compiled_truth',
embedding: basisEmbedding(embeddingIdx),
token_count: 15,
},
{
chunk_index: 1,
chunk_text: `2024-01-01: ${title} on ${topic} project.`,
chunk_source: 'timeline',
embedding: basisEmbedding(embeddingIdx + 100),
token_count: 10,
},
];
await engine.upsertChunks(slug, chunks);
}
async function seedCompany(
slug: string,
title: string,
topic: string,
embeddingIdx: number,
) {
await engine.putPage(slug, {
type: 'company',
title,
compiled_truth: `${title} is a company focused on ${topic}. Leader in ${topic}.`,
timeline: `2024-01-01: ${title} ${topic} milestone.`,
});
const chunks: ChunkInput[] = [
{
chunk_index: 0,
chunk_text: `${title} is a company focused on ${topic}. Leader in ${topic}.`,
chunk_source: 'compiled_truth',
embedding: basisEmbedding(embeddingIdx),
token_count: 15,
},
];
await engine.upsertChunks(slug, chunks);
}
async function seedConcept(
slug: string,
title: string,
topic: string,
embeddingIdx: number,
) {
await engine.putPage(slug, {
type: 'concept',
title,
compiled_truth: `${topic} is an important concept. Many explore ${topic}.`,
timeline: `2024-01-01: notes on ${topic}.`,
});
const chunks: ChunkInput[] = [
{
chunk_index: 0,
chunk_text: `${topic} is an important concept. Many explore ${topic}.`,
chunk_source: 'compiled_truth',
embedding: basisEmbedding(embeddingIdx),
token_count: 12,
},
];
await engine.upsertChunks(slug, chunks);
}
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
// People matching the synthetic fixture topics.
await seedPerson('wiki/people/example-alice', 'Alice Example', 'fintech payments', 10);
await seedPerson('wiki/people/example-bob', 'Bob Example', 'crypto investing', 12);
await seedPerson('wiki/people/example-carol', 'Carol Example', 'ai agents', 14);
await seedPerson('wiki/people/example-dave', 'Dave Example', 'distributed systems', 16);
await seedPerson('wiki/people/example-eve', 'Eve Example', 'healthcare technology', 18);
await seedPerson('wiki/people/example-frank', 'Frank Example', 'developer tools', 20);
await seedPerson('wiki/people/example-grace', 'Grace Example', 'machine learning research', 22);
await seedPerson('wiki/people/example-hank', 'Hank Example', 'climate tech', 24);
await seedPerson('wiki/people/example-ivy', 'Ivy Example', 'enterprise sales', 26);
await seedPerson('wiki/people/example-jake', 'Jake Example', 'hardware engineering', 28);
// Companies matching the synthetic fixture topics.
await seedCompany('wiki/companies/example-fintech-co', 'FintechCo', 'fintech payments', 11);
await seedCompany('wiki/companies/example-fund', 'CryptoFund', 'crypto investing', 13);
await seedCompany('wiki/companies/example-health-co', 'HealthCo', 'healthcare technology', 19);
await seedCompany('wiki/companies/example-devtools-co', 'DevtoolsCo', 'developer tools', 21);
await seedCompany('wiki/companies/example-climate-co', 'ClimateCo', 'climate tech', 25);
await seedCompany('wiki/companies/example-hardware-co', 'HardwareCo', 'hardware engineering', 29);
// Decoy non-person/non-company pages with the same topics (filter should hide).
await seedConcept('concepts/fintech-essay', 'Fintech Essay', 'fintech payments', 30);
await seedConcept('concepts/crypto-thoughts', 'Crypto Thoughts', 'crypto investing', 31);
}, 120_000);
afterAll(async () => {
if (engine) await engine.disconnect();
});
describe('whoknows E2E — quality gate on synthetic fixture', () => {
test('runs findExperts and the fixture quality gate at >= 80% hit rate', async () => {
// v0.33.1.3: The shipped fixture at test/fixtures/whoknows-eval.jsonl
// is now real-brain data (people/eric-vishria, etc.) — those slugs
// don't exist in this E2E's synthetic seed. We define an inline
// synthetic fixture matching the seed above. Production users replace
// the shipped fixture with their own real queries; this test verifies
// the eval pipeline mechanically, not against shipped data.
const inlineFixture = [
{ query: 'fintech payments', expected: ['wiki/people/example-alice', 'wiki/companies/example-fintech-co'] },
{ query: 'crypto investing', expected: ['wiki/companies/example-fund', 'wiki/people/example-bob'] },
{ query: 'ai agents', expected: ['wiki/people/example-carol'] },
{ query: 'distributed systems', expected: ['wiki/people/example-dave'] },
{ query: 'healthcare technology', expected: ['wiki/companies/example-health-co', 'wiki/people/example-eve'] },
{ query: 'developer tools', expected: ['wiki/people/example-frank', 'wiki/companies/example-devtools-co'] },
{ query: 'machine learning research', expected: ['wiki/people/example-grace'] },
{ query: 'climate tech', expected: ['wiki/companies/example-climate-co', 'wiki/people/example-hank'] },
{ query: 'enterprise sales', expected: ['wiki/people/example-ivy'] },
{ query: 'hardware engineering', expected: ['wiki/people/example-jake', 'wiki/companies/example-hardware-co'] },
];
let hits = 0;
for (const row of inlineFixture) {
const results = await findExperts(engine, { topic: row.query, limit: 5 });
const top3 = new Set(results.slice(0, 3).map((r) => r.slug));
const hit = row.expected.some((s) => top3.has(s));
if (hit) hits++;
}
const hitRate = hits / inlineFixture.length;
// Synthetic seed designed so every query has a clear best match.
// Assert >= 80% (the locked ENG-D2 threshold). In practice 100% on
// this controlled fixture.
expect(hitRate).toBeGreaterThanOrEqual(0.8);
}, 60_000);
test('shipped fixture at test/fixtures/whoknows-eval.jsonl loads and parses', () => {
// Sanity check that the shipped (real-brain) fixture exists and parses.
// Doesn't assert hit rate — the seeded brain doesn't have those slugs.
const fixture = readFixture(
`${process.cwd()}/test/fixtures/whoknows-eval.jsonl`,
);
expect(fixture.length).toBeGreaterThanOrEqual(5);
for (const row of fixture) {
expect(typeof row.query).toBe('string');
expect(row.expected_top_3_slugs.length).toBeGreaterThanOrEqual(1);
}
});
});
describe('whoknows E2E — typeFilter and shadow paths', () => {
test('type filter excludes concept pages (decoys do not appear in results)', async () => {
const results = await findExperts(engine, { topic: 'fintech payments', limit: 10 });
expect(results.length).toBeGreaterThan(0);
for (const r of results) {
expect(['person', 'company']).toContain(r.type);
}
// The decoy concept page must NOT appear.
expect(results.find((r) => r.slug === 'concepts/fintech-essay')).toBeUndefined();
});
test('zero matches returns empty array gracefully', async () => {
const results = await findExperts(engine, {
topic: 'this-topic-is-definitely-not-in-the-brain-xyzqwerty',
limit: 5,
});
expect(Array.isArray(results)).toBe(true);
// searchHybrid may return loosely-matching results based on stemming;
// we just assert it doesn't crash and returns sanely.
expect(results.length).toBeGreaterThanOrEqual(0);
});
test('--explain factor breakdown is present on every result', async () => {
const results = await findExperts(engine, { topic: 'crypto investing', limit: 3 });
expect(results.length).toBeGreaterThan(0);
for (const r of results) {
expect(r.factors).toBeDefined();
expect(typeof r.factors.expertise).toBe('number');
expect(typeof r.factors.recency_factor).toBe('number');
expect(typeof r.factors.salience).toBe('number');
expect(typeof r.score).toBe('number');
expect(Number.isFinite(r.score)).toBe(true);
}
});
test('top-K honors limit parameter', async () => {
const r5 = await findExperts(engine, { topic: 'developer tools', limit: 5 });
const r1 = await findExperts(engine, { topic: 'developer tools', limit: 1 });
expect(r5.length).toBeGreaterThanOrEqual(r1.length);
expect(r1.length).toBeLessThanOrEqual(1);
});
});