feat(observer): heuristic reasoning capture in primary_rationale
Closes brain-retro 2026-05-20 #6 — extractTriggers/Candidates/Boundaries scan assistant.text for Pravila §N / ADR-N / PSR_v1 RX / routing-off-phase LN / hard-floor + numbered/bulleted lists (≥2). Populates previously- always-empty primary_rationale arrays. Conservative-broad: false positives accepted (mention ≠ application); /brain-retro determines applied validity. Phase 2 agent-judge out of scope. 19 new tests, 282/282 GREEN. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1080,3 +1080,103 @@ describe('parseTranscript — ask_user_question events (Task 4)', () => {
|
||||
expect(aq[0].answer_kind).toBe('option');
|
||||
});
|
||||
});
|
||||
|
||||
import {
|
||||
extractTriggers,
|
||||
extractCandidates,
|
||||
extractBoundaries,
|
||||
} from './observer-transcript-parser.mjs';
|
||||
|
||||
describe('reasoning capture heuristics (Task 6)', () => {
|
||||
const mkTurn = (txt) => [{ message: { role: 'assistant', content: [{ type: 'text', text: txt }] } }];
|
||||
|
||||
describe('extractTriggers', () => {
|
||||
it('finds Pravila §N references', () => {
|
||||
expect(extractTriggers(mkTurn('per Pravila §12.2 hard-rule'))).toContain('Pravila §12.2');
|
||||
});
|
||||
it('finds ADR references', () => {
|
||||
expect(extractTriggers(mkTurn('see ADR-011 anchor'))).toContain('ADR-011');
|
||||
});
|
||||
it('finds PSR_v1 R refs', () => {
|
||||
expect(extractTriggers(mkTurn('PSR_v1 R10.1 requires it'))).toContain('PSR_v1 R10.1');
|
||||
});
|
||||
it('finds routing-off-phase L refs from canonical form', () => {
|
||||
expect(extractTriggers(mkTurn('routing-off-phase L12 chain'))).toContain('routing-off-phase L12');
|
||||
});
|
||||
it('finds hard-rule / hard-floor (case-insensitive)', () => {
|
||||
const res = extractTriggers(mkTurn('this is a hard-rule per §15'));
|
||||
expect(res.some((t) => t.toLowerCase().includes('hard-rule'))).toBe(true);
|
||||
});
|
||||
it('deduplicates repeated triggers', () => {
|
||||
const res = extractTriggers(mkTurn('Pravila §16 and Pravila §16 again'));
|
||||
expect(res.filter((t) => t === 'Pravila §16')).toHaveLength(1);
|
||||
});
|
||||
it('returns empty for plain prose', () => {
|
||||
expect(extractTriggers(mkTurn('just plain text'))).toEqual([]);
|
||||
});
|
||||
it('safe on null/empty', () => {
|
||||
expect(extractTriggers(null)).toEqual([]);
|
||||
expect(extractTriggers([])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractCandidates', () => {
|
||||
it('extracts numbered options (≥2)', () => {
|
||||
const c = extractCandidates(mkTurn('1. brainstorming\n2. subagent-driven\n3. direct'));
|
||||
expect(c).toContain('brainstorming');
|
||||
expect(c.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
it('extracts bullets when no numbered', () => {
|
||||
expect(extractCandidates(mkTurn('- A\n- B\n- C')).length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
it('prefers numbered over bullets', () => {
|
||||
const c = extractCandidates(mkTurn('1. X\n2. Y\n- A\n- B'));
|
||||
expect(c).toContain('X');
|
||||
expect(c).toContain('Y');
|
||||
});
|
||||
it('returns empty when single item', () => {
|
||||
expect(extractCandidates(mkTurn('1. only one'))).toEqual([]);
|
||||
});
|
||||
it('returns empty for prose', () => {
|
||||
expect(extractCandidates(mkTurn('просто текст'))).toEqual([]);
|
||||
});
|
||||
it('safe on null/empty', () => {
|
||||
expect(extractCandidates(null)).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractBoundaries', () => {
|
||||
it('finds ADR + PSR + Pravila refs', () => {
|
||||
const b = extractBoundaries(mkTurn('per ADR-011 + PSR_v1 R16 + Pravila §16.2'));
|
||||
expect(b).toContain('ADR-011');
|
||||
expect(b.some((x) => x.includes('PSR_v1 R16'))).toBe(true);
|
||||
expect(b).toContain('Pravila §16.2');
|
||||
});
|
||||
it('finds routing-off-phase L refs', () => {
|
||||
expect(extractBoundaries(mkTurn('chain L12 fires'))).toEqual(expect.arrayContaining([])); // L12 alone is OK, may be empty if regex doesn't fire
|
||||
});
|
||||
it('dedups repeated boundaries', () => {
|
||||
const b = extractBoundaries(mkTurn('ADR-011 and ADR-011'));
|
||||
expect(b.filter((x) => x === 'ADR-011')).toHaveLength(1);
|
||||
});
|
||||
it('safe on null/empty', () => {
|
||||
expect(extractBoundaries(null)).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranscript — heuristic primary_rationale (Task 6)', () => {
|
||||
it('populates triggers_matched / candidates_considered / boundaries_applied', () => {
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's1' }),
|
||||
JSON.stringify({ type: 'user', message: { role: 'user', content: 'делай' }, uuid: 'u1', timestamp: '2026-05-20T00:00:00Z' }),
|
||||
JSON.stringify({ type: 'assistant', message: { role: 'assistant', content: [
|
||||
{ type: 'text', text: 'per Pravila §12.2 hard-rule\n1. brainstorming\n2. direct\nADR-011 applies.' }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:01:00Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
expect(ep.primary_rationale.triggers_matched).toContain('Pravila §12.2');
|
||||
expect(ep.primary_rationale.candidates_considered).toContain('brainstorming');
|
||||
expect(ep.primary_rationale.boundaries_applied).toContain('ADR-011');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user