Поиск с флагом -q прекращает чтение на первом совпадении и закрывает свой конец
канала. Продюсер, которому осталось что писать, получает SIGPIPE и завершается
кодом 141, а `set -o pipefail` делает 141 статусом всей конструкции:
совпадение НАЙДЕНО -> продюсер оборван -> статус 141 -> «не найдено»
Для утвердительных проверок это ложный FAIL. Для отрицательных — «такой
конструкции в коде нет» — ложный PASS: запрещённая конструкция найдена, а гейт
зелёный. Отрицательными проверками закреплена половина инвариантов приёмки,
включая запрет обхода тестов и запрет `pnpm audit --prod`.
Порог резкий: пока вывод продюсера помещается в буфер канала (64 KiB на Linux),
он не блокируется и успевает завершиться раньше, чем потребитель начнёт читать.
Замер, 60 прогонов на размер: до 60 KiB — 0 отказов, ровно на 64 KiB — 58/60,
от 96 KiB — 60/60. То есть проверка выглядит исправной ровно до первого
источника крупнее буфера, а такие файлы в репозитории уже есть.
- 56 мест переведены на here-string: `grep -q PATTERN <<<"$content"`;
- продюсеры-команды (ss|awk, dpkg-query, /proc/cpuinfo, systemctl
list-unit-files) сначала читаются в переменную;
- введён code_has: десять отрицательных сканов держались на `|| true` внутри
code_without_comments, гасившем 141, — то есть на побочном эффекте
подавления ошибок, а не на заявленном свойстве;
- несуществующий путь в скане больше не означает успех: `2>/dev/null || true`
превращал опечатку в пустой вывод, а пустой вывод для проверки «этого в коде
нет» — это PASS. Проверка явная, а не через set -e: в контексте `! code_has`
bash отключает errexit на весь вызов;
- возврат пайплайна запрещён отдельной приёмкой.
Два гейта сборки проверяли не то, что обещали.
1. pnpm audit проверял production-подграф вместо всего lock-графа.
Гейт запускался с --prod под обоснованием «devDependencies в артефакт не
попадают». Для frontend build tooling это неверно по существу: vite и
rollup действительно не копируются на production-сервер как node_modules,
но они ИСПОЛНЯЮТСЯ на build-машине, читают наши исходники и порождают тот
самый production-бандл, который уезжает в артефакт.
Это не гипотеза: DOM clobbering в Rollup затрагивал именно генерируемый
бандл, и `pnpm audit --prod` его не показывал — по всему графу тот же
прогон дал 33 предупреждения против нуля. Критерий приёмки №47 в docs/11
формулировал «по всему графу» правильно ещё до того, как это стало правдой
в коде.
На текущем lock-файле полный граф на пороге high чист.
2. SKIP_TESTS позволял собрать production-артефакт без тестов.
Переменная была описана как «аварийное отключение тестов; для
release-сборок недопустимо». Недопустимость держалась исключительно на этой
фразе: ни metadata, ни финальная приёмка архива не проверяли, что тесты
запускались. То есть
SKIP_TESTS=true ./tools/build/build.sh
доходила до конца и выдавала обычный tarball с build_profile=production и
dependency_security_gate=true — артефакт, по которому невозможно отличить
проверенную сборку от непроверенной.
Глушила она при этом не только тесты: под тем же флагом пропускались
`tsc --noEmit` для оркестратора и `go vet` для админки, то есть проверка
типов и статический анализ того самого кода, который уезжает в production.
Выбран тот же строгий вариант, что уже принят для проверки зависимостей:
обхода нет. Готовый пакет объявляет tests_gate=true в metadata, и это
утверждение опирается на результат — обе функции прогона выставляют свой
флаг только после успешного завершения, а write_metadata отказывается
писать метаданные, если хотя бы один не подтверждён.
Приёмка закрепляет оба инварианта: --prod не может вернуться в гейт, SKIP_TESTS
не может вернуться ни в один модуль сборки и ни в README/docs, tests_gate=true
обязателен в metadata, а утверждение о прогоне обязано следовать за прогоном.
Новый docs/14-legacy-cleanup.md: как выглядит отказ установщика, полный
список маркеров чужой установки, что сохранить перед очисткой, работа
purge-v0.sh, ручная процедура и отдельно - случай незавершённой
установки текущего поколения, где нужен repair, а не очистка.
Обновлено под фактическое поведение:
- README и package/docs: установка описана как две фазы, PHASE 0 ничего
не меняет; добавлен troubleshooting по отказу clean-host; версии
toolchain больше не передаются через окружение;
- 02-build-layer: раздел про versions.env (что в нём есть и чего нет и
почему), verify_versions_contract, проверка происхождения артефакта
по upstream hashes.txt;
- 08-orchestrator-spec: двухфазный контракт, read-only guard,
идентификация поколения в install-state, ownership-aware rollback,
расширенная семантическая проверка конфига, структурная редакция;
- 04-admin-panel: таблица удалённых маршрутов и почему они удалены, а
не оставлены заглушками; сужена формулировка гарантии санитайза;
- 11-testing: новые unit-наборы, полный список инвариантов конфига,
раздел про одну реализацию URI вместо двух, сценарий проверки
границы установки на живом сервере;
- 12-operations и 13-runbook: диагностика отказов по поколению,
поведение diagnostics-бандла;
- tools/build/README: контракт версий, обе суммы Bun, hashes.txt.
CHANGELOG: раздел Unreleased с разбором каждого исправленного дефекта.
Сквозная миграция HY2XS на современную Hysteria (2.12.2) и переход на v1.
Build:
- версия Hysteria резолвится на этапе сборки из HyNetworks/hysteria и
замораживается в metadata пакета (version + immutable url + sha256);
- compatibility gate: реальный бинарник должен принять канонический конфиг
HY2XS для gecko и salamander до создания пакета;
- сборка прогоняет тесты оркестратора и админки.
Конфигурационный контракт:
- HY2XS_CONFIG_SCHEMA_VERSION=2, чужая схема отклоняется fail-fast;
- obfs стал настоящим union gecko|salamander, gecko — default;
- obfs-блок рендерится оркестратором целиком, два подтипа одновременно
структурно невозможны;
- современный baseline: congestion bbr/standard, disableLossCompensation=false,
disableStatelessReset=false, полный quic-блок.
Исправления:
- share URI для gecko: генератор был завязан на Obfs.Salamander.Password и
выдавал нерабочую ссылку при любой другой обфускации;
- SNI брался только из ACME-блока и уходил пустым при HY2XS_TLS_MODE=file;
- экспорт конфига выносил trafficStats.secret, access_token и obfs-пароль;
- экспорт терял неизвестные upstream-поля при round-trip через типизированную
модель;
- renderRuntimeEnv печатал тип обфускации литералом, расходясь с конфигом;
- namedotcom удалён из ACME-реестра (нет в Hysteria с 2.11.0).
Тесты:
- 95 тестов оркестратора: env, рендер, семантика профиля, резолвер, rollover;
- тесты URI и экспорта в Go;
- tools/test/e2e-hysteria.sh с реальным клиентом Hysteria.
UX:
- подсказки и примеры в форме создания пира.
Прочее: CHANGELOG.md, .gitattributes (LF для target-side файлов),
документация на русском.
HY2XS больше не описывается как форк H UI. Из README, docs, сообщений
builder'а и post-install metadata убрана вся fork/H UI терминология.
Лицензия:
- LICENSE: MIT заменён на полный текст AGPL-3.0-only
- README: бейдж и раздел лицензии, подпись Flamy Studio
- orchestrator/package.json, apps/frontend/package.json: license
- package.sh: LICENSE кладётся в install package, license=AGPL-3.0-only
в metadata
- verify.sh, acceptance.sh: проверки корневой AGPL и metadata
Документация:
- 04-admin-panel-h-ui-fork.md -> 04-admin-panel.md, переписан вокруг
модели Hysteria2 = external runtime dependency,
HY2XS admin = native HY2XS component
- docs 01, 02, 03, 08, 09, 11, 12, README: единая терминология HY2XS admin
post-install.env:
- блок HUI_* заменён на HY2XS_ADMIN_*, HUI_FORK_REF -> HY2XS_ADMIN_SOURCE
Внутренний legacy namespace (H_UI_* ключи SQLite, HUI_DATA/HUI_LOG,
API /hui, h_ui_db.sql) намеренно не тронут: он требует отдельной
миграции БД и выносится в отдельный этап.