Files
gbrain/eval/runner/queries/validator.test.ts
T
Garry TanandClaude Opus 4.7 e2a5dc4a2b feat(eval): Phase 2 query validator + Tier 5 Fuzzy + Tier 5.5 synthetic + N=5 tolerance bands
Closes multiple Phase 2 items in one commit since they form a cohesive
package: query schema enforcement + new query tiers + per-query-set
statistical rigor.

Added:
  eval/runner/queries/validator.ts               — hand-rolled Query schema validator
  eval/runner/queries/validator.test.ts          — 24 unit tests, all pass
  eval/runner/queries/tier5-fuzzy.ts             — 30 hand-authored Tier 5 Fuzzy/Vibe queries
  eval/runner/queries/tier5_5-synthetic.ts       — 50 SYNTHETIC-labeled outsider-style queries (author: "synthetic-outsider-v1")
  eval/runner/queries/index.ts                   — aggregator + validateAll()

Modified:
  eval/runner/multi-adapter.ts                   — N=5 runs per adapter (BRAINBENCH_N override), page-order shuffle, mean±stddev reporting

Query validator (hand-rolled, no zod dep to match gbrain codebase style):
  - Temporal verb regex enforces as_of_date (per eng pass 2 spec):
    /\\b(is|was|were|current|now|at the time|during|as of|when did)\\b/i
  - Validates tier enum, expected_output_type enum, gold shape per type
  - gold.relevant must be non-empty slug[] for cited-source-pages queries
  - abstention requires gold.expected_abstention === true
  - externally-authored tier requires author field
  - batch validation catches duplicate IDs

Tier 5 Fuzzy/Vibe (30 queries, hand-authored):
  - Vague recall: "Someone who was a senior engineer at a biotech company..."
  - Trait-based: "The engineer who pushed back on microservices"
  - Cultural/epithet: "Who is known as a 'systems builder' in security?"
  - Abstention bait: "Which Layer 1 project did the crypto guy leave?" (prose
    mentions but never names; good systems abstain)
  - Addresses Codex's circularity critique — vague queries where graph-heavy
    systems shouldn't inherently win.

Tier 5.5 Synthetic Outsider (50 queries, AI-authored placeholder):
  - Clearly labeled author: "synthetic-outsider-v1"
  - Phrasing variety not in the 4 template families:
    * fragment style ("crypto founder Goldman Sachs background")
    * polite/natural ("Can you pull up what we have on...")
    * comparison ("What is the difference between X and Y?")
    * follow-up ("And who else advises Orbit Labs?")
    * typos/misspellings ("adam lopez bioinformatcis")
    * similarity ("Find me someone like Alice Davis...")
    * imperative ("Pull up Alice Davis")
  - Real Tier 5.5 from outside researchers supersedes synthetic via
    PRs to eval/external-authors/ (docs ship in follow-up commit).

N=5 tolerance bands:
  - Default N=5, override via BRAINBENCH_N env var (e.g. BRAINBENCH_N=1 for dev loops)
  - Per-run seeded Fisher-Yates shuffle of page ingest order (LCG seed = run_idx+1)
  - Surfaces order-dependent adapter bugs (tie-break-by-first-seen etc.)
  - Reports mean ± sample-stddev per metric
  - "stddev = 0" is honest signal that the adapter is deterministic, not a bug.
    LLM-judge metrics (future) will naturally produce non-zero stddev.

Validation: all 80 Tier 5 + 5.5 queries pass validateAll(). 24 validator
unit tests pass.

