feat(brain): aggregator — node heat, distributions, redirect rate (+4 tests)
This commit is contained in:
@@ -137,6 +137,41 @@ export function filterEpisodes(episodes, filter = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Aggregates a list of episodes into dashboard metrics.
|
||||
export function aggregate(episodes) {
|
||||
const nodeHeat = {};
|
||||
const pathType = {};
|
||||
const outcome = {};
|
||||
const classification = {};
|
||||
const economy = {};
|
||||
let totalErrors = 0;
|
||||
let totalRetries = 0;
|
||||
let redirects = 0;
|
||||
for (const e of episodes) {
|
||||
for (const id of attributeNodes(e).nodeIds) nodeHeat[id] = (nodeHeat[id] || 0) + 1;
|
||||
if (e.pathType) pathType[e.pathType] = (pathType[e.pathType] || 0) + 1;
|
||||
outcome[e.outcome] = (outcome[e.outcome] || 0) + 1;
|
||||
if (e.taskClassification) classification[e.taskClassification] = (classification[e.taskClassification] || 0) + 1;
|
||||
const lvl = e.environment ? e.environment.economy_level : null;
|
||||
const key = lvl == null ? 'n/a' : String(lvl);
|
||||
economy[key] = (economy[key] || 0) + 1;
|
||||
totalErrors += e.errorCount;
|
||||
totalRetries += e.retryCount;
|
||||
if (e.decisionProvenance && e.decisionProvenance.kind === 'user_directed_method') redirects++;
|
||||
}
|
||||
return {
|
||||
nodeHeat,
|
||||
pathType,
|
||||
outcome,
|
||||
classification,
|
||||
economy,
|
||||
totalErrors,
|
||||
totalRetries,
|
||||
redirectRate: episodes.length ? redirects / episodes.length : 0,
|
||||
count: episodes.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function parseEpisodes(text) {
|
||||
const episodes = [];
|
||||
let skipped = 0;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { parseEpisodes, normalizeEpisode, attributeNodes, filterEpisodes, groupBySession } from '../docs/observer/dashboard-core.js';
|
||||
import { parseEpisodes, normalizeEpisode, attributeNodes, filterEpisodes, groupBySession, aggregate } from '../docs/observer/dashboard-core.js';
|
||||
|
||||
const v1 = {
|
||||
task_id: 'a', timestamps: { started_at: '2026-05-19T05:18:16.342Z', ended_at: '2026-05-19T06:05:55.439Z' },
|
||||
@@ -138,3 +138,33 @@ describe('groupBySession', () => {
|
||||
expect(groups[0].taskRef).toBe('S');
|
||||
});
|
||||
});
|
||||
|
||||
describe('aggregate', () => {
|
||||
const mk = (over) => normalizeEpisode({ ...v2, ...over });
|
||||
it('counts node heat from attributed nodes', () => {
|
||||
const list = [
|
||||
mk({ events: [{ kind: 'skill_invoked', skill: 'superpowers:writing-plans' }] }),
|
||||
mk({ events: [{ kind: 'skill_invoked', skill: 'superpowers:writing-plans' }] }),
|
||||
];
|
||||
expect(aggregate(list).nodeHeat.sk_wplans).toBe(2);
|
||||
});
|
||||
it('computes redirect rate', () => {
|
||||
const list = [
|
||||
mk({ decision_provenance: { kind: 'user_directed_method', claude_would_have_chosen: 'x' } }),
|
||||
mk({ decision_provenance: { kind: 'autonomous', claude_would_have_chosen: null } }),
|
||||
];
|
||||
expect(aggregate(list).redirectRate).toBe(0.5);
|
||||
});
|
||||
it('tallies path_type and outcome distributions', () => {
|
||||
const list = [mk({ path_type: 'improvised', outcome: 'unknown' }), mk({ path_type: 'regulated', outcome: 'success' })];
|
||||
const a = aggregate(list);
|
||||
expect(a.pathType).toEqual({ improvised: 1, regulated: 1 });
|
||||
expect(a.outcome).toEqual({ unknown: 1, success: 1 });
|
||||
});
|
||||
it('reports total error and retry counts', () => {
|
||||
const list = [mk({ events: [{ kind: 'error', message: 'e' }, { kind: 'retry' }] })];
|
||||
const a = aggregate(list);
|
||||
expect(a.totalErrors).toBe(1);
|
||||
expect(a.totalRetries).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user