feat(observer): state enricher helper для эпизодов (stage 3 follow-up 2)
readRouterState(sessionId, {baseDir}) -- pure read state-файла сторожа.
extractRouterFields(state) -- pure извлечение 4 полей для primary_rationale.
Используется парсером эпизодов на следующем шаге (Task 3).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+10
-10
@@ -1,6 +1,6 @@
|
||||
# Brain Status (auto-generated)
|
||||
|
||||
Last updated: 2026-05-24T08:32:29.613Z
|
||||
Last updated: 2026-05-24T12:42:18.762Z
|
||||
|
||||
| Контролёр | Состояние | Детали |
|
||||
|---|---|---|
|
||||
@@ -8,14 +8,14 @@ Last updated: 2026-05-24T08:32:29.613Z
|
||||
| C2 Cross-ref consistency | ✅ | [cross-ref-checker] OK — 0 drift in 4 files |
|
||||
| C3 Observer-of-observer | ✅ | [observer-of-observer] OK — last read 0 week(s) ago |
|
||||
| C4 Сигнальный статус | ✅ | This file (self-reference) |
|
||||
| C5 Observer-coverage | ⚠️ | 134 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) · 17 missed activation(s) — see /brain-retro |
|
||||
| C5 Observer-coverage | ⚠️ | 135 episode(s) this month · .git/hooks/post-commit not installed (run: npx lefthook install --force) · 17 missed activation(s) — see /brain-retro |
|
||||
| C6 Chain map sync | ✅ | [chain-map-checker] OK — 16 chains in sync |
|
||||
|
||||
## Метрики (информационные, не алерты)
|
||||
|
||||
- Observer evidence: 134 episodes this month, 0 observer_error markers, 5 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 10
|
||||
- Last /brain-retro: 0 day(s) ago
|
||||
- Observer evidence: 135 episodes this month, 0 observer_error markers, 6 PII matches before filter
|
||||
- Legacy v1 episodes (not in factor analysis): 11
|
||||
- Last /brain-retro: 1 day(s) ago
|
||||
- Использование узлов: см. `/brain-retro` (раз в спринт). missed_activations: 17. **Неиспользованные узлы — не алерт, если профильной задачи не было** (Pravila §16.4 v1.36; capability-readiness; см. memory `feedback_brain_unused_tools_not_problem` — outside-repo memory store).
|
||||
|
||||
## Метрики дисциплины
|
||||
@@ -32,17 +32,17 @@ Baseline дисциплины роутера (этап 2 router discipline overh
|
||||
| cleanup | 1 | 0.0% | 0.0% |
|
||||
| monitoring | 1 | 0.0% | 0.0% |
|
||||
|
||||
Router step distribution: 1: 129 ⚠️ suspicious — >90% эпизодов остановились на step=1 (вероятный sentinel-bug парсера)
|
||||
Router step distribution: 1: 55, 2: 45, 3: 12, 5: 18
|
||||
|
||||
Boundaries applied (ADR / границы): 13 of 129 эпизодов (10.1%).
|
||||
Boundaries applied (ADR / границы): 13 of 130 эпизодов (10.0%).
|
||||
|
||||
## Активные многоэтапные проекты
|
||||
|
||||
- **Router discipline overhaul** ([spec](../superpowers/specs/2026-05-23-router-discipline-overhaul-design.md))
|
||||
- Этап 1 (машиночитаемый реестр) ✅ закрыт 2026-05-23 — `docs/registry/nodes.yaml` (83 узла + 16 chains L1-L16), `tools/registry-load.mjs` + `tools/registry-render.mjs` (16 тестов), auto-render Tooling §4.0 + routing-off-phase, lefthook job 17 (warn-only).
|
||||
- Этап 2 (измерения + классификатор-парсер) ✅ закрыт 2026-05-24 — discipline-metrics (3 среза), brain-retro-analyzer переключён на реестр, STATUS.md блок «Метрики дисциплины», baseline snapshot `docs/observer/baselines/2026-05-24-pre-enforcement.md`. Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-2-measurements.md`.
|
||||
- Этап 3 (принуждение — хук на routing) ⏸ ждёт «продолжаем» от заказчика после ревью baseline-цифр.
|
||||
- Этап 4 (уборка правил) — не начат.
|
||||
- Этап 2 (измерения + классификатор-парсер) ✅ закрыт 2026-05-24 + влит в main 2026-05-24 — discipline-metrics (3 среза), brain-retro-analyzer переключён на реестр, STATUS.md блок «Метрики дисциплины», baseline snapshot `docs/observer/baselines/2026-05-24-pre-enforcement.md`. Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-2-measurements.md`.
|
||||
- Этап 3 (принуждение — хук на routing) — Phase A+B (классификатор + 3 хука: router-prehook/tool-gate/stop-gate в `.claude/settings.json`) ✅ + влит в main 2026-05-24. Гейт работает в режиме **`warn-only`** (только stderr-предупреждения, никакой блокировки). Bug-fix `bec69aa5`: `deriveRouterStep` в `tools/discipline-metrics.mjs` — шаг роутера теперь выводится из наблюдаемых признаков (был захардкоженной константой 1). CHECKPOINT B: дать warn-only накопить реальные наблюдения (план говорит «минимум 24 часа»), затем Task 9 — переключение в `enforce` + 2 новых метрики (domain-hit-rate / chain-completion). Plan: `docs/superpowers/plans/2026-05-24-router-overhaul-stage-3-enforcement.md`.
|
||||
- Этап 4 (уборка устаревших правил, deprecation `observer-classification-map.json` → удаление) — не начат.
|
||||
|
||||
## Алерт-индикаторы
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Router state enricher for observer episodes.
|
||||
* Reads ~/.claude/runtime/router-state-<sessionId>.json and exposes pure
|
||||
* extraction helpers for primary_rationale enrichment.
|
||||
*
|
||||
* Pure-ish — fs is parameterized via options.baseDir for testability.
|
||||
*
|
||||
* Per spec: docs/superpowers/specs/2026-05-24-router-stage3-three-fixes-design.md
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { homedir } from 'os';
|
||||
|
||||
function defaultBaseDir() {
|
||||
return join(homedir(), '.claude', 'runtime');
|
||||
}
|
||||
|
||||
export function readRouterState(sessionId, options = {}) {
|
||||
if (!sessionId || typeof sessionId !== 'string') return null;
|
||||
const baseDir = options.baseDir || defaultBaseDir();
|
||||
const path = join(baseDir, `router-state-${sessionId}.json`);
|
||||
if (!existsSync(path)) return null;
|
||||
try {
|
||||
const content = readFileSync(path, 'utf-8');
|
||||
return JSON.parse(content);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function extractRouterFields(state) {
|
||||
if (!state || typeof state !== 'object') {
|
||||
return { recommended_node: null, recommended_chain: null, chain_progress: [], chain_completed: false };
|
||||
}
|
||||
const cls = state.classification || {};
|
||||
return {
|
||||
recommended_node: cls.recommendedNode || null,
|
||||
recommended_chain: cls.recommendedChain || null,
|
||||
chain_progress: Array.isArray(state.chainProgress) ? state.chainProgress : [],
|
||||
chain_completed: state.chainCompleted === true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { mkdtempSync, writeFileSync, rmSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
import { readRouterState } from './observer-state-enricher.mjs';
|
||||
|
||||
describe('readRouterState', () => {
|
||||
let baseDir;
|
||||
|
||||
beforeEach(() => {
|
||||
baseDir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
rmSync(baseDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('returns null when state file does not exist', () => {
|
||||
expect(readRouterState('abc-123', { baseDir })).toBeNull();
|
||||
});
|
||||
|
||||
it('reads state file when present', () => {
|
||||
const state = {
|
||||
sessionId: 'abc-123',
|
||||
classification: { recommendedNode: '#62', recommendedChain: '#13' },
|
||||
chainProgress: ['brainstorming'],
|
||||
chainCompleted: false,
|
||||
};
|
||||
writeFileSync(join(baseDir, 'router-state-abc-123.json'), JSON.stringify(state));
|
||||
const result = readRouterState('abc-123', { baseDir });
|
||||
expect(result).toEqual(state);
|
||||
});
|
||||
|
||||
it('returns null on malformed JSON', () => {
|
||||
writeFileSync(join(baseDir, 'router-state-broken.json'), 'not-json');
|
||||
expect(readRouterState('broken', { baseDir })).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null on missing sessionId', () => {
|
||||
expect(readRouterState(null, { baseDir })).toBeNull();
|
||||
expect(readRouterState('', { baseDir })).toBeNull();
|
||||
});
|
||||
|
||||
it('uses ~/.claude/runtime/ as default baseDir', () => {
|
||||
// Smoke-check: default baseDir resolution doesn't throw.
|
||||
// Real-file reading covered above with explicit baseDir.
|
||||
const result = readRouterState('non-existent-session-xyz');
|
||||
// Either null (file doesn't exist there) or object — both fine.
|
||||
expect(result === null || typeof result === 'object').toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractRouterFields', () => {
|
||||
it('extracts the four fields from state, defaulting to null/empty', async () => {
|
||||
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
||||
const state = {
|
||||
classification: { recommendedNode: '#62', recommendedChain: '#13' },
|
||||
chainProgress: ['brainstorming', 'writing-plans'],
|
||||
chainCompleted: false,
|
||||
};
|
||||
expect(extractRouterFields(state)).toEqual({
|
||||
recommended_node: '#62',
|
||||
recommended_chain: '#13',
|
||||
chain_progress: ['brainstorming', 'writing-plans'],
|
||||
chain_completed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('returns nulls/empty when state is null', async () => {
|
||||
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
||||
expect(extractRouterFields(null)).toEqual({
|
||||
recommended_node: null,
|
||||
recommended_chain: null,
|
||||
chain_progress: [],
|
||||
chain_completed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('handles missing classification block', async () => {
|
||||
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
||||
expect(extractRouterFields({ chainProgress: ['x'], chainCompleted: true })).toEqual({
|
||||
recommended_node: null,
|
||||
recommended_chain: null,
|
||||
chain_progress: ['x'],
|
||||
chain_completed: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats empty string recommendedNode/recommendedChain as null', async () => {
|
||||
const { extractRouterFields } = await import('./observer-state-enricher.mjs');
|
||||
expect(extractRouterFields({ classification: { recommendedNode: '', recommendedChain: '' } })).toEqual({
|
||||
recommended_node: null,
|
||||
recommended_chain: null,
|
||||
chain_progress: [],
|
||||
chain_completed: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user