mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-31 04:07:52 +00:00
Life Chronicle Phase A.7. A brain-state advisor collector surfaces two proactive signals in `gbrain advisor`: unresolved ontology conflicts (warn, → `gbrain ontology-contradictions`) and recent meetings with no timeline coverage (info, → `gbrain chronicle-backfill`). Advisory-only (no dispatch_id); runs over MCP too (not workspace-dependent); tolerant of pre-migration brains. 3 tests + advisor-op-gate green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
/**
|
|
* advisor/run.ts — runs the collectors and ranks the findings.
|
|
*
|
|
* Hardcoded v1 collector list (open-Q4): a static array keeps ordering
|
|
* deterministic and the surface auditable. Each collector runs in its OWN
|
|
* try/catch so one failure never kills the whole report. Workspace-dependent
|
|
* collectors (A1) are dropped over MCP.
|
|
*/
|
|
|
|
import type { AdvisorCollector, AdvisorContext, AdvisorFinding, AdvisorReport, AdvisorSeverity } from './types.ts';
|
|
import { collectVersion } from './collect-version.ts';
|
|
import { collectMigration } from './collect-migration.ts';
|
|
import { collectSchemaPack } from './collect-schema-pack.ts';
|
|
import { collectStalledJobs } from './collect-stalled-jobs.ts';
|
|
import { collectUsageShape } from './collect-usage-shape.ts';
|
|
import { collectSetupSmells } from './collect-setup-smells.ts';
|
|
import { collectUninstalledBrainPack } from './collect-uninstalled-brain-pack.ts';
|
|
import { collectUninstalledBundled } from './collect-uninstalled-bundled.ts';
|
|
import { collectChronicle } from './collect-chronicle.ts';
|
|
|
|
/** Deterministic v1 collector order (also the secondary sort key for ranking). */
|
|
export const COLLECTORS: AdvisorCollector[] = [
|
|
collectVersion,
|
|
collectMigration,
|
|
collectSchemaPack,
|
|
collectStalledJobs,
|
|
collectUsageShape,
|
|
collectSetupSmells,
|
|
collectUninstalledBrainPack,
|
|
collectUninstalledBundled,
|
|
collectChronicle,
|
|
];
|
|
|
|
const SEV_RANK: Record<AdvisorSeverity, number> = { critical: 0, warn: 1, info: 2 };
|
|
|
|
/**
|
|
* Rank: critical > warn > info, ties broken by collector order (stable). All
|
|
* criticals are always kept; the info tail is capped so the agent isn't buried.
|
|
*/
|
|
export function rankFindings(findings: AdvisorFinding[], opts: { infoCap?: number } = {}): AdvisorFinding[] {
|
|
const order = new Map(COLLECTORS.map((c, i) => [c.id, i] as const));
|
|
const sorted = [...findings].sort((a, b) => {
|
|
const s = SEV_RANK[a.severity] - SEV_RANK[b.severity];
|
|
if (s !== 0) return s;
|
|
return (order.get(a.collector) ?? 99) - (order.get(b.collector) ?? 99);
|
|
});
|
|
const infoCap = opts.infoCap ?? 10;
|
|
const out: AdvisorFinding[] = [];
|
|
let infoSeen = 0;
|
|
for (const f of sorted) {
|
|
if (f.severity === 'info') {
|
|
if (infoSeen >= infoCap) continue;
|
|
infoSeen++;
|
|
}
|
|
out.push(f);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Run every collector and return a ranked report. Resilient: a collector that
|
|
* throws contributes nothing and never aborts the others.
|
|
*/
|
|
export async function runAdvisor(ctx: AdvisorContext): Promise<AdvisorReport> {
|
|
const all: AdvisorFinding[] = [];
|
|
for (const c of COLLECTORS) {
|
|
try {
|
|
const found = await c.collect(ctx);
|
|
for (const f of found) {
|
|
// A1: drop workspace-dependent findings over MCP.
|
|
if (ctx.remote && f.workspace_dependent) continue;
|
|
all.push(f);
|
|
}
|
|
} catch {
|
|
// one collector failing must not kill the report
|
|
}
|
|
}
|
|
const ranked = rankFindings(all);
|
|
const worst: AdvisorSeverity | null =
|
|
ranked.some((f) => f.severity === 'critical')
|
|
? 'critical'
|
|
: ranked.some((f) => f.severity === 'warn')
|
|
? 'warn'
|
|
: ranked.length > 0
|
|
? 'info'
|
|
: null;
|
|
return {
|
|
version: ctx.version,
|
|
generated_at: ctx.now.toISOString(),
|
|
findings: ranked,
|
|
worst,
|
|
};
|
|
}
|