diff --git a/tools/observer-routing-detector.mjs b/tools/observer-routing-detector.mjs index 33109a2a..53165430 100644 --- a/tools/observer-routing-detector.mjs +++ b/tools/observer-routing-detector.mjs @@ -35,11 +35,27 @@ export function loadKnownNodes(path = KNOWN_NODES_PATH) { return out; } +/** + * Снять ТОЛЬКО PASTED-контекст: fenced-блоки и markdown-blockquote-строки. + * НАМЕРЕННО НЕ трогает inline-backticks/кавычки/guillemets — в отличие от + * stripQuotedContext (tools/enforce-rationalization-audit.mjs), у которого + * ПРОТИВОПОЛОЖНОЕ безопасное направление (FN-safe). Здесь гейт routingGate + * FP-safe: inline-форма `/x` должна оставаться директивой (over-detection + * дешевле тихой потери governance-сигнала; sharp-edges SE-1). + */ +export function stripPastedContext(text) { + if (typeof text !== 'string') return ''; + let s = text; + s = s.replace(/```[\s\S]*?```/g, ' '); // fenced blocks + s = s.replace(/^[ \t]*(?:>|>).*$/gm, ' '); // markdown blockquote lines + return s; +} + /** * @returns {{directed: boolean, node: string|null}} */ export function detectMethodDirected(promptText, knownNodes) { - const text = String(promptText || '').toLowerCase(); + const text = stripPastedContext(String(promptText || '')).toLowerCase(); for (const node of knownNodes || []) { const n = String(node).toLowerCase(); if (!n) continue; diff --git a/tools/observer-routing-detector.test.mjs b/tools/observer-routing-detector.test.mjs index 86f9acb8..58aff8b3 100644 --- a/tools/observer-routing-detector.test.mjs +++ b/tools/observer-routing-detector.test.mjs @@ -48,3 +48,37 @@ describe('loadKnownNodes', () => { expect(loadKnownNodes('tools/does-not-exist.txt')).toEqual([]); }); }); + +import { stripPastedContext } from './observer-routing-detector.mjs'; + +describe('stripPastedContext (узкий: fenced + blockquote)', () => { + it('removes fenced blocks', () => { + expect(stripPastedContext('a\n```\n/x here\n```\nb')).not.toContain('/x'); + }); + it('removes blockquote lines', () => { + expect(stripPastedContext('> /x cited\nkeep')).not.toContain('/x'); + }); + it('keeps inline backticks (FP-safe bias)', () => { + expect(stripPastedContext('use `/x` now')).toContain('/x'); + }); + it('is non-string safe', () => { + expect(stripPastedContext(null)).toBe(''); + }); +}); + +describe('detectMethodDirected — PASTED-context guard (4-й FP-класс)', () => { + it('does NOT flag a /node inside a fenced code block', () => { + const t = 'смотри лог:\n```\nуже использовал /brainstorming тут\n```\nчто думаешь?'; + expect(detectMethodDirected(t, NODES).directed).toBe(false); + }); + it('does NOT flag a blockquote-cited directive', () => { + const t = '> используй systematic-debugging здесь\nэто цитата из старого чата'; + expect(detectMethodDirected(t, NODES).directed).toBe(false); + }); + it('STILL flags an inline /slash directive (FP-safe bias preserved)', () => { + expect(detectMethodDirected('сделай через `/brainstorming`', NODES).directed).toBe(true); + }); + it('STILL flags a plain-prose directive', () => { + expect(detectMethodDirected('запусти discovery-interview', NODES).directed).toBe(true); + }); +});