mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(doctor): doctor-categories foundation — BRAIN/SKILL/OPS/META sets + drift guard
Categorizes every doctor check name into exactly one of four categories. Exported
constants + categorizeCheck(name) helper are the single source of truth for the
v0.41.20.0 brain_checks_score + category_scores + --scope=brain wave. Drift guard
test parses doctor.ts source for both inline {name: 'foo'} and helper
const name = 'foo' patterns; CI fires if any check name lacks a category.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(doctor): brain_checks_score + category_scores + --scope=brain skip-computation
Extends Check with optional category and DoctorReport with brain_checks_score +
category_scores (additive — schema_version stays at 2; back-compat health_score
math byte-identical). buildChecks gains --scope=brain with explicit early-skip
gates around the SKILL check group (resolver_health + skill_conformance +
skill_brain_first + whoknows_health). Sub-second doctor on a brain with thousands
of skills. computeDoctorReport tags every check via categorizeCheck() at compute
time. Human output leads with the brain figure and renders the weighted
BrainHealth.brain_score alongside.
Test seam fix in test/doctor-home-dir-in-worktree.test.ts: the pre-existing
fragile JSON parser walked back from "checks" to find the envelope's outer
brace; v0.41.20.0's new nested category_scores object broke that heuristic.
Anchored on the canonical {"schema_version" envelope prefix instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(status): gbrain status — single-screen brain health dashboard + get_status_snapshot MCP op
NEW gbrain status command (src/commands/status.ts) composes 6 sections:
sync (per-source last_sync_at + staleness via buildSyncStatusReport),
cycle (TWO rows: last autopilot-cycle + last autopilot-* of any kind —
reflects v0.36.4.0 health-aware autopilot's targeted handler routing;
totals read from result.report.totals per the canonical handler shape),
locks (gbrain_cycle_locks active rows), workers (readSupervisorEvents +
summarizeCrashes), queue (LIVE counts NO time-window — old stuck jobs
are exactly what status surfaces), autopilot (PID liveness via kill -0).
Stable --json envelope (schema_version: 1). Exit codes 0=ok / 1=snapshot
failed / 2=usage. --section filter.
Thin-client mode routes Sync + Cycle through NEW get_status_snapshot MCP
op (admin scope, NOT localOnly; payload deliberately omits Locks /
Workers / Queue / Autopilot so feature creep can't quietly widen the
admin-scoped data exposure). Local-only sections render "local-only —
N/A on remote brain" honestly instead of pretending the local install's
empty state is the remote brain's.
CLI dispatch: pre-engine-bind branch for thin-client (no PGLite needed)
+ engine-connected dispatch case for local mode. CLI-only architecture
per codex MAJOR-4 (status owns its own thin-client branch inside
runStatus, not routed through op dispatch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: bump version to v0.41.20.0 + CHANGELOG + TODOS + llms regen
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(doctor-categories): categorize batch_retry_health from v0.41.19.0 Supavisor wave
The drift guard correctly caught a new check introduced by master's v0.41.19.0
Supavisor Retry Cathedral (PR #1537). batch_retry_health surfaces batch-write
retry events from the new src/core/audit/batch-retry-audit.ts module — OPS
category (infrastructure liveness).
This is exactly why the drift guard exists: any future check added to doctor.ts
without a category entry fails CI immediately instead of silently degrading
to 'meta'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
162 lines
5.8 KiB
TypeScript
162 lines
5.8 KiB
TypeScript
/**
|
|
* 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.
|
|
*
|
|
* This is the structural failure the v0.41.19.0 plan-eng-review caught:
|
|
* doctor.ts grows new checks regularly; without this guard, the
|
|
* categorization map silently goes stale and unknown checks degrade to
|
|
* 'meta' without anyone noticing.
|
|
*/
|
|
|
|
import { describe, test, expect, beforeEach } from 'bun:test';
|
|
import { readFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import {
|
|
BRAIN_CHECK_NAMES,
|
|
SKILL_CHECK_NAMES,
|
|
OPS_CHECK_NAMES,
|
|
META_CHECK_NAMES,
|
|
categorizeCheck,
|
|
_resetUnknownCheckWarningsForTest,
|
|
} from '../src/core/doctor-categories.ts';
|
|
|
|
const DOCTOR_TS_PATH = join(import.meta.dir, '..', 'src', 'commands', 'doctor.ts');
|
|
|
|
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]);
|
|
}
|
|
return names;
|
|
}
|
|
|
|
describe('doctor-categories drift guard', () => {
|
|
test('every check name in doctor.ts source belongs to exactly one category set', () => {
|
|
const discovered = enumerateCheckNames();
|
|
const allCategorized = new Set<string>([
|
|
...BRAIN_CHECK_NAMES,
|
|
...SKILL_CHECK_NAMES,
|
|
...OPS_CHECK_NAMES,
|
|
...META_CHECK_NAMES,
|
|
]);
|
|
|
|
const missing: string[] = [];
|
|
for (const name of discovered) {
|
|
if (!allCategorized.has(name)) missing.push(name);
|
|
}
|
|
if (missing.length > 0) {
|
|
throw new Error(
|
|
`These check names appear in doctor.ts but are not categorized in ` +
|
|
`src/core/doctor-categories.ts: ${missing.sort().join(', ')}. ` +
|
|
`Add each to BRAIN/SKILL/OPS/META_CHECK_NAMES.`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('no check name appears in more than one category set', () => {
|
|
const counts = new Map<string, string[]>();
|
|
const tag = (s: ReadonlySet<string>, label: string) => {
|
|
for (const n of s) {
|
|
if (!counts.has(n)) counts.set(n, []);
|
|
counts.get(n)!.push(label);
|
|
}
|
|
};
|
|
tag(BRAIN_CHECK_NAMES, 'brain');
|
|
tag(SKILL_CHECK_NAMES, 'skill');
|
|
tag(OPS_CHECK_NAMES, 'ops');
|
|
tag(META_CHECK_NAMES, 'meta');
|
|
|
|
const dupes: string[] = [];
|
|
for (const [name, cats] of counts) {
|
|
if (cats.length > 1) dupes.push(`${name} in [${cats.join(', ')}]`);
|
|
}
|
|
expect(dupes).toEqual([]);
|
|
});
|
|
|
|
test('every categorized name is currently used in doctor.ts source (no stale entries)', () => {
|
|
const discovered = enumerateCheckNames();
|
|
const allCategorized = new Set<string>([
|
|
...BRAIN_CHECK_NAMES,
|
|
...SKILL_CHECK_NAMES,
|
|
...OPS_CHECK_NAMES,
|
|
...META_CHECK_NAMES,
|
|
]);
|
|
|
|
const stale: string[] = [];
|
|
for (const name of allCategorized) {
|
|
if (!discovered.has(name)) stale.push(name);
|
|
}
|
|
// Stale entries are warnings, not hard errors — a check may be temporarily
|
|
// removed during refactor. But the build should still flag them so we
|
|
// catch the drift quickly. Use a soft assertion via console hint and a
|
|
// strict expectation that the count is small (<=2). Adjust if real
|
|
// refactors require more headroom.
|
|
if (stale.length > 2) {
|
|
throw new Error(
|
|
`These categorized names no longer appear in doctor.ts: ${stale.sort().join(', ')}. ` +
|
|
`Remove them from src/core/doctor-categories.ts.`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('categorizeCheck', () => {
|
|
beforeEach(() => {
|
|
_resetUnknownCheckWarningsForTest();
|
|
});
|
|
|
|
test('returns the right category for a known brain name', () => {
|
|
expect(categorizeCheck('embedding_provider')).toBe('brain');
|
|
expect(categorizeCheck('graph_coverage')).toBe('brain');
|
|
expect(categorizeCheck('sync_freshness')).toBe('brain');
|
|
});
|
|
|
|
test('returns the right category for a known skill name', () => {
|
|
expect(categorizeCheck('resolver_health')).toBe('skill');
|
|
expect(categorizeCheck('skill_conformance')).toBe('skill');
|
|
});
|
|
|
|
test('returns the right category for a known ops name', () => {
|
|
expect(categorizeCheck('connection')).toBe('ops');
|
|
expect(categorizeCheck('rls')).toBe('ops');
|
|
expect(categorizeCheck('supervisor')).toBe('ops');
|
|
});
|
|
|
|
test('returns the right category for a known meta name', () => {
|
|
expect(categorizeCheck('schema_version')).toBe('meta');
|
|
expect(categorizeCheck('upgrade_errors')).toBe('meta');
|
|
});
|
|
|
|
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[] = [];
|
|
(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('made_up_check_name_not_in_any_set')).toBe('meta');
|
|
expect(categorizeCheck('made_up_check_name_not_in_any_set')).toBe('meta');
|
|
const warns = captured.filter((c) => c.includes('made_up_check_name_not_in_any_set'));
|
|
expect(warns.length).toBe(1);
|
|
} finally {
|
|
(process.stderr as { write: typeof process.stderr.write }).write = originalWrite;
|
|
}
|
|
});
|
|
});
|