Compare commits

..
Author SHA1 Message Date
00ebc5bb61 fix(doctor): register onboard check names in doctor-categories to stop unknown-check warnings
doctor.ts pushes runAllOnboardChecks results into the checks list, but the
7 onboard check names (embed_staleness, entity_link_coverage,
timeline_coverage, takes_count, dangling_aliases, pack_upgrade_available,
type_proliferation) were never added to doctor-categories.ts, so every
doctor run emitted an 'unknown check name' stderr warn per onboard check.

Registers the 5 data-quality names under BRAIN and the 2 schema-pack names
under META (alphabetical order preserved), and widens the drift-guard test
to scan src/core/onboard/checks.ts alongside src/commands/doctor.ts so
future onboard checks can't drift uncategorized.

Takeover of #1839, rebased onto master (keeps master's timeline_dedup_index).

Co-authored-by: mvanhorn <mvanhorn@users.noreply.github.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-21 14:19:28 -07:00
4 changed files with 60 additions and 140 deletions
+1 -31
View File
@@ -56,36 +56,6 @@ export const litellmProxy: Recipe = {
cost_per_1m_output_usd: undefined,
price_last_verified: '2026-06-14',
},
// LiteLLM normalizes Cohere / Voyage / Jina / etc. rerank backends to the
// same wire shape gbrain's gateway.rerank() already speaks (the
// ZeroEntropy/llama.cpp contract):
// { model, query, documents, top_n } → { results: [{ index, relevance_score }] }
// So any rerank model the user registers in their LiteLLM config is
// reachable via `gbrain config set search.reranker.model litellm:<model>`
// with no request/response adapter — same as embeddings ride the proxy.
reranker: {
models: [], // user-provided; whatever rerank models the proxy serves
// No canonical default — the proxy defines its own model ids. The user
// sets search.reranker.model explicitly (mirrors the embedding
// touchpoint's user_provided_models contract).
default_model: '',
// The proxied backend bills (Cohere/Voyage/…); pricing-unknown is the
// honest state — same stance as this recipe's embedding/chat
// touchpoints and budget-tracker's deliberate litellm exclusion from
// the free-provider sets.
cost_per_1m_tokens_usd: undefined,
price_last_verified: '2026-06-27',
max_payload_bytes: 5_000_000,
// LEAF path only (matches llama-server-reranker's convention). LiteLLM
// serves both `/rerank` and `/v1/rerank`, and LITELLM_BASE_URL may be
// set with or without the `/v1` suffix (the setup_hint allows both), so
// the leaf form yields a valid route either way:
// http://localhost:4000 + /rerank → /rerank ✓
// http://localhost:4000/v1 + /rerank → /v1/rerank ✓
// Pinning '/v1/rerank' here would double to /v1/v1/rerank → 404 on
// /v1-suffixed bases.
path: '/rerank',
},
},
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL (include the /v1 suffix if your proxy serves the OpenAI route there, e.g. http://localhost:4000/v1) + pass --embedding-model litellm:<model> and --embedding-dimensions <N>. For rerank: register a rerank model in LiteLLM and set search.reranker.model litellm:<model-name>.',
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL (include the /v1 suffix if your proxy serves the OpenAI route there, e.g. http://localhost:4000/v1) + pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
};
+11 -3
View File
@@ -35,10 +35,11 @@
*
* The doctor renders both side by side.
*
* Drift contract: every check name that ships in doctor.ts MUST appear in
* Drift contract: every check name that ships through doctor MUST appear in
* exactly one set below. The drift-guard test in
* `test/doctor-categories.test.ts` enforces this by reading doctor.ts source
* via a tagged-string scan and asserting set membership exactly.
* `test/doctor-categories.test.ts` enforces this by reading doctor check
* emitter sources via a tagged-string scan and asserting set membership
* exactly.
*
* If you add a new doctor check, you MUST add its name to the appropriate
* set here. The categorize step in `src/commands/doctor.ts` falls through
@@ -67,12 +68,15 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
'conversation_parser_probe_health',
'cross_modal_modality_backfill',
'cycle_freshness',
'dangling_aliases',
'effective_date_health',
'embed_staleness',
'embedding_column_registry',
'embedding_env_override',
'embedding_provider',
'embedding_width_consistency',
'embeddings',
'entity_link_coverage',
'eval_drift',
'extract_atoms_backlog',
'extract_health',
@@ -102,7 +106,9 @@ export const BRAIN_CHECK_NAMES: ReadonlySet<string> = new Set([
'stub_guard_24h',
'sync_failures',
'sync_freshness',
'takes_count',
'takes_weight_grid',
'timeline_coverage',
'unified_multimodal_coverage',
'voice_gate_health',
]);
@@ -170,12 +176,14 @@ export const META_CHECK_NAMES: ReadonlySet<string> = new Set([
'eval_capture',
'minions_migration',
'multi_source_drift',
'pack_upgrade_available',
'schema_pack_active',
'schema_pack_consistency',
'schema_pack_source_drift',
'schema_version',
'slug_fallback_audit',
'timeline_dedup_index',
'type_proliferation',
'upgrade_errors',
]);
-88
View File
@@ -1,88 +0,0 @@
/**
* litellm-proxy reranker touchpoint smoke.
*
* Sibling of recipe-llama-server-reranker.test.ts. Pins the reranker
* touchpoint on the LiteLLM proxy recipe so:
* - the touchpoint exists with the LEAF '/rerank' path (LiteLLM serves both
* /rerank and /v1/rerank, so the leaf form is valid whether or not the
* user's LITELLM_BASE_URL carries the /v1 suffix the setup_hint allows)
* - a /v1-suffixed base URL does NOT produce /v1/v1/rerank (the original
* community PR pinned '/v1/rerank' which 404s on /v1-suffixed bases)
* - models: [] (user-provided; proxy defines the model ids)
* - pricing stays undefined (proxy can front a paid provider — same honest
* pricing-unknown stance as the embedding/chat touchpoints)
*
* The gateway.rerank() URL tests drive the real URL builder via the stubbed
* transport (same seam as test/ai/rerank.test.ts).
*/
import { describe, expect, test, afterEach } from 'bun:test';
import { getRecipe } from '../../src/core/ai/recipes/index.ts';
import {
configureGateway,
resetGateway,
rerank,
__setRerankTransportForTests,
} from '../../src/core/ai/gateway.ts';
afterEach(() => {
__setRerankTransportForTests(null);
resetGateway();
});
describe('recipe: litellm reranker touchpoint', () => {
test('declares reranker touchpoint with leaf /rerank path', () => {
const r = getRecipe('litellm')!;
const tp = r.touchpoints.reranker;
expect(tp).toBeDefined();
expect(tp!.path).toBe('/rerank');
expect(tp!.max_payload_bytes).toBe(5_000_000);
});
test('reranker touchpoint uses empty models[] for user-provided model ids', () => {
const r = getRecipe('litellm')!;
expect(r.touchpoints.reranker!.models).toEqual([]);
});
test('pricing stays undefined — proxy can front a paid provider', () => {
const r = getRecipe('litellm')!;
expect(r.touchpoints.reranker!.cost_per_1m_tokens_usd).toBeUndefined();
});
test('setup_hint keeps the /v1-suffix guidance AND mentions rerank', () => {
const r = getRecipe('litellm')!;
expect(r.setup_hint).toMatch(/\/v1 suffix/);
expect(r.setup_hint).toMatch(/search\.reranker\.model litellm:/);
});
});
describe('gateway.rerank() URL via litellm recipe', () => {
async function capturedRerankUrl(baseUrl?: string): Promise<string> {
configureGateway({
reranker_model: 'litellm:my-reranker',
env: {},
...(baseUrl ? { base_urls: { litellm: baseUrl } } : {}),
});
let capturedUrl = '';
__setRerankTransportForTests(async (url) => {
capturedUrl = url;
return new Response(
JSON.stringify({ results: [{ index: 0, relevance_score: 0.9 }] }),
{ status: 200, headers: { 'content-type': 'application/json' } },
);
});
await rerank({ query: 'q', documents: ['d'] });
return capturedUrl;
}
test('default base (no /v1 suffix) → /rerank', async () => {
const url = await capturedRerankUrl();
expect(url).toBe('http://localhost:4000/rerank');
});
test('/v1-suffixed base → /v1/rerank, NOT /v1/v1/rerank', async () => {
const url = await capturedRerankUrl('http://localhost:4000/v1');
expect(url).toBe('http://localhost:4000/v1/rerank');
expect(url).not.toContain('/v1/v1/');
});
});
+48 -18
View File
@@ -1,10 +1,10 @@
/**
* Drift guard for src/core/doctor-categories.ts.
*
* Reads src/commands/doctor.ts source via a literal-string scan, enumerates
* every `name: '<...>'` Check name, and asserts each appears in exactly ONE
* category set. The union of the four sets must equal the discovered names
* exactly — no orphans, no extras.
* Reads doctor check emitter source via a literal-string scan, enumerates every
* `name: '<...>'` Check name, and asserts each appears in exactly ONE category
* set. The union of the four sets must equal the discovered names exactly —
* no orphans, no extras.
*
* This is the structural failure the v0.41.19.0 plan-eng-review caught:
* doctor.ts grows new checks regularly; without this guard, the
@@ -25,26 +25,30 @@ import {
} from '../src/core/doctor-categories.ts';
const DOCTOR_TS_PATH = join(import.meta.dir, '..', 'src', 'commands', 'doctor.ts');
const ONBOARD_CHECKS_TS_PATH = join(import.meta.dir, '..', 'src', 'core', 'onboard', 'checks.ts');
const CHECK_SOURCE_PATHS = [DOCTOR_TS_PATH, ONBOARD_CHECKS_TS_PATH];
function enumerateCheckNames(): Set<string> {
const source = readFileSync(DOCTOR_TS_PATH, 'utf-8');
const names = new Set<string>();
// 1) Inline object-literal form: `{ name: 'foo', ... }`.
for (const m of source.matchAll(/name:\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
names.add(m[1]);
}
// 2) Helper-function form: `const name = 'foo';` inside a check helper.
// Catches checks like `nightly_quality_probe_health` and
// `conversation_facts_backlog` that build the Check from a captured
// name constant.
for (const m of source.matchAll(/const\s+name\s*=\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
names.add(m[1]);
for (const path of CHECK_SOURCE_PATHS) {
const source = readFileSync(path, 'utf-8');
// 1) Inline object-literal form: `{ name: 'foo', ... }`.
for (const m of source.matchAll(/name:\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
names.add(m[1]);
}
// 2) Helper-function form: `const name = 'foo';` inside a check helper.
// Catches checks like `nightly_quality_probe_health` and
// `conversation_facts_backlog` that build the Check from a captured
// name constant.
for (const m of source.matchAll(/const\s+name\s*=\s*['"]([a-z][a-z0-9_]+)['"]/g)) {
names.add(m[1]);
}
}
return names;
}
describe('doctor-categories drift guard', () => {
test('every check name in doctor.ts source belongs to exactly one category set', () => {
test('every doctor-emitted check name belongs to exactly one category set', () => {
const discovered = enumerateCheckNames();
const allCategorized = new Set<string>([
...BRAIN_CHECK_NAMES,
@@ -59,7 +63,7 @@ describe('doctor-categories drift guard', () => {
}
if (missing.length > 0) {
throw new Error(
`These check names appear in doctor.ts but are not categorized in ` +
`These check names appear in doctor check emitters but are not categorized in ` +
`src/core/doctor-categories.ts: ${missing.sort().join(', ')}. ` +
`Add each to BRAIN/SKILL/OPS/META_CHECK_NAMES.`,
);
@@ -86,7 +90,7 @@ describe('doctor-categories drift guard', () => {
expect(dupes).toEqual([]);
});
test('every categorized name is currently used in doctor.ts source (no stale entries)', () => {
test('every categorized name is currently used in doctor check emitters (no stale entries)', () => {
const discovered = enumerateCheckNames();
const allCategorized = new Set<string>([
...BRAIN_CHECK_NAMES,
@@ -124,6 +128,14 @@ describe('categorizeCheck', () => {
expect(categorizeCheck('sync_freshness')).toBe('brain');
});
test('returns the right category for onboard data-quality check names', () => {
expect(categorizeCheck('embed_staleness')).toBe('brain');
expect(categorizeCheck('entity_link_coverage')).toBe('brain');
expect(categorizeCheck('timeline_coverage')).toBe('brain');
expect(categorizeCheck('takes_count')).toBe('brain');
expect(categorizeCheck('dangling_aliases')).toBe('brain');
});
test('returns the right category for a known skill name', () => {
expect(categorizeCheck('resolver_health')).toBe('skill');
expect(categorizeCheck('skill_conformance')).toBe('skill');
@@ -140,6 +152,24 @@ describe('categorizeCheck', () => {
expect(categorizeCheck('upgrade_errors')).toBe('meta');
});
test('returns the right category for onboard schema-pack check names without warning', () => {
const originalWrite = process.stderr.write.bind(process.stderr);
const captured: string[] = [];
(process.stderr as { write: typeof process.stderr.write }).write = ((
chunk: string | Uint8Array,
) => {
captured.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString());
return true;
}) as typeof process.stderr.write;
try {
expect(categorizeCheck('pack_upgrade_available')).toBe('meta');
expect(categorizeCheck('type_proliferation')).toBe('meta');
expect(captured.filter((c) => c.includes('[doctor-categories]'))).toEqual([]);
} finally {
(process.stderr as { write: typeof process.stderr.write }).write = originalWrite;
}
});
test('unknown check name falls through to meta with a stderr warn (once per process)', () => {
const originalWrite = process.stderr.write.bind(process.stderr);
const captured: string[] = [];