From 4e1aef53c4e119526b659dbce631d5fc404462d0 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: Mon, 8 Jun 2026 19:40:34 +0300 Subject: [PATCH] =?UTF-8?q?docs(router-mentor):=20E/G=20spec=20=E2=80=94?= =?UTF-8?q?=204=20=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=D0=BE=D0=BF=D0=B0=D1=81=D0=BD=D0=BE=D1=81=D1=82=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=20adversarial-=D0=B0=D0=BD=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Цепочка audit-context-building/sharp-edges/variant-analysis. P1 узкий stripPastedContext (fenced+blockquote only, не reuse stripQuotedContext — сохранить FP-смещение гейта). P2 counting-replacers по конвейеру sanitize (count==редакции). P4 escapeCell anti-injection в STATUS.md. P5 пиннинг документируемого инварианта. Жёсткие стены М2/М5/М4/М6 не затронуты. --- ...08-router-mentor-eg-warmup-batch-design.md | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-06-08-router-mentor-eg-warmup-batch-design.md b/docs/superpowers/specs/2026-06-08-router-mentor-eg-warmup-batch-design.md index ee11f600..2dd0d694 100644 --- a/docs/superpowers/specs/2026-06-08-router-mentor-eg-warmup-batch-design.md +++ b/docs/superpowers/specs/2026-06-08-router-mentor-eg-warmup-batch-design.md @@ -2,6 +2,7 @@ **Дата:** 2026-06-08 · **Ветка:** `worktree-brainrepo` · **Кодовая фраза эпика:** «роутер-наставник». **Статус:** УТВЕРЖДЁН владельцем (2026-06-08). Источник — реестр хвостов `2026-06-08-router-mentor-loose-ends-registry.md`, блоки E (Doc-gaps/косметика) + G (периферия мозга). +**Анализ-фаза (adversarial, до плана):** прогнан цепочкой `audit-context-building → sharp-edges → variant-analysis` на «не пробьёт ли дыр в М1–М7». Вывод: жёсткие стены (М2/М5/М4/М6) не затронуты; единственный затронутый гейт — observer routing-gate (Машина 0). Внесены 4 правки безопасности: P1 узкий `stripPastedContext` (SE-1, сохранить FP-смещение), P2 counting-replacers по конвейеру sanitize (SE-2, `count==редакции`), P4 `escapeCell` (SE-3, anti-injection в STATUS.md), P5 пиннинг документируемого инварианта (SE-4). ## Цель @@ -17,27 +18,33 @@ ### 1. G — 4-й FP-класс routing-gate (`tools/observer-routing-detector.mjs`) -**Дефект:** `detectMethodDirected` ([:46](../../../tools/observer-routing-detector.mjs)) флагует `/` где угодно в тексте, включая процитированный/вставленный фрагмент (`` `/brain-retro` `` в backticks, «…используй /node…» в кавычках, строки-цитаты `> …`). Цитата → ложный `directed:true`. Зеркало известного урока `stripQuotedContext`. +**Дефект:** `detectMethodDirected` ([:46](../../../tools/observer-routing-detector.mjs)) флагует `/` где угодно в тексте, включая вставленный фрагмент (PASTED-транскрипт/документ). Цитата → ложный `directed:true`. Этот символ кормит **живой** Stop-gate `routingGateDecision` ([observer-stop-hook.mjs:350](../../../tools/observer-stop-hook.mjs)) — не только телеметрию. -**Подход:** добавить чистую предобработку `stripQuotedContext(text)` — снять inline-code (одиночные/тройные backticks), guillemets `«…»`, двойные кавычки, markdown-blockquote-строки (начинающиеся с `>`) — **до** `/node`-скана и directive-verb-окна. Применяется и к `/`-форме, и к verb-window-форме. +**Граница направления отказа (audit-context + sharp-edges SE-1):** текущий дизайн «conservative-broad» намеренно ошибается в сторону FP (`directed:true` дёшево — контроллер просто добавляет routing-тег). Под-детекция (FN) = тихая потеря governance-сигнала. Поэтому strip обязан быть **узким**, сохраняя FP-смещение для inline-форм. + +**Variant-факт:** существующий `stripQuotedContext` ([enforce-rationalization-audit.mjs:51-59](../../../tools/enforce-rationalization-audit.mjs)) стрипает fenced **+ inline-backticks + кавычки**, но **НЕ** blockquote `>`. Его **нельзя reuse** для гейта: он снял бы inline-директиву `использу­й \`/x\`` → FN. При этом реальный FP (pasted-транскрипт) обычно — `>`-цитата, которую существующая функция не снимает. + +**Подход:** новая отдельная чистая функция `stripPastedContext(text)` в `observer-routing-detector.mjs` — снимает **только** (а) fenced-блоки ` ```…``` ` и (б) markdown-blockquote-строки (начинающиеся с `>`/`>`). **НЕ** трогает inline-backticks, кавычки, guillemets. Применяется к тексту **до** `/node`-скана и directive-verb-окна (обе формы). Docstring явно фиксирует расхождение с `stripQuotedContext` (противоположное безопасное направление: тот FN-safe, этот FP-safe). **Приёмка:** -- `` `/brain-retro` `` (в backticks) → `directed:false`. -- «он написал: "запусти brain-retro"» (в кавычках/цитате) → `directed:false`. +- `/brain-retro` внутри fenced-блока → `directed:false`. +- строка-цитата `> используй brain-retro` → `directed:false`. +- inline `сделай через \`/brainstorming\`` (НЕ в fence/цитате) → `directed:true` (сохраняем FP-смещение — это НЕ FP-баг, это safe-direction). - голый `/brain-retro` в начале промпта → `directed:true` (без регрессии). -- «запусти brain-retro» (не в кавычках) → `directed:true` (без регрессии). +- «запусти brain-retro» (обычная проза) → `directed:true` (без регрессии). - существующие тесты `observer-routing-detector.test.mjs` — зелёные. ### 2. G — PII double-count перекрытий (`tools/observer-pii-filter.mjs`) **Дефект:** `countString` ([:66-73](../../../tools/observer-pii-filter.mjs)) прогоняет КАЖДЫЙ паттерн независимо по сырой строке. Токен, матчащий два паттерна (например `sk-…` внутри `Bearer sk-…`), считается дважды → телеметрия `.pii-counters.json` завышена. Сам `sanitizeString` корректен (последовательное потребление через `.replace`). -**Подход:** считать по тому же последовательному потреблению, что и `sanitizeString` — заменять каждый паттерн на sentinel в том же порядке (специфичные первыми) и считать число замен на каждом шаге. Перекрывающийся спан засчитывается один раз — самым специфичным паттерном (первым по порядку). +**Подход (sharp-edges SE-2 — count == реально заредактированное):** считать инструментированием **того же** replace-конвейера, что и `sanitizeString` (тот же порядок регексов, специфичные первыми), а **НЕ** параллельным матчером по сырой строке. Каждый `.replace(re, …)` получает counting-replacer-функцию, инкрементящую счётчик паттерна на фактическую замену; следующий паттерн работает уже по строке после предыдущей замены (перекрытие потреблено первым/самым специфичным). По конструкции `count` равен числу реальных редакций sanitize — под-счёт реальной утечки невозможен (под-счёт хуже пере-счёта для security-телеметрии). `sanitize()` / `sanitizeString()` **не трогаем** — только `countString` переписывается на общий конвейер (вынести общий шаг replace+count, чтобы санитайз и счёт не разъехались). **Приёмка:** -- `Bearer sk-AAAAAAAAAAAAAAAAAAAA` → суммарный счёт по этому спану = 1 (по первому матчнувшему паттерну в порядке), а не 2. -- строка с одиночными, непересекающимися токенами → счётчики без изменений. -- `sanitizeWithCount(...).sanitized` остаётся byte-identical прежнему (санитайз не трогаем). +- `Bearer sk-AAAAAAAAAAAAAAAAAAAA` → суммарный счёт по этому спану = 1 (первый матчнувший паттерн в порядке конвейера), а не 2. +- две РАЗНЫЕ непересекающиеся утечки рядом (`a@b.co +71234567890`) → счёт 2 (не «съесть» вторую). +- строка с одиночными непересекающимися токенами → счётчики без изменений. +- `sanitizeWithCount(...).sanitized` остаётся byte-identical прежнему (санитайз не тронут). - существующие тесты `observer-pii-filter.test.mjs` — зелёные. ### 3. G — release-class `\bcommit\b` (`tools/observer-transcript-parser.mjs:215`) @@ -58,8 +65,11 @@ **Подход:** при непустых `recentEscapes`/`recentBlocks` рендерить компактную таблицу (время · действие · причина) под строкой-счётчиком. Пустые массивы → текущий вывод byte-identical. Чистая функция; `main()` продолжает передавать `[]` (live-источник — B-блок, граница соблюдена). +**Guard (sharp-edges SE-3 — injection в STATUS.md):** поля событий идут в markdown-таблицу. Поле с `|`/переводом строки сломало бы таблицу или **подделало строки обороны** (ложное «✅ зарегистрирован» → ложное чувство защиты у владельца). Готового escape-helper'а в `status-md-generator.mjs` нет (variant-факт). Добавить маленький `escapeCell(v)`: `String(v)` → заменить `|`→`\|`, `\r?\n`→` `, cap длины (например 120 символов с `…`). Применять к каждому полю detail-таблицы. Делаем safe-by-construction **сейчас**, до того как B-блок подведёт live-источник. + **Приёмка:** - given sample-массив escape/block-событий → деталь-таблица отрендерена (колонки время/действие/причина). +- поле `reason` с `|` и `\n` → в выводе экранировано (`\|`, без переноса), таблица цела, лишних строк нет. - `recentEscapes=[]`, `recentBlocks=[]` → вывод byte-identical текущему (счётчики-строка). - существующие тесты `status-md-generator.test.mjs` — зелёные. @@ -67,11 +77,11 @@ **Дефект:** node-graph docstring «Потребляет registry от loadRegistry» неточен (функция берёт plain-registry-объект, не обязательно «от loadRegistry»); reviewer prompt-caching `cache_control` — намеренный no-op на Opus (<4096-токенный префикс), это не задокументировано в коде. -**Подход:** уточнить формулировки. Чтобы честно пройти TDD-gate на comment-only правке `.mjs` — к каждой косметике приколотить 1 пиннинг-ассерт (RED→GREEN): node-graph — `buildNodeGraph` корректно строит граф из plain-объекта `{nodes, chains}` (документирует развязку); reviewer — structured-prompt возвращает непустой `system`-блок (документирует, что caching применяется к нему). Если TDD-gate не срабатывает на comment-only edit — править напрямую без пиннинга. +**Подход:** уточнить формулировки. Чтобы честно пройти TDD-gate на comment-only правке `.mjs` — к каждой косметике приколотить 1 пиннинг-ассерт (RED→GREEN), который пинит **именно документируемый инвариант, не трюизм** (sharp-edges SE-4): node-graph — `buildNodeGraph` строит корректный граф из plain-литерала `{nodes, chains}` (документирует развязку от `loadRegistry`); reviewer — structured-prompt возвращает объект с непустым `system`-блоком (документирует, к чему применяется caching). **Variant-факт:** существующие тесты уже зовут `buildNodeGraph(REG)` с plain-объектом — поэтому новый тест именуется по claim'у («accepts plain registry literal, not loadRegistry-bound»), а не дублирует покрытие случайно. Если TDD-gate не срабатывает на comment-only edit — править напрямую без пиннинга. **Приёмка:** - docstring/комментарий уточнён в обоих файлах. -- пиннинг-ассерты GREEN (или, при отсутствии срабатывания gate, чистая comment-правка). +- пиннинг-ассерты GREEN и пинят документируемый инвариант (не `expect(true).toBe(true)`-трюизм); либо, при отсутствии срабатывания gate, чистая comment-правка. ### 6. E — DOC-1 escape-awareness note (M7 design spec + loose-ends registry)