mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-17 02:12:40 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
00ebc5bb61 |
@@ -142,16 +142,12 @@ export async function runOnboard(engine: BrainEngine, args: string[]): Promise<v
|
||||
|
||||
// --auto path: runs through the T2 library orchestrator. Hooks emit CLI
|
||||
// progress to stderr; the final result lands as JSON on stdout (or human
|
||||
// summary). extraRemediations (gathered above from runAllOnboardChecks)
|
||||
// is threaded into the runner so the onboard-check remediations
|
||||
// (extract-ner, extract-timeline-from-meetings, etc.) reach the planner
|
||||
// — the same wiring the --check path uses above.
|
||||
// summary).
|
||||
const result = await runRemediation(
|
||||
engine,
|
||||
{
|
||||
targetScore,
|
||||
maxUsd,
|
||||
extraRemediations,
|
||||
// --auto --yes opts into the prompt_required tier too; library
|
||||
// doesn't distinguish auto_apply vs prompt_required, it just runs
|
||||
// every remediation in the plan. The plan-building side (T12 render)
|
||||
|
||||
@@ -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',
|
||||
]);
|
||||
|
||||
|
||||
@@ -4957,7 +4957,7 @@ const run_onboard: Operation = {
|
||||
// typo, the underlying queue.add would reject. Defense-in-depth.
|
||||
const result = await runRemediation(
|
||||
ctx.engine,
|
||||
{ targetScore, maxUsd, extraRemediations: allowedExtras },
|
||||
{ targetScore, maxUsd },
|
||||
{},
|
||||
);
|
||||
|
||||
|
||||
@@ -66,10 +66,9 @@ export async function runRemediation(
|
||||
} = await import('../remediation-checkpoint.ts');
|
||||
|
||||
const ctx = await loadRecommendationContext(engine);
|
||||
const extraRemediations = opts.extraRemediations ?? [];
|
||||
|
||||
// Pre-flight ceiling check via the shared plan computation.
|
||||
const initialPlan = await computeRemediationPlan(engine, { targetScore, extraRemediations });
|
||||
const initialPlan = await computeRemediationPlan(engine, { targetScore });
|
||||
if (initialPlan.target_unreachable) {
|
||||
hooks.onTargetUnreachable?.(targetScore, initialPlan.max_reachable_score);
|
||||
return {
|
||||
@@ -88,7 +87,7 @@ export async function runRemediation(
|
||||
}
|
||||
|
||||
const initialHealth = await engine.getHealth();
|
||||
let recs: RemediationStep[] = computeRecommendations(initialHealth, ctx, extraRemediations)
|
||||
let recs: RemediationStep[] = computeRecommendations(initialHealth, ctx)
|
||||
.filter((r) => r.status === 'remediable');
|
||||
if (recs.length === 0) {
|
||||
hooks.onNothingToDo?.(initialHealth.brain_score, targetScore);
|
||||
@@ -306,13 +305,7 @@ export async function runRemediation(
|
||||
// steps with bumped retry suffix (D1).
|
||||
if (recs.length === 0 || stepCount >= maxJobs) break;
|
||||
const freshHealth = await engine.getHealth();
|
||||
// Extras carry a static status:'remediable' — a fresh health snapshot
|
||||
// never ages them out the way health-derived steps drop. Filter out
|
||||
// ids this run already processed (any terminal status), or the recheck
|
||||
// would resubmit completed extras every iteration, forever.
|
||||
const processedIds = new Set(submitted.map((s) => s.id));
|
||||
const pendingExtras = extraRemediations.filter((r) => !processedIds.has(r.id));
|
||||
recs = computeRecommendations(freshHealth, ctx, pendingExtras).filter((r) => r.status === 'remediable');
|
||||
recs = computeRecommendations(freshHealth, ctx).filter((r) => r.status === 'remediable');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -63,16 +63,6 @@ export interface RemediationOpts {
|
||||
resumePlanHash?: string;
|
||||
/** Whether to attempt resume at all (default false). */
|
||||
resume?: boolean;
|
||||
/**
|
||||
* Caller-supplied RemediationStep entries threaded into the planner.
|
||||
* Mirrors RemediationPlanOpts.extraRemediations so onboard's --apply
|
||||
* --auto path (and MCP run_onboard auto modes) forward the same
|
||||
* onboard-check remediations the --check path already passes through
|
||||
* computeRemediationPlan. Without this the runner saw only generic
|
||||
* brain_score remediations and reported "Nothing to do" whenever the
|
||||
* only applicable work was an extra (e.g. extract-ner).
|
||||
*/
|
||||
extraRemediations?: RemediationStep[];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// test/remediation-run-extras.serial.test.ts
|
||||
// Regression for PR #2161 takeover: `gbrain onboard --apply --auto` dropped
|
||||
// onboard-check extraRemediations. Two distinct halves of the bug:
|
||||
// 1. runRemediation built the pre-flight plan + initial recs WITHOUT the
|
||||
// extras, so an extras-only plan reported "Nothing to do".
|
||||
// 2. The D7 mid-run recheck rebuilt recs WITHOUT the extras after every
|
||||
// completed step, so with 2+ plannable steps all remaining extras were
|
||||
// dropped after step 1. The recheck must also filter out extras this
|
||||
// run already processed — extras carry static status:'remediable', so
|
||||
// unfiltered threading would resubmit completed extras forever.
|
||||
//
|
||||
// SERIAL: mock.module (queue + wait-for-completion stubs, R2) + GBRAIN_HOME
|
||||
// env mutation so checkpoint files land in a tmpdir, not ~/.gbrain.
|
||||
|
||||
import { describe, expect, test, beforeAll, afterAll, mock } from 'bun:test';
|
||||
import { mkdtempSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
||||
import { makeRemediationStep } from '../src/core/remediation-step.ts';
|
||||
|
||||
// Stub the Minion queue: every submitted job is immediately 'completed'.
|
||||
// runRemediation only calls queue.add + waitForCompletion(queue, id).
|
||||
let nextJobId = 1;
|
||||
const submittedJobs: Array<{ name: string }> = [];
|
||||
mock.module('../src/core/minions/queue.ts', () => ({
|
||||
MinionQueue: class {
|
||||
async add(name: string) {
|
||||
submittedJobs.push({ name });
|
||||
return { id: nextJobId++, status: 'completed' };
|
||||
}
|
||||
},
|
||||
}));
|
||||
mock.module('../src/core/minions/wait-for-completion.ts', () => ({
|
||||
waitForCompletion: async () => ({ status: 'completed' }),
|
||||
}));
|
||||
|
||||
let engine: PGLiteEngine;
|
||||
let home: string;
|
||||
const prevHome = process.env.GBRAIN_HOME;
|
||||
|
||||
beforeAll(async () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'gbrain-remextras-'));
|
||||
process.env.GBRAIN_HOME = home;
|
||||
engine = new PGLiteEngine();
|
||||
await engine.connect({});
|
||||
await engine.initSchema();
|
||||
}, 120_000);
|
||||
|
||||
afterAll(async () => {
|
||||
await engine.disconnect();
|
||||
if (prevHome === undefined) delete process.env.GBRAIN_HOME;
|
||||
else process.env.GBRAIN_HOME = prevHome;
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function extra(id: string, job: string) {
|
||||
return makeRemediationStep({
|
||||
id,
|
||||
job,
|
||||
params: {},
|
||||
severity: 'medium',
|
||||
est_seconds: 5,
|
||||
est_usd_cost: 0,
|
||||
rationale: 'synthetic onboard-check extra',
|
||||
status: 'remediable',
|
||||
});
|
||||
}
|
||||
|
||||
describe('runRemediation extraRemediations threading', () => {
|
||||
test('extras-only plan runs BOTH extras and terminates (no Nothing-to-do, no resubmit loop)', async () => {
|
||||
// Empty PGLite brain → zero health-derived recommendations. Without the
|
||||
// fix, half 1 makes this run return submitted: [] via onNothingToDo.
|
||||
// With only half 1 (the original PR #2161 diff), the mid-run recheck
|
||||
// drops the second extra after step 1 — submitted has 1 entry, not 2.
|
||||
const { runRemediation } = await import('../src/core/remediation/run.ts');
|
||||
let nothingToDo = false;
|
||||
const result = await runRemediation(
|
||||
engine,
|
||||
{
|
||||
targetScore: 1,
|
||||
extraRemediations: [
|
||||
extra('onboard.extract_ner', 'extract-ner'),
|
||||
extra('onboard.extract_timeline', 'extract-timeline-from-meetings'),
|
||||
],
|
||||
// Safety bound: an unfiltered recheck would resubmit completed
|
||||
// extras forever; maxJobs turns that regression into a fast fail
|
||||
// (extra count > 1 below) instead of a hung test.
|
||||
maxJobs: 5,
|
||||
},
|
||||
{ onNothingToDo: () => { nothingToDo = true; } },
|
||||
);
|
||||
|
||||
expect(nothingToDo).toBe(false);
|
||||
const ids = result.submitted.map((s) => s.id);
|
||||
expect(ids).toContain('onboard.extract_ner');
|
||||
expect(ids).toContain('onboard.extract_timeline');
|
||||
// Each extra ran exactly once — the recheck must not re-plan extras the
|
||||
// run already processed.
|
||||
expect(ids.filter((i) => i === 'onboard.extract_ner').length).toBe(1);
|
||||
expect(ids.filter((i) => i === 'onboard.extract_timeline').length).toBe(1);
|
||||
expect(result.submitted.every((s) => s.status === 'completed')).toBe(true);
|
||||
expect(submittedJobs.map((j) => j.name).sort()).toEqual([
|
||||
'extract-ner',
|
||||
'extract-timeline-from-meetings',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user