Next commit: world.html contributor explorer (Phase 3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 00:10:15 +08:00

210 lines
7.0 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { validateQuery, validateQuerySet, TEMPORAL_VERBS } from './validator.ts';
import type { Query } from '../types.ts';
function mkValidQuery(overrides: Partial<Query> = {}): Query {
return {
id: 'q-0001',
tier: 'easy',
text: 'Who founded Acme?',
expected_output_type: 'cited-source-pages',
gold: { relevant: ['people/alice-chen'] },
...overrides,
};
}
describe('validateQuery — required fields', () => {
test('valid query passes', () => {
const r = validateQuery(mkValidQuery());
expect(r.ok).toBe(true);
expect(r.issues.length).toBe(0);
});
test('missing id fails', () => {
const r = validateQuery(mkValidQuery({ id: '' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'id')).toBe(true);
});
test('missing text fails', () => {
const r = validateQuery(mkValidQuery({ text: '' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'text')).toBe(true);
});
test('invalid tier fails', () => {
const r = validateQuery(mkValidQuery({ tier: 'invalid' as unknown as 'easy' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'tier')).toBe(true);
});
test('invalid expected_output_type fails', () => {
const r = validateQuery(mkValidQuery({
expected_output_type: 'nonsense' as unknown as 'answer-string',
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'expected_output_type')).toBe(true);
});
});
describe('validateQuery — temporal as_of_date rule', () => {
test('non-temporal query without as_of_date passes', () => {
const r = validateQuery(mkValidQuery({ text: 'Who founded Acme?' }));
expect(r.ok).toBe(true);
});
test('"is" verb without as_of_date fails', () => {
const r = validateQuery(mkValidQuery({ text: 'Where is Sarah working?' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'as_of_date')).toBe(true);
});
test('"was" verb without as_of_date fails', () => {
const r = validateQuery(mkValidQuery({ text: 'Who was at the meeting?' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'as_of_date')).toBe(true);
});
test('"as of" verb without as_of_date fails', () => {
const r = validateQuery(mkValidQuery({ text: 'As of 2024, who invested?' }));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'as_of_date')).toBe(true);
});
test('temporal query with "corpus-end" passes', () => {
const r = validateQuery(mkValidQuery({
text: 'Who is the CEO now?',
as_of_date: 'corpus-end',
}));
expect(r.ok).toBe(true);
});
test('temporal query with "per-source" passes', () => {
const r = validateQuery(mkValidQuery({
text: 'Who was at the meeting?',
as_of_date: 'per-source',
}));
expect(r.ok).toBe(true);
});
test('temporal query with ISO-8601 date passes', () => {
const r = validateQuery(mkValidQuery({
text: 'Who was at Acme in 2023?',
as_of_date: '2023-01-01',
}));
expect(r.ok).toBe(true);
});
test('temporal query with bogus as_of_date fails', () => {
const r = validateQuery(mkValidQuery({
text: 'Who is the CEO now?',
as_of_date: 'yesterday',
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'as_of_date')).toBe(true);
});
});
describe('validateQuery — gold shape by expected_output_type', () => {
test('cited-source-pages requires gold.relevant array', () => {
const r = validateQuery(mkValidQuery({
expected_output_type: 'cited-source-pages',
gold: {},
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'gold.relevant')).toBe(true);
});
test('cited-source-pages with malformed slug fails', () => {
const r = validateQuery(mkValidQuery({
gold: { relevant: ['not-a-slug'] },
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'gold.relevant')).toBe(true);
});
test('abstention requires expected_abstention=true', () => {
const r = validateQuery(mkValidQuery({
expected_output_type: 'abstention',
gold: {},
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'gold.expected_abstention')).toBe(true);
});
test('abstention with expected_abstention=true passes', () => {
const r = validateQuery(mkValidQuery({
expected_output_type: 'abstention',
gold: { expected_abstention: true },
}));
expect(r.ok).toBe(true);
});
});
describe('validateQuery — tier 5.5 author requirement', () => {
test('externally-authored without author fails', () => {
const r = validateQuery(mkValidQuery({
tier: 'externally-authored',
}));
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.field === 'author')).toBe(true);
});
test('externally-authored with author passes', () => {
const r = validateQuery(mkValidQuery({
tier: 'externally-authored',
author: 'synthetic-outsider-v1',
}));
expect(r.ok).toBe(true);
});
});
describe('validateQuerySet — batch level', () => {
test('duplicate ids fail', () => {
const r = validateQuerySet([
mkValidQuery({ id: 'q-0001' }),
mkValidQuery({ id: 'q-0001' }),
]);
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.reason === 'duplicate id in batch')).toBe(true);
});
test('unique ids pass', () => {
const r = validateQuerySet([
mkValidQuery({ id: 'q-0001' }),
mkValidQuery({ id: 'q-0002' }),
]);
expect(r.ok).toBe(true);
});
test('issues from multiple queries aggregated', () => {
const r = validateQuerySet([
mkValidQuery({ id: 'q-0001', text: '' }), // missing text
mkValidQuery({ id: 'q-0002', text: 'Who is CEO now?' }), // missing as_of_date
]);
expect(r.ok).toBe(false);
expect(r.issues.some(i => i.queryId === 'q-0001' && i.field === 'text')).toBe(true);
expect(r.issues.some(i => i.queryId === 'q-0002' && i.field === 'as_of_date')).toBe(true);
});
});
describe('TEMPORAL_VERBS regex', () => {
test('matches common temporal verbs', () => {
expect(TEMPORAL_VERBS.test('Where is Sarah?')).toBe(true);
expect(TEMPORAL_VERBS.test('Where was Sarah?')).toBe(true);
expect(TEMPORAL_VERBS.test('Who were the founders?')).toBe(true);
expect(TEMPORAL_VERBS.test('What was the current valuation?')).toBe(true);
expect(TEMPORAL_VERBS.test('Where is she now?')).toBe(true);
expect(TEMPORAL_VERBS.test('At the time, who led the round?')).toBe(true);
expect(TEMPORAL_VERBS.test('During Q1, which deals closed?')).toBe(true);
expect(TEMPORAL_VERBS.test('As of 2024, who works at Acme?')).toBe(true);
expect(TEMPORAL_VERBS.test('When did Alice join?')).toBe(true);
});
test('does not match non-temporal verbs', () => {
expect(TEMPORAL_VERBS.test('Who founded Acme?')).toBe(false);
expect(TEMPORAL_VERBS.test('Which investors backed Beta?')).toBe(false);
expect(TEMPORAL_VERBS.test('List the advisors at Gamma.')).toBe(false);
});
});