2026-05-29 20:50:48 +03:00
// 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).
2026-05-29 21:01:56 +03:00
// 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+).
2026-05-29 20:50:48 +03:00
// 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 = [
2026-05-29 21:01:56 +03:00
/(?<![\p{L}\p{N}_])я\s+проанализировал\s+(?:свои|собственные)\s+(?:паттерны|поведенческие|обходные)/iu ,
2026-05-29 20:50:48 +03:00
/\b(?:retrospect|self-evaluation|self-analysis|self-debrief)\b/i ,
2026-05-29 21:01:56 +03:00
/(?<![\p{L}\p{N}_])обобщ(?:аю|ил)\s+(?:опыт|выводы)/iu ,
/(?<![\p{L}\p{N}_])я\s+(?:заметил|обнаружил|увидел)\s+(?:паттерн|тенденцию|behavioral)/iu ,
2026-05-29 20:50:48 +03:00
/\bself-retro/i ,
2026-05-29 21:01:56 +03:00
/(?<![\p{L}\p{N}_])брэйн-ретро/iu ,
2026-05-29 20:50:48 +03:00
/\bgenerali[sz](?:able|ed)\s+lesson\b/i ,
2026-05-29 21:01:56 +03:00
/(?:\blesson|(?<![\p{L}\p{N}_])урок)\s+v?\d+\.\d+\s*[:—-]/iu ,
2026-05-29 20:50:48 +03:00
] ;
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.' ,
} ;
}