#!/usr/bin/env node /** * Routing-gate method-direction detector (brain governance, observer * factor-analysis spec §5.1). Pure — given a user-prompt text and a list of * known node names, decides whether the user *dictated* a specific method. * Conservative-broad: a directive verb within a 40-char window before a node * name, or a /slash-command form. * * Security Guidance #40: pure string ops — no exec/execSync. */ import { readFileSync, existsSync } from 'fs'; import { resolve, dirname } from 'path'; import { fileURLToPath } from 'url'; /** Repo root = parent of the tools/ directory where this module lives. */ const REPO_ROOT = dirname(dirname(fileURLToPath(import.meta.url))); const KNOWN_NODES_PATH = 'tools/observer-known-nodes.txt'; const DIRECTIVE_VERBS = [ 'запусти', 'запускай', 'используй', 'вызови', 'вызывай', 'прогони', 'применяй', 'применить', 'через', 'run', 'use', 'invoke', 'via', ]; /** Load the directable node names from the data file (# comments / blanks skipped). */ export function loadKnownNodes(path = KNOWN_NODES_PATH) { const absPath = resolve(REPO_ROOT, path); if (!existsSync(absPath)) return []; const out = []; for (const line of readFileSync(absPath, 'utf-8').split('\n')) { const t = line.trim(); if (!t || t.startsWith('#')) continue; out.push(t); } 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 = stripPastedContext(String(promptText || '')).toLowerCase(); for (const node of knownNodes || []) { const n = String(node).toLowerCase(); if (!n) continue; if (text.includes('/' + n)) return { directed: true, node }; const idx = text.indexOf(n); if (idx === -1) continue; const before = text.slice(Math.max(0, idx - 40), idx); if (DIRECTIVE_VERBS.some((v) => before.includes(v))) return { directed: true, node }; } return { directed: false, node: null }; } if (process.argv[1] && process.argv[1].replace(/\\/g, '/').endsWith('/observer-routing-detector.mjs')) { const det = detectMethodDirected(process.argv.slice(2).join(' '), loadKnownNodes()); console.log(JSON.stringify(det)); process.exit(0); }