From f2365caf4bad7d527af7b0dcd8f904ea50f0453d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9?= Date: Thu, 18 Jun 2026 23:20:01 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20escape-=D0=BE=D0=BA=D0=BE=D1=88=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=BC=D0=B8=D0=BC=D0=BE=20anti-cosmetic-=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B0=D0=B6=D0=B0=20(escape-flow=20unblock)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anti-cosmetic-детектор (>2 простых AskUser за сессию → hard-block, требует brainstorming) глушил ЛЕГИТИМНЫЙ поток escape-окошек: владелец даёт разрешение FLOOR-ESCAPE через AskUser, и после >2 таких окошек стена их блокировала — нормативку под стеной нельзя было довести (баг найден живым прогоном 18.06). Фикс по аналогии с git-approval exemption (Calibration 5): isEscapeAuthQuestion (вопрос несёт метку FLOOR-ESCAPE) освобождается в decide() — не косметика, не считаем, не блокируем. Не абьюзится: метка сигналит авторизацию владельца, а не подмену идеации. Свод 4383 зелёный. Co-Authored-By: Claude Opus 4.8 --- tools/askuser-cosmetic-detector.mjs | 14 ++++++++++- tools/askuser-cosmetic-escape-exempt.test.mjs | 24 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 tools/askuser-cosmetic-escape-exempt.test.mjs 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'); + }); +});