feat(observer): opt-in reasoning-tag merges with heuristic primary_rationale
Closes brain-retro 2026-05-20 #11 — parseReasoningTag extracts opt-in <!-- reasoning: triggers="..." candidates="..." boundaries="..." --> HTML-comment from assistant text. Semicolon-separated values merged into heuristic-derived primary_rationale arrays via Set-dedupe. Conservative: tag is opt-in; heuristic still runs even when tag present (heuristic provides baseline, tag enriches). 5 new vitest tests, 309/309 GREEN. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -532,6 +532,31 @@ export function parseRoutingTag(turn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const REASONING_TAG_RE =
|
||||
/<!--\s*reasoning:\s*triggers="([^"]*)"\s+candidates="([^"]*)"\s+boundaries="([^"]*)"\s*-->/;
|
||||
|
||||
/**
|
||||
* Opt-in reasoning tag (Task 11). Claude may emit at most one such comment
|
||||
* per turn to declare triggers / candidates / boundaries explicitly. Values
|
||||
* are semicolon-separated. When present, parser merges them into the
|
||||
* heuristic-derived arrays via Set-dedupe.
|
||||
*/
|
||||
export function parseReasoningTag(turn) {
|
||||
for (const e of turn || []) {
|
||||
const content = e && e.message && Array.isArray(e.message.content) ? e.message.content : [];
|
||||
for (const b of content) {
|
||||
if (b && b.type === 'text' && typeof b.text === 'string') {
|
||||
const m = b.text.match(REASONING_TAG_RE);
|
||||
if (m) {
|
||||
const split = (s) => s.split(';').map((x) => x.trim()).filter(Boolean);
|
||||
return { triggers: split(m[1]), candidates: split(m[2]), boundaries: split(m[3]) };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Text of the last real user prompt — used by the Stop-hook routing-gate (Task 5). */
|
||||
export function extractLastUserPromptText(transcriptText) {
|
||||
const { entries } = parseLines(transcriptText);
|
||||
@@ -615,17 +640,21 @@ export function parseTranscript(transcriptText, fallbackSessionId = null) {
|
||||
environment: extractEnvironment(entries, start),
|
||||
task_size: extractTaskSize(turn),
|
||||
task_cost: extractTokenUsage(turn),
|
||||
primary_rationale: {
|
||||
step: 1,
|
||||
node_chosen: skills.length > 0 ? skills[0] : 'direct',
|
||||
triggers_matched: extractTriggers(turn),
|
||||
candidates_considered: extractCandidates(turn),
|
||||
boundaries_applied: extractBoundaries(turn),
|
||||
hard_floor: usedSuperpowers
|
||||
? { invoked: true, rules: ['Pravila §12'] }
|
||||
: { invoked: false, rules: [] },
|
||||
task_classification: classifyTask(prompt),
|
||||
},
|
||||
primary_rationale: (() => {
|
||||
const tag = parseReasoningTag(turn);
|
||||
const merge = (heur, fromTag) => [...new Set([...heur, ...fromTag])];
|
||||
return {
|
||||
step: 1,
|
||||
node_chosen: skills.length > 0 ? skills[0] : 'direct',
|
||||
triggers_matched: merge(extractTriggers(turn), tag ? tag.triggers : []),
|
||||
candidates_considered: merge(extractCandidates(turn), tag ? tag.candidates : []),
|
||||
boundaries_applied: merge(extractBoundaries(turn), tag ? tag.boundaries : []),
|
||||
hard_floor: usedSuperpowers
|
||||
? { invoked: true, rules: ['Pravila §12'] }
|
||||
: { invoked: false, rules: [] },
|
||||
task_classification: classifyTask(prompt),
|
||||
};
|
||||
})(),
|
||||
events,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1327,3 +1327,51 @@ describe('classifyPromptSignal — extended dictionary (Task 9)', () => {
|
||||
expect(classifyPromptSignal('переделай это')).toBe('correction');
|
||||
});
|
||||
});
|
||||
|
||||
import { parseReasoningTag } from './observer-transcript-parser.mjs';
|
||||
|
||||
describe('parseReasoningTag (Task 11)', () => {
|
||||
it('parses opt-in reasoning tag from assistant text', () => {
|
||||
const turn = [{ message: { role: 'assistant', content: [
|
||||
{ type: 'text', text: '<!-- reasoning: triggers="Pravila §12.2;ADR-011" candidates="brain-retro;brainstorming" boundaries="Pravila §16" -->\nAnswer.' }
|
||||
] } }];
|
||||
const tag = parseReasoningTag(turn);
|
||||
expect(tag).toEqual({
|
||||
triggers: ['Pravila §12.2', 'ADR-011'],
|
||||
candidates: ['brain-retro', 'brainstorming'],
|
||||
boundaries: ['Pravila §16'],
|
||||
});
|
||||
});
|
||||
it('returns null when no tag present', () => {
|
||||
expect(parseReasoningTag([{ message: { role: 'assistant', content: [{ type: 'text', text: 'plain' }] } }])).toBeNull();
|
||||
});
|
||||
it('safe on null/empty turn', () => {
|
||||
expect(parseReasoningTag(null)).toBeNull();
|
||||
expect(parseReasoningTag([])).toBeNull();
|
||||
});
|
||||
it('skips non-text blocks', () => {
|
||||
const turn = [{ message: { role: 'assistant', content: [{ type: 'tool_use', name: 'Read' }] } }];
|
||||
expect(parseReasoningTag(turn)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTranscript — reasoning-tag merges with heuristic (Task 11)', () => {
|
||||
it('merges tag triggers into heuristic triggers (deduped)', () => {
|
||||
const transcript = [
|
||||
JSON.stringify({ sessionId: 's' }),
|
||||
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: 'Pravila §12.2 hard-rule\n<!-- reasoning: triggers="Pravila §12.2;ADR-011" candidates="brain-retro" boundaries="Pravila §16" -->' }
|
||||
] }, uuid: 'u2', timestamp: '2026-05-20T00:01:00Z' }),
|
||||
].join('\n');
|
||||
const ep = parseTranscript(transcript);
|
||||
// heuristic finds 'Pravila §12.2' from text + tag adds 'ADR-011'; dedup
|
||||
expect(ep.primary_rationale.triggers_matched).toContain('Pravila §12.2');
|
||||
expect(ep.primary_rationale.triggers_matched).toContain('ADR-011');
|
||||
expect(ep.primary_rationale.triggers_matched.filter((t) => t === 'Pravila §12.2')).toHaveLength(1);
|
||||
// candidates: tag contributes 'brain-retro' (no numbered list in text)
|
||||
expect(ep.primary_rationale.candidates_considered).toContain('brain-retro');
|
||||
// boundaries: heuristic empty (no marker in plain text), tag adds 'Pravila §16'
|
||||
expect(ep.primary_rationale.boundaries_applied).toContain('Pravila §16');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user