diff --git a/tools/askuser-cosmetic-detector.mjs b/tools/askuser-cosmetic-detector.mjs index 1d2ca34..8371e8b 100644 --- a/tools/askuser-cosmetic-detector.mjs +++ b/tools/askuser-cosmetic-detector.mjs @@ -50,6 +50,18 @@ export function isGitApprovalQuestion(questions) { q.options.some((o) => o && typeof o.label === 'string' && GIT_CMD_RE.test(o.label))); } +// Escape-авторизация владельца — санкционированный канал floor_escape (владелец вставляет строку +// FLOOR-ESCAPE в ответ окошка), НЕ замена структурной идеации. По аналогии с git-approval +// (Calibration 5): такие окошки не косметика → не считаем и не блокируем. Иначе anti-cosmetic +// страж глушил легитимный поток разрешений после >2 окошек (баг найден живым прогоном 18.06). +const FLOOR_ESCAPE_RE = /FLOOR-ESCAPE:/i; + +/** True if this AskUser is an owner escape-authorization prompt (question text carries the FLOOR-ESCAPE marker). */ +export function isEscapeAuthQuestion(questions) { + if (!Array.isArray(questions)) return false; + return questions.some((q) => q && typeof q.question === 'string' && FLOOR_ESCAPE_RE.test(q.question)); +} + /** * Pure cosmetic-AskUser decision (v4.1 §4.5). * Caller passes PRIOR counts; decide computes prospective new counts. @@ -62,7 +74,7 @@ export function decide({ questions, simpleCountSession = 0, simpleCountTurn = 0, // git-approval channel, never cosmetic ideation. Allow, do not count, never // block. (Cannot be abused to dodge ideation discipline: a git-command label // makes the answer a real approve_git_operation, not a cosmetic clarification.) - if (isGitApprovalQuestion(questions)) { + if (isGitApprovalQuestion(questions) || isEscapeAuthQuestion(questions)) { return { action: 'allow', block: false, reason: null, isSimpleAB: false, newSessionCount: simpleCountSession, newTurnCount: simpleCountTurn }; } const simple = isSimpleAB(questions); diff --git a/tools/askuser-cosmetic-escape-exempt.test.mjs b/tools/askuser-cosmetic-escape-exempt.test.mjs new file mode 100644 index 0000000..161cd72 --- /dev/null +++ b/tools/askuser-cosmetic-escape-exempt.test.mjs @@ -0,0 +1,24 @@ +import { describe, it, expect } from 'vitest'; +import { isEscapeAuthQuestion, decide } from './askuser-cosmetic-detector.mjs'; + +const escQ = [{ question: 'Разрешение: FLOOR-ESCAPE: write:c:/x.md — авторизуешь?', options: [{ label: 'Отмена' }, { label: 'Не получается' }] }]; +const plainQ = [{ question: 'Какой вариант?', options: [{ label: 'A' }, { label: 'B' }] }]; + +describe('escape-окошки освобождены от cosmetic-счётчика (FLOOR-ESCAPE в вопросе)', () => { + it('isEscapeAuthQuestion: вопрос с FLOOR-ESCAPE → true', () => { + expect(isEscapeAuthQuestion(escQ)).toBe(true); + }); + it('isEscapeAuthQuestion: обычный вопрос → false; не массив → false', () => { + expect(isEscapeAuthQuestion(plainQ)).toBe(false); + expect(isEscapeAuthQuestion(null)).toBe(false); + }); + it('decide: escape-окошко при >2 простых за сессию → allow (не hard_block)', () => { + const r = decide({ questions: escQ, simpleCountSession: 3, brainstormingInvoked: false }); + expect(r.action).toBe('allow'); + expect(r.block).toBe(false); + }); + it('decide: обычное простое окошко при >2 → hard_block (без регрессии)', () => { + const r = decide({ questions: plainQ, simpleCountSession: 3, brainstormingInvoked: false }); + expect(r.action).toBe('hard_block'); + }); +});