feat(observer): parser v2 — environment, task_size, prompt_signal extractors

This commit is contained in:
Дмитрий
2026-05-19 10:15:17 +03:00
parent 2501b00079
commit 85a95aa2d0
3 changed files with 171 additions and 6 deletions
+91 -1
View File
@@ -1,5 +1,10 @@
import { describe, it, expect } from 'vitest';
import { parseTranscript } from './observer-transcript-parser.mjs';
import {
parseTranscript,
extractEnvironment,
extractTaskSize,
classifyPromptSignal,
} from './observer-transcript-parser.mjs';
// Build a JSONL transcript string from entry objects.
function jsonl(entries) {
@@ -215,3 +220,88 @@ describe('parseTranscript', () => {
}
});
});
describe('extractEnvironment', () => {
it('reads economy_level from the ECONOMY MODE marker', () => {
const entries = [
userPrompt('=== ECONOMY MODE: 0% (пользователь указал явно) ===\nfix it', '2026-05-19T10:00:00Z'),
];
expect(extractEnvironment(entries, 0).economy_level).toBe(0);
});
it('economy_level is null when no marker present', () => {
const entries = [userPrompt('just do it', '2026-05-19T10:00:00Z')];
expect(extractEnvironment(entries, 0).economy_level).toBeNull();
});
it('reads model from an assistant message', () => {
const entries = [
userPrompt('go', '2026-05-19T10:00:00Z'),
{ type: 'assistant', message: { role: 'assistant', model: 'claude-opus-4-7', content: [] }, timestamp: '2026-05-19T10:01:00Z', sessionId: 's1' },
];
expect(extractEnvironment(entries, 0).model).toBe('claude-opus-4-7');
});
it('post_compaction is true when an isCompactSummary entry precedes the turn', () => {
const entries = [
{ type: 'user', isCompactSummary: true, message: { role: 'user', content: 'summary' }, timestamp: '2026-05-19T09:00:00Z' },
userPrompt('the real turn', '2026-05-19T10:00:00Z'),
];
expect(extractEnvironment(entries, 1).post_compaction).toBe(true);
});
it('post_compaction is false with no compaction marker', () => {
const entries = [userPrompt('turn one', '2026-05-19T09:00:00Z'), userPrompt('turn two', '2026-05-19T10:00:00Z')];
expect(extractEnvironment(entries, 1).post_compaction).toBe(false);
});
it('session_turn counts real user prompts up to and including the turn start', () => {
const entries = [
userPrompt('one', '2026-05-19T09:00:00Z'),
userPrompt('two', '2026-05-19T09:30:00Z'),
userPrompt('three', '2026-05-19T10:00:00Z'),
];
expect(extractEnvironment(entries, 2).session_turn).toBe(3);
});
});
describe('extractTaskSize', () => {
it('counts tool calls and unique file paths', () => {
const turn = [
assistantTurn(
[
{ type: 'tool_use', id: 't1', name: 'Read', input: { file_path: '/a.js' } },
{ type: 'tool_use', id: 't2', name: 'Edit', input: { file_path: '/a.js' } },
{ type: 'tool_use', id: 't3', name: 'Write', input: { file_path: '/b.js' } },
{ type: 'tool_use', id: 't4', name: 'Bash', input: {} },
],
'2026-05-19T10:01:00Z'
),
];
const size = extractTaskSize(turn);
expect(size.tool_calls).toBe(4);
expect(size.files_touched).toBe(2);
expect(size.files.sort()).toEqual(['/a.js', '/b.js']);
});
it('returns zeros for an empty turn', () => {
expect(extractTaskSize([])).toEqual({ tool_calls: 0, files_touched: 0, files: [] });
});
});
describe('classifyPromptSignal', () => {
it('detects corrections', () => {
expect(classifyPromptSignal('не то, переделай')).toBe('correction');
expect(classifyPromptSignal('почему ты это сделал')).toBe('correction');
});
it('detects approvals', () => {
expect(classifyPromptSignal('ок, спасибо')).toBe('approval');
expect(classifyPromptSignal('готово, дальше')).toBe('approval');
});
it('detects a new task', () => {
expect(classifyPromptSignal('добавь новую фичу экспорта в CSV')).toBe('new_task');
});
it('falls back to neutral', () => {
expect(classifyPromptSignal('hmm')).toBe('neutral');
});
});