fix(router-gate): stream E — punctuation-aware stop detection + review nits (BOM/JSDoc/??)

This commit is contained in:
Дмитрий
2026-05-29 19:45:57 +03:00
parent 0ff1271fab
commit 490ac1ac0e
2 changed files with 24 additions and 15 deletions
+8 -4
View File
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env node
/**
* AskUserQuestion answer parsing library (router-gate v4, Stream E).
*
@@ -53,14 +53,17 @@ const STOP_TOKENS = new Set(STOP_KEYWORDS.filter((k) => !k.includes(' ')));
/**
* True if a free-form answer is a stop/abort/cancel intent (S27).
* Keyword-based; normalizes (E33 invisible strip + ws-collapse + lowercase) first.
* Punctuation attached to tokens (e.g. "нет,") is stripped before matching.
*/
export function isStopAnswer(text) {
const norm = normalizeAnswer(text);
if (!norm) return false;
const depunct = (s) => s.replace(/[.,;:!?…«»"'()\[\]{}]+/g, ' ').split(/\s+/).filter(Boolean).join(' ');
const cleaned = depunct(norm);
for (const phrase of STOP_PHRASES) {
if (norm.includes(normalizeAnswer(phrase))) return true;
if (cleaned.includes(depunct(normalizeAnswer(phrase)))) return true;
}
const tokens = norm.split(' ');
const tokens = cleaned.split(' ');
for (const t of tokens) {
if (STOP_TOKENS.has(t)) return true;
}
@@ -72,6 +75,7 @@ export function isStopAnswer(text) {
* @param {string} text
* @param {{llmJudge?: (text:string)=>Promise<boolean>}} opts
* llmJudge default-stub returns false (never escalates). Stream D wires real judge.
* The injected llmJudge receives whitespace-collapsed lowercase text (post-normalizeAnswer), not the raw input.
* @returns {Promise<boolean>}
*/
export async function detectStopWithFallback(text, { llmJudge } = {}) {
@@ -149,7 +153,7 @@ export function detectOtherSocialEng(controllerText) {
*/
export function buildApprovalRecord({ kind, pattern, sessionId, nowMs }) {
return {
kind: String(kind || 'approve_generic'),
kind: String(kind ?? 'approve_generic'),
approved_action_pattern: normalizeCommand(pattern),
session_id: sessionId || 'unknown',
approved_at_ms: typeof nowMs === 'number' ? nowMs : Date.now(),
+16 -11
View File
@@ -3,6 +3,13 @@ import {
stripInvisible,
normalizeAnswer,
normalizeCommand,
STOP_KEYWORDS,
isStopAnswer,
detectStopWithFallback,
parseAskUserResult,
matchesApproval,
detectOtherSocialEng,
buildApprovalRecord,
} from './askuser-answer-parser.mjs';
describe('askuser-answer-parser / stripInvisible (E33)', () => {
@@ -49,11 +56,6 @@ describe('askuser-answer-parser / normalizeCommand (E34)', () => {
});
});
import {
STOP_KEYWORDS,
isStopAnswer,
detectStopWithFallback,
} from './askuser-answer-parser.mjs';
describe('askuser-answer-parser / STOP_KEYWORDS (S27)', () => {
it('includes core Russian + English stop tokens', () => {
@@ -98,6 +100,15 @@ describe('askuser-answer-parser / isStopAnswer', () => {
it('returns false for non-string', () => {
expect(isStopAnswer(null)).toBe(false);
});
it('matches a stop token with a trailing comma', () => {
expect(isStopAnswer('нет, это лишнее')).toBe(true);
expect(isStopAnswer('стоп.')).toBe(true);
});
it('still matches multi-word phrase without the comma', () => {
expect(isStopAnswer('всё поехали назад')).toBe(true);
});
});
describe('askuser-answer-parser / detectStopWithFallback', () => {
@@ -127,12 +138,6 @@ describe('askuser-answer-parser / detectStopWithFallback', () => {
});
});
import {
parseAskUserResult,
matchesApproval,
detectOtherSocialEng,
buildApprovalRecord,
} from './askuser-answer-parser.mjs';
describe('askuser-answer-parser / parseAskUserResult', () => {
it('extracts a single selected answer label', () => {