feat(observer): обогащение primary_rationale из router-state (Task 3)

- parseTranscript получает третий параметр options = {}
- options.routerStateBaseDir пробрасывается в readRouterState
- recommended_node: router-state переопределяет classification-map
- новые поля: recommended_chain, chain_progress, chain_completed
- 2 новых теста (enrich + fallback), 538/538 tools GREEN

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Дмитрий
2026-05-24 15:53:59 +03:00
parent 593f12ae6a
commit 92bbd64eed
2 changed files with 73 additions and 5 deletions
+59
View File
@@ -1,4 +1,7 @@
import { describe, it, expect } from 'vitest';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import {
parseTranscript,
extractEnvironment,
@@ -1655,3 +1658,59 @@ describe('parseTranscript v3 fields', () => {
expect(typeof hookEvent.scripts).toBe('object');
});
});
describe('parseTranscript — router-state enrichment (Task 3)', () => {
function makeTranscript(sessionId) {
return [
JSON.stringify({
type: 'user',
message: { role: 'user', content: 'добавь новый endpoint /api/bar' },
timestamp: '2026-05-24T10:00:00Z',
uuid: 'u-t3-1',
sessionId,
}),
JSON.stringify({
type: 'assistant',
message: { role: 'assistant', content: [{ type: 'text', text: 'делаю' }] },
timestamp: '2026-05-24T10:00:01Z',
uuid: 'u-t3-2',
sessionId,
}),
].join('\n');
}
it('enriches primary_rationale from router-state file when present', () => {
const dir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
const sessionId = 'test-session-t3-enrich';
const state = {
classification: { recommendedNode: '#42', recommendedChain: 'L13' },
chainProgress: ['step-a', 'step-b'],
chainCompleted: false,
};
writeFileSync(join(dir, `router-state-${sessionId}.json`), JSON.stringify(state));
try {
const ep = parseTranscript(makeTranscript(sessionId), sessionId, { routerStateBaseDir: dir });
expect(ep.primary_rationale.recommended_node).toBe('#42');
expect(ep.primary_rationale.recommended_chain).toBe('L13');
expect(ep.primary_rationale.chain_progress).toEqual(['step-a', 'step-b']);
expect(ep.primary_rationale.chain_completed).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
it('falls back gracefully when router-state file is absent', () => {
const dir = mkdtempSync(join(tmpdir(), 'router-state-test-'));
const sessionId = 'test-session-t3-missing';
try {
const ep = parseTranscript(makeTranscript(sessionId), sessionId, { routerStateBaseDir: dir });
// recommended_node falls back to classification-map result (direct episode → feature → #19)
expect(ep.primary_rationale.recommended_node).toBe('#19');
expect(ep.primary_rationale.recommended_chain).toBeNull();
expect(ep.primary_rationale.chain_progress).toEqual([]);
expect(ep.primary_rationale.chain_completed).toBe(false);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});