Files
brain/tools/discipline-metrics.test.mjs
T

185 lines
8.3 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
disciplinePercentByClassification,
routerStepReached,
deriveRouterStep,
boundariesAppliedRate,
} from './discipline-metrics.mjs';
const map = { feature: ['#19'], bugfix: ['#18'], refactor: ['#11', '#12'] };
function ep(overrides = {}) {
return {
schema_version: 2,
primary_rationale: {
task_classification: 'feature',
node_chosen: 'direct',
triggers_matched: [],
boundaries_applied: [],
step: 1,
},
path_type: 'regulated',
...overrides,
};
}
describe('disciplinePercentByClassification', () => {
it('counts episodes per classification', () => {
const eps = [
ep({ primary_rationale: { task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], boundaries_applied: [], step: 1 } }),
ep({ primary_rationale: { task_classification: 'feature', node_chosen: '#19', triggers_matched: [{node:'#19'}], boundaries_applied: [], step: 3 } }),
ep({ primary_rationale: { task_classification: 'bugfix', node_chosen: 'direct', triggers_matched: [], boundaries_applied: [], step: 1 } }),
];
const res = disciplinePercentByClassification(eps, map);
expect(res.feature.episodes).toBe(2);
expect(res.feature.withTriggerMatch).toBe(1);
expect(res.feature.viaSkill).toBe(1);
expect(res.feature.pctTriggerMatch).toBeCloseTo(0.5);
expect(res.feature.pctViaSkill).toBeCloseTo(0.5);
expect(res.bugfix.episodes).toBe(1);
expect(res.bugfix.pctTriggerMatch).toBe(0);
});
it('ignores classifications outside the map', () => {
const eps = [ep({ primary_rationale: { task_classification: 'unknown', node_chosen: 'direct', triggers_matched: [], boundaries_applied: [], step: 1 } })];
const res = disciplinePercentByClassification(eps, map);
expect(res.unknown).toBeUndefined();
});
it('ignores v1 episodes and observer_error markers', () => {
const eps = [
{ schema_version: 1, primary_rationale: { task_classification: 'feature', node_chosen: 'direct' } },
{ observer_error: true },
ep(),
];
const res = disciplinePercentByClassification(eps, map);
expect(res.feature.episodes).toBe(1);
});
it('returns empty object on empty input', () => {
expect(disciplinePercentByClassification([], map)).toEqual({});
});
});
describe('deriveRouterStep', () => {
// Маппинг наблюдаемых признаков primary_rationale → шаг router-procedure.md
// (1 hard-floor → 2 классификация → 3 триггеры → 4 цепочка → 5 исполнение узла).
// Берётся МАКСИМУМ достигнутой стадии. Хранимое pr.step игнорируется.
it('returns 1 for a bare direct episode (hard-floor only, no signals)', () => {
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [], chain_ref: [], node_chosen: 'direct' })).toBe(1);
});
it('returns 2 when a real task_classification was produced', () => {
expect(deriveRouterStep({ task_classification: 'feature', triggers_matched: [], chain_ref: [], node_chosen: 'direct' })).toBe(2);
});
it("treats 'other' classification as not reaching step 2", () => {
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [], chain_ref: null, node_chosen: 'direct' })).toBe(1);
});
it('returns 3 when triggers matched', () => {
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [{ keyword: 'x' }], chain_ref: [], node_chosen: 'direct' })).toBe(3);
});
it('returns 4 when a chain was referenced (array or non-empty string)', () => {
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [], chain_ref: ['routing-off-phase L1'], node_chosen: 'direct' })).toBe(4);
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [], chain_ref: 'L1', node_chosen: 'direct' })).toBe(4);
});
it('returns 5 when a node was actually chosen (execution)', () => {
expect(deriveRouterStep({ task_classification: 'other', triggers_matched: [], chain_ref: [], node_chosen: '#19' })).toBe(5);
});
it('takes the furthest stage reached (max), not the first', () => {
expect(deriveRouterStep({ task_classification: 'feature', triggers_matched: [{ k: 1 }], chain_ref: [], node_chosen: '#19' })).toBe(5);
});
it('handles a missing/empty primary_rationale → 1', () => {
expect(deriveRouterStep(undefined)).toBe(1);
expect(deriveRouterStep({})).toBe(1);
});
});
describe('routerStepReached (derived from observable signals)', () => {
// Признаковые шаблоны (хранимый step специально проставлен 1/99 — должен игнорироваться).
const at = {
1: { task_classification: 'other', triggers_matched: [], chain_ref: [], node_chosen: 'direct' },
3: { task_classification: 'other', triggers_matched: [{ k: 1 }], chain_ref: [], node_chosen: 'direct' },
5: { task_classification: 'feature', triggers_matched: [], chain_ref: [], node_chosen: '#19' },
};
it('counts episodes by derived step, ignoring any stored pr.step value', () => {
const eps = [
ep({ primary_rationale: { ...at[1], step: 1 } }),
ep({ primary_rationale: { ...at[1], step: 99 } }),
ep({ primary_rationale: { ...at[3], step: 1 } }),
ep({ primary_rationale: { ...at[5], step: 1 } }),
];
const res = routerStepReached(eps);
expect(res.distribution['1']).toBe(2);
expect(res.distribution['3']).toBe(1);
expect(res.distribution['5']).toBe(1);
expect(res.total).toBe(4);
});
it('flags suspicious=true when >90% эпизодов выводятся в step 1', () => {
const eps = Array.from({ length: 11 }, (_, i) =>
ep({ primary_rationale: i === 10 ? { ...at[3], step: 1 } : { ...at[1], step: 1 } })
);
expect(routerStepReached(eps).suspicious).toBe(true);
});
it('suspicious=false when distribution более равномерное', () => {
const eps = [
ep({ primary_rationale: { ...at[1], step: 1 } }),
ep({ primary_rationale: { ...at[3], step: 1 } }),
ep({ primary_rationale: { ...at[5], step: 1 } }),
];
expect(routerStepReached(eps).suspicious).toBe(false);
});
it('ignores v1 episodes and observer_error markers', () => {
const eps = [
{ schema_version: 1, primary_rationale: { ...at[5] } },
{ observer_error: true },
ep({ primary_rationale: { ...at[3], step: 1 } }),
];
const res = routerStepReached(eps);
expect(res.distribution).toEqual({ '3': 1 });
expect(res.total).toBe(1);
});
});
describe('boundariesAppliedRate', () => {
it('counts overall rate of boundaries applied', () => {
const eps = [
ep({ primary_rationale: { boundaries_applied: [{ adr: 'ADR-001' }], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
ep({ primary_rationale: { boundaries_applied: [], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
ep({ primary_rationale: { boundaries_applied: [{ adr: 'ADR-002' }], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
];
const res = boundariesAppliedRate(eps);
expect(res.total).toBe(3);
expect(res.withBoundaries).toBe(2);
expect(res.rate).toBeCloseTo(2 / 3);
});
it('splits by path_type', () => {
const eps = [
ep({ path_type: 'regulated', primary_rationale: { boundaries_applied: [{ adr: 'X' }], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
ep({ path_type: 'regulated', primary_rationale: { boundaries_applied: [], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
ep({ path_type: 'free', primary_rationale: { boundaries_applied: [{ adr: 'Y' }], task_classification: 'feature', node_chosen: 'direct', triggers_matched: [], step: 1 } }),
];
const res = boundariesAppliedRate(eps);
expect(res.byPathType.regulated.total).toBe(2);
expect(res.byPathType.regulated.withBoundaries).toBe(1);
expect(res.byPathType.free.total).toBe(1);
expect(res.byPathType.free.withBoundaries).toBe(1);
});
it('returns rate=0 on empty input', () => {
expect(boundariesAppliedRate([])).toEqual({
total: 0, withBoundaries: 0, rate: 0, byPathType: {},
});
});
});