Merge pull request #665 from proffesor-for-testing/feat/structural-retrieval-benchmark

feat(learning): add structural retrieval qualification
This commit is contained in:
Dragan Spiridonov
2026-09-06 13:48:39 +02:00
committed by GitHub
9 changed files with 618 additions and 0 deletions
@@ -0,0 +1,17 @@
# Structural Retrieval Qualification
AQE includes a deterministic qualification corpus for measuring whether a ranker retrieves reusable test patterns across surface changes. It is an evaluation boundary; it does not change the production ranker.
`createStructuralRetrievalCorpus()` returns 100 query groups spanning rename, framework transfer, reordered steps, distractor terminology, and cross-domain transfer. Each group identifies gold relevance, a surface-similar wrong remedy, an environment-sensitive counterexample, and ordering constraints. Source families belong to exactly one of the train, calibration, or audit splits. The revision and SHA-256 lineage hash bind every pattern and query.
Pass one result for every query to `evaluateStructuralRetrieval()`. The report includes candidate recall@k, strict Hit@1, MRR, nDCG, signed gold-to-best-wrong margin, deterministic 95% bootstrap intervals, per-transformation support, ordering violations, and these attribution states:
- `not_retrieved`: the relevant pattern did not enter the top-k candidate set.
- `retrieved_misranked`: it entered the set but a wrong pattern ranked first.
- `ranked_not_used`: it ranked first but downstream execution did not apply it.
- `used_no_benefit`: it was applied but the linked outcome did not improve.
- `success`: it ranked first, was applied, and improved the linked outcome.
Use `createRetrievalReceipt()` to bind a run to its revision, representation version, embedding-space identity, candidates and scores, ranker version, applied pattern IDs, and downstream outcome reference. The receipt intentionally has no reasoning or chain-of-thought field. Missing provenance, non-finite scores, incomplete benchmark results, and corpus-lineage mismatches fail closed.
The initial lexical-overlap negative control is asserted by `tests/unit/learning/structural-retrieval.test.ts`. Production HNSW/FTS candidate composition and a recorded production baseline remain follow-up work under issue #653.
+19
View File
@@ -79,6 +79,12 @@ export {
mapQEDomainToAQE,
QE_DOMAIN_LIST,
QE_DOMAINS,
createStructuralRetrievalCorpus,
verifyStructuralRetrievalCorpus,
evaluateStructuralRetrieval,
diagnoseRetrieval,
createRetrievalReceipt,
STRUCTURAL_RETRIEVAL_TRANSFORMATIONS,
} from './learning';
export type {
QEPattern,
@@ -92,6 +98,19 @@ export type {
PatternSearchResult,
QERoutingRequest,
QERoutingResult,
StructuralTransformation,
CorpusSplit,
StructuralPattern,
StructuralRetrievalQuery,
StructuralRetrievalCorpus,
RankedCandidate,
RetrievalDiagnostic,
QueryRetrievalResult,
MetricEstimate,
RetrievalMetricSet,
StructuralRetrievalReport,
RetrievalReceipt,
CreateRetrievalReceiptInput,
} from './learning';
// Feedback Module - Quality Feedback Loop (ADR-023)
+2
View File
@@ -101,6 +101,8 @@ export type {
LearningSegment,
} from './learning-evidence-admission.js';
export * from './structural-retrieval/index.js';
export type {
QEDomain,
QEPatternType,
+123
View File
@@ -0,0 +1,123 @@
import { createHash } from 'node:crypto';
import type {
CorpusSplit,
StructuralPattern,
StructuralRetrievalCorpus,
StructuralRetrievalQuery,
StructuralTransformation,
} from './types.js';
const REVISION = 'aqe-structural-transfer-2026-09-06';
const TRANSFORMATIONS: StructuralTransformation[] = [
'rename', 'framework-transfer', 'reordered-steps',
'distractor-terminology', 'cross-domain-transfer',
];
interface FamilySeed {
mechanism: string;
invariant: string;
action: string;
wrongAction: string;
oracle: string;
domain: string;
framework: string;
}
const SEEDS: FamilySeed[] = [
['async-race', 'observe completion before assertion', 'await the operation', 'increase the timeout', 'completion probe', 'unit', 'vitest'],
['stale-cache', 'invalidate on every state mutation', 'invalidate the cache', 'retry the read', 'freshness assertion', 'integration', 'redis'],
['transaction-leak', 'rollback isolates each test', 'rollback the transaction', 'truncate a shared table', 'row-count check', 'database', 'postgres'],
['clock-skew', 'compare times in one clock domain', 'inject a monotonic clock', 'widen the time window', 'clock boundary check', 'unit', 'node'],
['event-loss', 'subscribe before publishing', 'register the listener first', 'publish twice', 'delivery assertion', 'events', 'eventemitter'],
['resource-leak', 'release resources on every exit', 'close in a finally block', 'raise the pool limit', 'handle-count check', 'integration', 'playwright'],
['eventual-consistency', 'poll the observable state', 'poll with a bounded deadline', 'sleep for a fixed delay', 'state convergence', 'distributed', 'kubernetes'],
['identity-confusion', 'compare canonical identities', 'normalize the identifier', 'use display names', 'identity equality', 'security', 'oauth'],
['partial-write', 'publish only complete state', 'commit through an atomic swap', 'ignore missing fields', 'atomicity check', 'filesystem', 'node'],
['order-dependence', 'tests start from isolated state', 'reset state in setup', 'force alphabetical order', 'shuffle repeat', 'unit', 'jest'],
['schema-drift', 'validate at the boundary', 'reject the incompatible schema', 'cast the payload', 'schema validator', 'api', 'zod'],
['backpressure-loss', 'producer respects consumer capacity', 'await the drain signal', 'increase buffer memory', 'loss counter', 'performance', 'streams'],
['retry-duplication', 'retries preserve idempotency', 'attach an idempotency key', 'disable all retries', 'duplicate detector', 'api', 'fetch'],
['permission-bypass', 'authorization precedes mutation', 'check permission before write', 'hide the user interface', 'denial assertion', 'security', 'express'],
['encoding-mismatch', 'encode and decode with one charset', 'set UTF-8 explicitly', 'strip non-ASCII text', 'round-trip equality', 'api', 'json'],
['pagination-gap', 'cursor advances from the last item', 'use the returned cursor', 'increase page size', 'set equality', 'api', 'graphql'],
['cancellation-leak', 'cancellation propagates to children', 'forward the abort signal', 'wait for natural completion', 'abort observation', 'integration', 'abortcontroller'],
['floating-point-boundary', 'compare numeric results by tolerance', 'use an error bound', 'round every input', 'relative-error check', 'unit', 'vitest'],
['locale-dependence', 'format under an explicit locale', 'pass the locale explicitly', 'update the snapshot locally', 'multi-locale check', 'ui', 'intl'],
['lock-starvation', 'lock acquisition remains fair', 'queue lock waiters', 'raise retry count', 'bounded-wait check', 'concurrency', 'mutex'],
].map(([mechanism, invariant, action, wrongAction, oracle, domain, framework]) => ({
mechanism, invariant, action, wrongAction, oracle, domain, framework,
}));
function splitFor(index: number): CorpusSplit {
if (index < 12) return 'train';
if (index < 16) return 'calibration';
return 'audit';
}
function stableHash(value: unknown): string {
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
}
function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.freeze(value);
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
}
return value;
}
function queryText(seed: FamilySeed, transformation: StructuralTransformation, index: number): string {
const object = `component-${index + 41}`;
switch (transformation) {
case 'rename': return `${object} intermittently fails; ensure ${seed.invariant} by applying: ${seed.action}`;
case 'framework-transfer': return `In an alternative runtime, ${seed.mechanism} appears. Preserve this rule: ${seed.invariant}`;
case 'reordered-steps': return `Verify with ${seed.oracle}; first arrange the fixture, then ${seed.action}; fault: ${seed.mechanism}`;
case 'distractor-terminology': return `${seed.wrongAction} is tempting for ${seed.mechanism}, but the required invariant is ${seed.invariant}`;
case 'cross-domain-transfer': return `A ${seed.domain} workflow moved to service-${index}; reuse the mechanism ${seed.mechanism} and ${seed.action}`;
}
}
export function createStructuralRetrievalCorpus(): StructuralRetrievalCorpus {
const patterns: StructuralPattern[] = [];
const queries: StructuralRetrievalQuery[] = [];
SEEDS.forEach((seed, index) => {
const familyId = `family-${String(index + 1).padStart(2, '0')}`;
const correctId = `${familyId}-structural`;
const distractorId = `${familyId}-surface-distractor`;
const counterexampleId = `${familyId}-counterexample`;
patterns.push(
{ id: correctId, familyId, framework: seed.framework, domain: seed.domain,
failureMechanism: seed.mechanism, invariant: seed.invariant, actions: [seed.action], oracle: seed.oracle,
text: `${seed.framework} ${seed.domain} ${seed.mechanism}: ${seed.action}; verify ${seed.oracle}` },
{ id: distractorId, familyId, framework: seed.framework, domain: seed.domain,
failureMechanism: `symptom resembling ${seed.mechanism}`, invariant: 'mask the observed symptom',
actions: [seed.wrongAction], oracle: 'single rerun',
text: `${seed.framework} ${seed.domain} ${seed.mechanism}: ${seed.wrongAction}; quick ${seed.oracle}` },
{ id: counterexampleId, familyId, framework: seed.framework, domain: seed.domain,
failureMechanism: seed.mechanism, invariant: `environment forbids ${seed.action}`,
actions: [`escalate instead of ${seed.action}`], oracle: 'environment constraint check',
text: `${seed.mechanism} ${seed.action} ${seed.framework}, but environment forbids the action` },
);
TRANSFORMATIONS.forEach((transformation) => queries.push({
id: `${familyId}-${transformation}`,
familyId,
split: splitFor(index),
transformation,
text: queryText(seed, transformation, index),
relevantPatternIds: [correctId],
mustNotRankAbove: [
{ lowerPatternId: distractorId, higherPatternId: correctId },
{ lowerPatternId: counterexampleId, higherPatternId: correctId },
],
}));
});
const payload = { schemaVersion: 'aqe-structural-retrieval/v1' as const, corpusRevision: REVISION, patterns, queries };
return deepFreeze({ ...payload, lineageHash: stableHash(payload) });
}
export const STRUCTURAL_RETRIEVAL_TRANSFORMATIONS = Object.freeze([...TRANSFORMATIONS]);
export function verifyStructuralRetrievalCorpus(corpus: StructuralRetrievalCorpus): boolean {
const { lineageHash: _lineageHash, ...payload } = corpus;
return stableHash(payload) === corpus.lineageHash;
}
@@ -0,0 +1,131 @@
import type {
MetricEstimate, QueryRetrievalResult, RetrievalDiagnostic, RetrievalMetricSet,
StructuralRetrievalCorpus, StructuralRetrievalQuery, StructuralRetrievalReport,
} from './types.js';
import { verifyStructuralRetrievalCorpus } from './corpus.js';
interface Scores { recall: number; hit: number; reciprocalRank: number; ndcg: number; margin: number }
function queryScores(query: StructuralRetrievalQuery, result: QueryRetrievalResult, k: number): Scores {
const relevant = new Set(query.relevantPatternIds);
const rank = result.candidates.findIndex(candidate => relevant.has(candidate.patternId));
const topKHits = result.candidates.slice(0, k).filter(candidate => relevant.has(candidate.patternId)).length;
const idealCount = Math.min(k, relevant.size);
const dcg = result.candidates.slice(0, k).reduce(
(sum, candidate, index) => sum + (relevant.has(candidate.patternId) ? 1 / Math.log2(index + 2) : 0), 0,
);
const idcg = Array.from({ length: idealCount }, (_, index) => 1 / Math.log2(index + 2)).reduce((a, b) => a + b, 0);
const relevantScore = rank >= 0 ? result.candidates[rank]!.score : 0;
const bestWrong = Math.max(0, ...result.candidates.filter(c => !relevant.has(c.patternId)).map(c => c.score));
return {
recall: relevant.size === 0 ? 0 : topKHits / relevant.size,
hit: rank === 0 ? 1 : 0,
reciprocalRank: rank < 0 ? 0 : 1 / (rank + 1),
ndcg: idcg === 0 ? 0 : dcg / idcg,
margin: relevantScore - bestWrong,
};
}
function mean(values: number[]): number {
return values.length === 0 ? 0 : values.reduce((sum, value) => sum + value, 0) / values.length;
}
function estimate(values: number[], seed: number): MetricEstimate {
if (values.length === 0) return { value: 0, confidenceInterval: { lower: 0, upper: 0, level: 0.95 } };
let state = seed >>> 0;
const random = () => ((state = (1664525 * state + 1013904223) >>> 0) / 0x100000000);
const bootstraps = Array.from({ length: 1000 }, () => mean(Array.from(
{ length: values.length }, () => values[Math.floor(random() * values.length)]!,
))).sort((a, b) => a - b);
return {
value: mean(values),
confidenceInterval: {
lower: bootstraps[Math.floor(bootstraps.length * 0.025)]!,
upper: bootstraps[Math.floor(bootstraps.length * 0.975)]!,
level: 0.95,
},
};
}
function metricSet(scores: Scores[]): RetrievalMetricSet {
return {
support: scores.length,
recallAtK: estimate(scores.map(s => s.recall), 11),
hitAt1: estimate(scores.map(s => s.hit), 13),
mrr: estimate(scores.map(s => s.reciprocalRank), 17),
ndcg: estimate(scores.map(s => s.ndcg), 19),
top1Margin: estimate(scores.map(s => s.margin), 23),
};
}
export function diagnoseRetrieval(query: StructuralRetrievalQuery, result: QueryRetrievalResult, k: number): RetrievalDiagnostic {
const relevant = new Set(query.relevantPatternIds);
const relevantCandidates = result.candidates.slice(0, k).filter(c => relevant.has(c.patternId));
if (relevantCandidates.length === 0) return 'not_retrieved';
if (!relevant.has(result.candidates[0]?.patternId ?? '')) return 'retrieved_misranked';
const applied = result.appliedPatternIds ?? [];
if (applied.length === 0 || !applied.some(id => relevant.has(id))) return 'ranked_not_used';
if (result.outcomeImproved !== true) return 'used_no_benefit';
return 'success';
}
export function evaluateStructuralRetrieval(
corpus: StructuralRetrievalCorpus,
results: QueryRetrievalResult[],
options: { rankerVersion: string; k?: number },
): StructuralRetrievalReport {
const k = options.k ?? 10;
if (!Number.isInteger(k) || k < 1) throw new Error('k must be a positive integer');
if (!verifyStructuralRetrievalCorpus(corpus)) throw new Error('corpus lineage hash mismatch');
if (results.length !== corpus.queries.length) {
throw new Error('results must contain exactly one entry for every corpus query');
}
const byQuery = new Map(results.map(result => [result.queryId, result]));
if (byQuery.size !== corpus.queries.length || corpus.queries.some(query => !byQuery.has(query.id))) {
throw new Error('results must contain exactly one entry for every corpus query');
}
const knownPatterns = new Set(corpus.patterns.map(pattern => pattern.id));
for (const result of results) {
const candidateIds = new Set<string>();
for (const candidate of result.candidates) {
if (!knownPatterns.has(candidate.patternId)) throw new Error(`unknown candidate pattern: ${candidate.patternId}`);
if (!Number.isFinite(candidate.score)) throw new Error(`candidate score must be finite: ${candidate.patternId}`);
if (candidateIds.has(candidate.patternId)) throw new Error(`duplicate candidate pattern: ${candidate.patternId}`);
candidateIds.add(candidate.patternId);
}
for (const appliedId of result.appliedPatternIds ?? []) {
if (!candidateIds.has(appliedId)) throw new Error(`applied pattern was not a candidate: ${appliedId}`);
}
}
const rows = corpus.queries.map(query => {
const result = byQuery.get(query.id)!;
return { query, result, scores: queryScores(query, result, k) };
});
const diagnosticNames: RetrievalDiagnostic[] = [
'success', 'not_retrieved', 'retrieved_misranked', 'ranked_not_used', 'used_no_benefit',
];
const diagnostics = Object.fromEntries(diagnosticNames.map(name => [name, 0])) as Record<RetrievalDiagnostic, number>;
for (const row of rows) diagnostics[diagnoseRetrieval(row.query, row.result, k)] += 1;
const relationViolations = rows.reduce((count, { query, result }) => {
const ranks = new Map(result.candidates.map((candidate, index) => [candidate.patternId, index]));
return count + query.mustNotRankAbove.filter(relation => {
const low = ranks.get(relation.lowerPatternId);
const high = ranks.get(relation.higherPatternId);
return low !== undefined && (high === undefined || low < high);
}).length;
}, 0);
const slices: StructuralRetrievalReport['slices'] = {};
for (const transformation of new Set(corpus.queries.map(query => query.transformation))) {
slices[transformation] = metricSet(rows.filter(row => row.query.transformation === transformation).map(row => row.scores));
}
return {
corpusRevision: corpus.corpusRevision,
corpusLineageHash: corpus.lineageHash,
rankerVersion: options.rankerVersion,
k,
overall: metricSet(rows.map(row => row.scores)),
slices,
diagnostics,
relationViolations,
};
}
@@ -0,0 +1,9 @@
export {
createStructuralRetrievalCorpus,
verifyStructuralRetrievalCorpus,
STRUCTURAL_RETRIEVAL_TRANSFORMATIONS,
} from './corpus.js';
export { diagnoseRetrieval, evaluateStructuralRetrieval } from './evaluate.js';
export { createRetrievalReceipt } from './receipt.js';
export type * from './types.js';
export type { CreateRetrievalReceiptInput } from './receipt.js';
@@ -0,0 +1,49 @@
import { createHash } from 'node:crypto';
import type { RankedCandidate, RetrievalReceipt } from './types.js';
export interface CreateRetrievalReceiptInput {
revision: string;
queryId: string;
queryRepresentationVersion: string;
embeddingSpaceId: string;
rankerVersion: string;
candidates: RankedCandidate[];
appliedPatternIds?: string[];
outcomeRef?: string | null;
outcomeImproved?: boolean | null;
}
function deepFreeze<T>(value: T): T {
if (value && typeof value === 'object' && !Object.isFrozen(value)) {
Object.freeze(value);
for (const child of Object.values(value as Record<string, unknown>)) deepFreeze(child);
}
return value;
}
export function createRetrievalReceipt(input: CreateRetrievalReceiptInput): RetrievalReceipt {
if (!input.revision || !input.queryId || !input.queryRepresentationVersion || !input.embeddingSpaceId || !input.rankerVersion) {
throw new Error('retrieval receipt provenance fields must be non-empty');
}
const candidateIds = new Set(input.candidates.map(candidate => candidate.patternId));
if (candidateIds.size !== input.candidates.length || input.candidates.some(candidate => !candidate.patternId || !Number.isFinite(candidate.score))) {
throw new Error('retrieval receipt candidates must have an id and finite score');
}
if ((input.appliedPatternIds ?? []).some(id => !candidateIds.has(id))) {
throw new Error('retrieval receipt applied patterns must be present in candidates');
}
const payload = {
schemaVersion: 'aqe-retrieval-receipt/v1' as const,
revision: input.revision,
queryId: input.queryId,
queryRepresentationVersion: input.queryRepresentationVersion,
embeddingSpaceId: input.embeddingSpaceId,
rankerVersion: input.rankerVersion,
candidates: input.candidates.map(candidate => ({ ...candidate })),
appliedPatternIds: [...(input.appliedPatternIds ?? [])],
outcomeRef: input.outcomeRef ?? null,
outcomeImproved: input.outcomeImproved ?? null,
};
const receiptId = createHash('sha256').update(JSON.stringify(payload)).digest('hex');
return deepFreeze({ ...payload, receiptId });
}
@@ -0,0 +1,96 @@
export type StructuralTransformation =
| 'rename'
| 'framework-transfer'
| 'reordered-steps'
| 'distractor-terminology'
| 'cross-domain-transfer';
export type CorpusSplit = 'train' | 'calibration' | 'audit';
export interface StructuralPattern {
id: string;
familyId: string;
framework: string;
domain: string;
failureMechanism: string;
invariant: string;
actions: string[];
oracle: string;
text: string;
}
export interface StructuralRetrievalQuery {
id: string;
familyId: string;
split: CorpusSplit;
transformation: StructuralTransformation;
text: string;
relevantPatternIds: string[];
mustNotRankAbove: Array<{ lowerPatternId: string; higherPatternId: string }>;
}
export interface StructuralRetrievalCorpus {
schemaVersion: 'aqe-structural-retrieval/v1';
corpusRevision: string;
lineageHash: string;
patterns: StructuralPattern[];
queries: StructuralRetrievalQuery[];
}
export interface RankedCandidate {
patternId: string;
score: number;
}
export type RetrievalDiagnostic =
| 'success'
| 'not_retrieved'
| 'retrieved_misranked'
| 'ranked_not_used'
| 'used_no_benefit';
export interface QueryRetrievalResult {
queryId: string;
candidates: RankedCandidate[];
appliedPatternIds?: string[];
outcomeImproved?: boolean;
}
export interface MetricEstimate {
value: number;
confidenceInterval: { lower: number; upper: number; level: 0.95 };
}
export interface RetrievalMetricSet {
support: number;
recallAtK: MetricEstimate;
hitAt1: MetricEstimate;
mrr: MetricEstimate;
ndcg: MetricEstimate;
top1Margin: MetricEstimate;
}
export interface StructuralRetrievalReport {
corpusRevision: string;
corpusLineageHash: string;
rankerVersion: string;
k: number;
overall: RetrievalMetricSet;
slices: Partial<Record<StructuralTransformation, RetrievalMetricSet>>;
diagnostics: Record<RetrievalDiagnostic, number>;
relationViolations: number;
}
export interface RetrievalReceipt {
schemaVersion: 'aqe-retrieval-receipt/v1';
receiptId: string;
revision: string;
queryId: string;
queryRepresentationVersion: string;
embeddingSpaceId: string;
rankerVersion: string;
candidates: RankedCandidate[];
appliedPatternIds: string[];
outcomeRef: string | null;
outcomeImproved: boolean | null;
}
@@ -0,0 +1,172 @@
import { describe, expect, it } from 'vitest';
import {
createRetrievalReceipt,
createStructuralRetrievalCorpus,
diagnoseRetrieval,
evaluateStructuralRetrieval,
} from '../../../src/learning/structural-retrieval/index.js';
import type {
QueryRetrievalResult,
StructuralPattern,
StructuralRetrievalQuery,
} from '../../../src/learning/structural-retrieval/index.js';
function tokens(text: string): Set<string> {
return new Set(text.toLowerCase().split(/[^a-z0-9]+/).filter(token => token.length > 2));
}
function lexicalScore(query: string, pattern: StructuralPattern): number {
const queryTokens = tokens(query);
const patternTokens = tokens(pattern.text);
return [...queryTokens].filter(token => patternTokens.has(token)).length / Math.max(1, queryTokens.size);
}
function lexicalResults(): QueryRetrievalResult[] {
const corpus = createStructuralRetrievalCorpus();
return corpus.queries.map(query => ({
queryId: query.id,
candidates: corpus.patterns
.map(pattern => ({ patternId: pattern.id, score: lexicalScore(query.text, pattern) }))
.sort((a, b) => b.score - a.score || a.patternId.localeCompare(b.patternId))
.slice(0, 10),
}));
}
describe('structural retrieval qualification corpus', () => {
it('freezes a lineage-tracked corpus with 100 queries and five transformations', () => {
const corpus = createStructuralRetrievalCorpus();
expect(corpus.schemaVersion).toBe('aqe-structural-retrieval/v1');
expect(corpus.lineageHash).toMatch(/^[a-f0-9]{64}$/);
expect(createStructuralRetrievalCorpus().lineageHash).toBe(corpus.lineageHash);
expect(corpus.queries).toHaveLength(100);
expect(new Set(corpus.queries.map(query => query.transformation))).toHaveLength(5);
expect(Object.isFrozen(corpus)).toBe(true);
expect(Object.isFrozen(corpus.queries)).toBe(true);
expect(Object.isFrozen(corpus.queries[0]!.mustNotRankAbove)).toBe(true);
});
it('keeps source families disjoint across train, calibration, and audit splits', () => {
const corpus = createStructuralRetrievalCorpus();
const families = new Map<string, Set<string>>();
for (const query of corpus.queries) {
const splits = families.get(query.familyId) ?? new Set<string>();
splits.add(query.split);
families.set(query.familyId, splits);
}
expect([...families.values()].every(splits => splits.size === 1)).toBe(true);
expect(new Set(corpus.queries.map(query => query.split))).toEqual(new Set(['train', 'calibration', 'audit']));
});
it('includes hard distractors, counterexamples, gold relevance, and ordering constraints', () => {
const corpus = createStructuralRetrievalCorpus();
expect(corpus.queries.every(query => query.relevantPatternIds.length > 0)).toBe(true);
expect(corpus.queries.every(query => query.mustNotRankAbove.length === 2)).toBe(true);
expect(corpus.patterns.filter(pattern => pattern.id.endsWith('surface-distractor'))).toHaveLength(20);
expect(corpus.patterns.filter(pattern => pattern.id.endsWith('counterexample'))).toHaveLength(20);
});
});
describe('structural retrieval metrics and diagnostics', () => {
it('reports perfect ranking metrics, slices, attribution, and relation compliance', () => {
const corpus = createStructuralRetrievalCorpus();
const results = corpus.queries.map(query => ({
queryId: query.id,
candidates: [
{ patternId: query.relevantPatternIds[0]!, score: 0.9 },
{ patternId: query.mustNotRankAbove[0]!.lowerPatternId, score: 0.3 },
],
appliedPatternIds: [query.relevantPatternIds[0]!],
outcomeImproved: true,
}));
const report = evaluateStructuralRetrieval(corpus, results, { rankerVersion: 'oracle/v1', k: 2 });
expect(report.overall).toMatchObject({ support: 100 });
expect(report.overall.recallAtK.value).toBe(1);
expect(report.overall.hitAt1.value).toBe(1);
expect(report.overall.mrr.value).toBe(1);
expect(report.overall.ndcg.value).toBe(1);
expect(report.overall.top1Margin.value).toBeCloseTo(0.6);
expect(report.diagnostics.success).toBe(100);
expect(report.relationViolations).toBe(0);
expect(Object.values(report.slices).every(slice => slice?.support === 20)).toBe(true);
});
it('acts as a negative control by exposing lexical-overlap ranking failure', () => {
const corpus = createStructuralRetrievalCorpus();
const report = evaluateStructuralRetrieval(corpus, lexicalResults(), { rankerVersion: 'lexical-overlap/v1' });
expect(report.overall.hitAt1.value).toBeLessThan(0.8);
expect(report.relationViolations).toBeGreaterThan(0);
expect(report.diagnostics.success).toBe(0);
});
it('distinguishes each retrieval-to-outcome failure stage', () => {
const corpus = createStructuralRetrievalCorpus();
const query = corpus.queries[0]!;
const relevant = query.relevantPatternIds[0]!;
const wrong = query.mustNotRankAbove[0]!.lowerPatternId;
const result = (candidates: QueryRetrievalResult['candidates'], appliedPatternIds?: string[], outcomeImproved?: boolean) =>
({ queryId: query.id, candidates, appliedPatternIds, outcomeImproved });
expect(diagnoseRetrieval(query, result([{ patternId: wrong, score: 1 }]), 2)).toBe('not_retrieved');
expect(diagnoseRetrieval(query, result([{ patternId: wrong, score: 1 }, { patternId: relevant, score: 0.5 }]), 2))
.toBe('retrieved_misranked');
expect(diagnoseRetrieval(query, result([{ patternId: relevant, score: 1 }]), 2)).toBe('ranked_not_used');
expect(diagnoseRetrieval(query, result([{ patternId: relevant, score: 1 }], [relevant], false), 2))
.toBe('used_no_benefit');
expect(diagnoseRetrieval(query, result([{ patternId: relevant, score: 1 }], [relevant], true), 2)).toBe('success');
});
it('rejects incomplete benchmark runs', () => {
const corpus = createStructuralRetrievalCorpus();
expect(() => evaluateStructuralRetrieval(corpus, [], { rankerVersion: 'broken/v1' }))
.toThrow('exactly one entry for every corpus query');
});
it('rejects attribution to a pattern absent from the candidate evidence', () => {
const corpus = createStructuralRetrievalCorpus();
const results = corpus.queries.map(query => ({
queryId: query.id,
candidates: [{ patternId: query.relevantPatternIds[0]!, score: 1 }],
appliedPatternIds: [] as string[],
}));
results[0]!.appliedPatternIds = ['family-20-counterexample'];
expect(() => evaluateStructuralRetrieval(corpus, results, { rankerVersion: 'ranker/v1' }))
.toThrow('applied pattern was not a candidate');
});
it('rejects tampered corpus lineage and malformed candidate evidence', () => {
const corpus = createStructuralRetrievalCorpus();
const forged = { ...corpus, corpusRevision: 'forged' };
expect(() => evaluateStructuralRetrieval(forged, [], { rankerVersion: 'ranker/v1' }))
.toThrow('corpus lineage hash mismatch');
const results = corpus.queries.map(query => ({
queryId: query.id,
candidates: [{ patternId: query.relevantPatternIds[0]!, score: 1 }],
}));
results[0]!.candidates.push({ patternId: results[0]!.candidates[0]!.patternId, score: Number.NaN });
expect(() => evaluateStructuralRetrieval(corpus, results, { rankerVersion: 'ranker/v1' }))
.toThrow(/finite|duplicate/);
});
});
describe('retrieval receipts', () => {
it('binds representation, embedding, candidates, ranker, use, and outcome without reasoning text', () => {
const input = {
revision: 'abc123', queryId: 'query-1', queryRepresentationVersion: 'typed/v1',
embeddingSpaceId: 'minilm:384:v1', rankerVersion: 'baseline/v1',
candidates: [{ patternId: 'pattern-1', score: 0.75 }], appliedPatternIds: ['pattern-1'],
outcomeRef: 'run-9', outcomeImproved: true,
};
const receipt = createRetrievalReceipt(input);
expect(receipt.receiptId).toBe(createRetrievalReceipt(input).receiptId);
expect(receipt).toMatchObject(input);
expect(JSON.stringify(receipt)).not.toMatch(/chain.of.thought|reasoning/i);
expect(Object.isFrozen(receipt.candidates)).toBe(true);
});
it('fails closed when provenance is absent', () => {
expect(() => createRetrievalReceipt({
revision: '', queryId: 'query-1', queryRepresentationVersion: 'typed/v1', embeddingSpaceId: '',
rankerVersion: 'ranker/v1', candidates: [],
})).toThrow('provenance fields must be non-empty');
});
});