diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb1541..611ffea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,27 @@ Hardening-проход перед релизом `1.0.0`. Основная те первый же, кто убрал бы маскировку ошибок. Введён `code_has`, читающий код в переменную отдельным оператором. +- **Три отрицательных скана падали на прозе, а не на коде.** Комментарий, + объясняющий, почему чего-то больше нет, называет это по имени — и скан по + голой подстроке не отличает такой комментарий от кода: + + ```text + скан иконок -> блочный комментарий о замене плагина + скан имён раннеров -> слово `systemd-run` в прозе + скан cancelFirewall… -> комментарий о разделении функции + ``` + + Все три исправлены по форме, а не удалением комментариев: скан раннеров + требует, чтобы перед именем не стоял дефис (в JavaScript идентификатор после + дефиса не начинается, поэтому ни один настоящий вызов не пропускается); скан + `cancelFirewallRollback` ищет имя со скобкой, то есть объявление или вызов; + литеральный скан по `virtual:svg-icons-register` удалён, потому что его роль + исполняет более сильный и более ранний гейт — production `vite build`, где + неразрешимый виртуальный модуль роняет сборку bundle. + + Из них скан `cancelFirewallRollback` был сломан с момента появления и не + падал только потому, что сборка до него не доходила. + - **Несуществующий путь в скане означал успех.** `2>/dev/null || true` в `code_without_comments` превращал опечатку в имени файла в пустой вывод, а пустой вывод для проверки «запрещённой конструкции здесь нет» — это PASS. diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index b98e948..cce872b 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -1448,6 +1448,31 @@ hy2xs-orchestrator doctor 74. обещанное окно отката — контракт systemd, а не намерение: у транзиентного таймера явно задан `AccuracySec=1s`, иначе умолчание `AccuracySec=1min` превращало «45 секунд» в 45–105 75. состояние guard читает один наблюдатель: `status` берёт его у того же кода, что и барьер, и сообщает `unknown` вместо тихого «guard'ов нет» при отказе systemd 76. ни один релизный гейт не подаёт вывод в поиск с флагом `-q` через пайплайн: под `set -o pipefail` оборванный продюсер отдаёт 141, и «совпадение найдено» превращается в ненулевой код — для отрицательных проверок это ложный PASS. Сравнение идёт через here-string, и возврат пайплайна запрещён отдельной приёмкой +77. отрицательные сканы по дереву исходников формулируют **синтаксическую форму**, а не подстроку: вызов — имя со скобкой или обратной кавычкой, импорт — `import` со спецификатором, зависимость — ключ в `package.json`. Прозаическое упоминание удалённой вещи разрешено, иначе гейт запрещает документировать собственную работу + +### Почему отрицательный скан не ищет подстроку + +Комментарий, объясняющий, почему чего-то больше нет, обязан называть это по +имени. Скан по голой подстроке такой комментарий не отличает от кода и падает +ровно на документации к выполненной им же работе. В этом файле урок оплачен +пять раз: скан versions contract ловил сам себя на `/hui`; dead-route скан +падал на `router_test.go`, который перечисляет удалённые маршруты, чтобы +доказать их отсутствие; скан иконок — на блочном комментарии о замене плагина; +скан прежних имён раннеров — на слове `systemd-run` в прозе; скан +`cancelFirewallRollback` — на комментарии о её разделении. + +`code_has` отбрасывает **строчные** комментарии (`//`, `#`), но не блочные +`/* … */`. Блок-парсер сознательно не заводится: наивный стриппер спотыкается +о `/*` внутри строк и регулярных выражений и может вычистить настоящий код — +а это ложный PASS, то есть лекарство хуже болезни. Для файлов с блочными +комментариями формулируется синтаксическая форма либо утверждение опирается на +более сильный гейт. + +Пример последнего: литерального скана по `virtual:svg-icons-register` больше +нет. Его роль исполняет production `vite build`, который проходит раньше: +активный `import "virtual:svg-icons-register"` при отсутствующем плагине не +разрешается резолвером, и сборка bundle падает. Проверяется исполняемый импорт, +а не совпадение подстроки. ### Почему пайплайн в `grep -q` запрещён diff --git a/tools/build/lib/acceptance.sh b/tools/build/lib/acceptance.sh index a39ba26..a3f3306 100644 --- a/tools/build/lib/acceptance.sh +++ b/tools/build/lib/acceptance.sh @@ -8,10 +8,29 @@ set -euo pipefail # — и проверка, ищущая просто подстроку, начинает падать ровно на той # документации, которая описывает выполненную ею же работу. # -# Урок в этом файле уже дважды оплачен: скан versions contract ловил сам себя на -# строке '/hui', а dead-route скан падал на router_test.go, который ПЕРЕЧИСЛЯЕТ -# удалённые маршруты, чтобы доказать их отсутствие. Помощник существует, чтобы -# третьего раза не было. +# Урок в этом файле оплачен уже пять раз: скан versions contract ловил сам себя +# на строке '/hui'; dead-route скан падал на router_test.go, который ПЕРЕЧИСЛЯЕТ +# удалённые маршруты, чтобы доказать их отсутствие; скан иконок падал на +# блочном комментарии, объясняющем замену плагина; скан прежних имён раннеров — +# на слове `systemd-run` в прозе; скан `cancelFirewallRollback` — на +# комментарии, описывающем её разделение. +# +# Отсюда правило для КАЖДОЙ отрицательной проверки по дереву исходников: +# +# формулируй синтаксическую форму, а не подстроку. +# +# Вызов — это имя со скобкой или с обратной кавычкой; импорт — это `import` со +# спецификатором; зависимость — это ключ в package.json. Прозаическое +# упоминание удалённой вещи обязано быть разрешено: комментарий, объясняющий, +# почему чего-то больше нет, называет это по имени — иначе он бесполезен. +# +# Ограничение помощника, о котором важно помнить: он отбрасывает СТРОЧНЫЕ +# комментарии (`//`, `#`), но НЕ блочные `/* ... */`. Блок-парсер здесь +# сознательно не заводится: наивный стриппер спотыкается о `/*` внутри строк и +# регулярных выражений и может вычистить настоящий код, а это ложный PASS — +# отказ хуже болезни. Для файлов с блочными комментариями формулируйте +# синтаксическую форму либо опирайтесь на более сильный гейт (сборка bundle, +# компилятор, тесты). # # Вид комментария выбирается по расширению файла. # @@ -260,10 +279,21 @@ run_fix20_acceptance_subset() { # `Patched versions: <0.0.0` — исправленной версии не существует. ! grep -q '"vite-plugin-svg-icons"' apps/frontend/package.json \ || fail "acceptance: неподдерживаемый vite-plugin-svg-icons вернулся в зависимости" - ! grep -rq 'virtual:svg-icons-register' apps/frontend/src \ - || fail "acceptance: виртуальный модуль удалённого плагина иконок вернулся" grep -q 'registerSvgIcons' apps/frontend/src/main.ts \ || fail "acceptance: спрайт локальных иконок не регистрируется при старте приложения" + # Литерального скана по `virtual:svg-icons-register` здесь СОЗНАТЕЛЬНО нет. + # + # Он ловил не код, а прозу: единственное вхождение строки в дереве — в + # блочном комментарии `components/SvgIcon/sprite.ts`, который объясняет, что + # именно заменено. Комментарий, называющий удалённую вещь по имени, обязан + # существовать — иначе он бесполезен, — и гейт, падающий на нём, запрещает + # документировать собственную работу. + # + # Утверждение при этом не потеряно, а обеспечено более сильной проверкой: + # production `vite build` выше по сборке уже прошёл. Активный + # `import "virtual:svg-icons-register"` при отсутствующем плагине не + # разрешается резолвером, и сборка bundle падает — то есть проверяется + # исполняемый импорт, а не совпадение подстроки. # Каждая иконка обязана давать symbol с viewBox. # @@ -410,7 +440,15 @@ run_clean_install_acceptance() { || fail "acceptance: process.ts must expose an explicit read-only runner" grep -q 'export async function runMutating' orchestrator/src/lib/process.ts \ || fail "acceptance: process.ts must expose an explicit mutating runner" - ! grep -rEq '(^|[^A-Za-z0-9_])(run|runVisible|runHidden|runSecret|runRawVisible)`' orchestrator/src \ + # Дефис в списке исключённых предшествующих символов — не косметика. + # + # Скан ищет ВЫЗОВ прежнего раннера: имя, за которым сразу идёт обратная + # кавычка tagged template. Без дефиса под это описание попадала любая проза + # вида `systemd-run` — имя команды, взятое в кавычки в комментарии, — и гейт + # падал на строке документации, не содержащей никакого вызова. В JavaScript + # идентификатор не может начинаться после дефиса, поэтому исключение точное: + # ни один настоящий вызов оно не пропускает. + ! grep -rEq '(^|[^-A-Za-z0-9_])(run|runVisible|runHidden|runSecret|runRawVisible)`' orchestrator/src \ || fail "acceptance: the pre-split runner names must not come back" # Порог считается от числа мутирующих раннеров, а не задан константой: новый # раннер обязан приносить с собой и проверку guard'а, а не проходить под @@ -1081,7 +1119,13 @@ run_single_owner_acceptance() { || fail "acceptance: снятие таймера автоотката не отделено от удаления резервных копий" grep -q 'export async function cleanupFirewallRollback' orchestrator/src/steps/firewall.ts \ || fail "acceptance: удаление резервных копий firewall не выделено в отдельную операцию" - ! grep -rq 'cancelFirewallRollback' orchestrator/src \ + # Ищется ОБЪЯВЛЕНИЕ или ВЫЗОВ, то есть имя со скобкой, а не упоминание. + # + # Скан по голой подстроке был сломан с самого своего появления: комментарий у + # `disarmFirewallRollback` объясняет разделение, называя прежнюю функцию по + # имени, и гейт падал бы на нём. Не падал он только потому, что до этого шага + # сборка не доходила — более ранние гейты останавливали её раньше. + ! grep -rEq 'cancelFirewallRollback[[:space:]]*\(' orchestrator/src \ || fail "acceptance: объединённая cancelFirewallRollback вернулась; она удаляла копии до фиксации успеха" "$BUN_BIN" -e ' const fs = require("node:fs");