diff --git a/src/core/doctor-categories.ts b/src/core/doctor-categories.ts index e445bfeea..a59a7ea9d 100644 --- a/src/core/doctor-categories.ts +++ b/src/core/doctor-categories.ts @@ -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 = 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 = 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 = 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', ]); diff --git a/test/doctor-categories.test.ts b/test/doctor-categories.test.ts index b6bbf3b8f..d5d248620 100644 --- a/test/doctor-categories.test.ts +++ b/test/doctor-categories.test.ts @@ -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 { - const source = readFileSync(DOCTOR_TS_PATH, 'utf-8'); const names = new Set(); - // 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([ ...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([ ...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[] = [];