Files
brain/tools/self-debrief-detector.mjs
T

69 lines
3.1 KiB
JavaScript

// tools/self-debrief-detector.mjs
/**
* Self-debrief detector — router-gate v4.1 spec §3.12 (NEW). Закрывает F18.
* Pure: ловит retrospective/self-analysis content в response без recent
* self-retrospect / brain-retro Skill invocation.
*/
// NOTE: \b does not fire before Cyrillic characters in Node.js (ASCII word boundary).
// Cyrillic-leading patterns use Unicode lookbehind (?<![\p{L}\p{N}_]) to avoid
// mid-word false positives (e.g. судь*я*, семь*я*). All such patterns carry the
// `u` flag which is required for \p{…} to work (Node 20+).
// Pattern 7 uses generali[sz] to cover both US (generalizable) and British (generalisable)
// spellings — required by the plan's own Step 1 test ("Generalisable lesson").
export const SELF_DEBRIEF_PATTERNS = [
/(?<![\p{L}\p{N}_])я\s+проанализировал\s+(?:свои|собственные)\s+(?:паттерны|поведенческие|обходные)/iu,
/\b(?:retrospect|self-evaluation|self-analysis|self-debrief)\b/i,
/(?<![\p{L}\p{N}_])обобщ(?:аю|ил)\s+(?:опыт|выводы)/iu,
/(?<![\p{L}\p{N}_])я\s+(?:заметил|обнаружил|увидел)\s+(?:паттерн|тенденцию|behavioral)/iu,
/\bself-retro/i,
/(?<![\p{L}\p{N}_])брэйн-ретро/iu,
/\bgenerali[sz](?:able|ed)\s+lesson\b/i,
/(?:\blesson|(?<![\p{L}\p{N}_])урок)\s+v?\d+\.\d+\s*[:—-]/iu,
];
function normSkill(name) {
return String(name || '').trim().toLowerCase();
}
function skillMatchesAny(name, list) {
const n = normSkill(name);
return list.some((s) => {
const t = normSkill(s);
return n === t || n.endsWith(':' + t) || t.endsWith(':' + n);
});
}
export function extractSkillCallsLastNTurns(transcript, n) {
const recs = transcript || [];
let scoped = recs;
const withTurn = recs.filter((r) => typeof r.turn === 'number');
if (withTurn.length > 0) {
const maxTurn = Math.max(...withTurn.map((r) => r.turn));
scoped = recs.filter((r) => typeof r.turn !== 'number' || r.turn > maxTurn - n);
}
const out = [];
for (const rec of scoped) {
if (rec.type === 'tool_use' && rec.name === 'Skill') {
out.push({ skill_name: normSkill(rec.input?.skill ?? rec.input?.command ?? '') });
}
}
return out;
}
export function detectSelfDebrief(controllerResponseText, transcript, opts = {}) {
const { recentTurns = 30, selfRetroSkills = ['self-retrospect', 'brain-retro'] } = opts;
const text = String(controllerResponseText || '');
const matched = SELF_DEBRIEF_PATTERNS.some((re) => re.test(text));
if (!matched) return { action: 'allow' };
const recent = extractSkillCallsLastNTurns(transcript, recentTurns);
const selfRetroInvoked = recent.some((c) => skillMatchesAny(c.skill_name, selfRetroSkills));
if (selfRetroInvoked) return { action: 'allow' };
return {
action: 'hard_block_next_mutating',
reason: 'v4.1 self-debrief hard-block: response содержит retrospective/self-analysis content без recent self-retrospect или brain-retro Skill invocation. Invoke matching Skill для honest captured retrospect, не inline narrative analysis.',
};
}