English | Русский
CLI: качество и проверки
Проверка, шлюзы, ревью и аудит. Часть справочника команд. Начало и рабочий набор — cli.md.
Верификация
v1.5 Verify-First Contract. Тяжёлые гейты (pytest, tsc, cargo, phpstan, javac, js-test, terraform-validate, helm-lint, kubeconform, hadolint, ansible-lint) живут на триггере verify, а не task-done. Это разделяет «закрытие задачи» (миллисекунды) от «полной проверки» (минуты на больших проектах). Результат verify кешируется в таблице verification_runs на 10 минут (TTL настраивается через verify_cache_ttl_seconds в config.json), и task done использует кеш для немедленного закрытия.
Две фазы внутри прогона гейтов (1.10). Статические гейты идут ПЕРВЫМИ и отчитываются вместе; прогон тестов запускается, только если блокирующих отказов среди них нет. Замер: ruff плюс аудит дублирующихся тестов плюс аудит языка прозы отвечают за 3,5 секунды, полная лента — около четырёх минут, и пятнадцать раз за смену статический гейт падал ПОСЛЕ ленты, каждый раз стоя ещё одного прогона и двух-трёх вызовов. Внутри фазы отказ НЕ прерывает остальные: три дефекта обязаны вернуться одним отчётом, а не тремя раундами. Непрогнанный тестовый гейт отчитывается как COULD_NOT_RUN, а не как пройденный, — он применим и не дал свидетельства (SENAR §8.6(e)).
verify [--task SLUG] [--relevant-files PATH ...]
[--scope {lightweight,standard,high,critical,manual}]
[--no-tests-expected]
# Запустить scoped verify-trigger gates ad-hoc; пишет в verify cache.
# С --task: гейты scoped по relevant_files задачи.
# Без --task: гейты с пустым file scope (full suite для pytest).
# Cache hit (тот же files_hash, < 10 мин) пропускает запуск.
# Security-sensitive файлы (auth/payment/hooks) обходят cache.--relevant-files. Объявляет область задачи И проверяет её одной командой. Требует --task: область — свойство задачи, и записывать её больше некуда. Пути СОХРАНЯЮТСЯ в задачу, поэтому task done читает ту же область и попадает в кеш — источник правды один, строка задачи.
Без объявленной области каждый scoped-гейт получает [SKIP], а чек всё равно подписывается: он удостоверяет пустоту. До появления этого флага единственный рабочий путь (task update <slug> --relevant-files ...) не назывался ни в одном сообщении, а предупреждение предлагало флаг, которого у verify не было — поэтому игнорировать предупреждение было РАЗУМНЫМ поведением, а не небрежностью.
Пути передаются ЧЕРЕЗ ПРОБЕЛ — --relevant-files a.py b.py, одинаково у verify, task update и task done; список хранится как JSON, и это форма ХРАНЕНИЯ, а не формат ввода. Значение вида "a.py,b.py" для argparse — один аргумент, и раньше оно сохранялось одним путём: scoped-гейты бежали по пустоте, а отказ приходил от verify со словами «на эти файлы нет тестов» (GitLab #13). С 1.9 элемент с запятой, не разрешающийся ни в один файл, отвергается при записи с указанием верной формы; существующий путь с запятой в имени принимается как есть; объявленный путь, которого ещё нет, принимается с пометкой — задача может как раз его создавать.
--no-tests-expected. Прогон, в котором ни один гейт не выполнился (все [SKIP]), блокируется: он ничего не доказывает, а записанный зелёный по нему жил бы весь TTL при любых правках дерева. Для документации, конфигов и миграций это тупик — тестов там нет и не будет. Флаг объявляет это ЯВНО: прогон записывается зелёным с no_tests_declared = 1.
То же, когда выполнились только гейты НА ВЕСЬ ПРОЕКТ — без file_extensions, без file_patterns и без {files} в команде, вроде своего make check. Их PASS одинаков, менялись объявленные файлы или нет, поэтому, пока все гейты с файловой областью пропущены, прогон считается полностью пропущенным, а отказ называет гейты на весь проект. Конфиг только из таких гейтов это не затрагивает. Гейты тестов и сборки поставляемых стеков поэтому объявляют расширения своего языка.
Флаг покупает видимость, а не разрешение. Закрытие всё равно происходит без единого выполненного гейта — разница в том, что теперь такие закрытия можно пересчитать одним запросом, а не отличать их от проверенных нельзя вовсе:
SELECT task_slug, ran_at FROM verification_runs WHERE no_tests_declared = 1;Флаг касается только ПРОПУЩЕННЫХ гейтов. Упавший гейт остаётся красным, и verify по-прежнему выходит с кодом 1.
Workflow с verify-first:
.tausik/tausik task start my-task # QG-0
# … работа над кодом …
.tausik/tausik verify --task my-task # heavy: pytest etc. + печатает Verify handle
.tausik/tausik task done my-task --ac-verified # lightweight: поиск свежего прогона
# либо предъявить хендл, напечатанный verify, — точечная проверка вместо поиска:
.tausik/tausik task done my-task --ac-verified --verify-handle 4821.9f3c1a2b4d5e6f7089abcdef01234567Opt-out на legacy (CI/inline): установите в .tausik/config.json:
{ "task_done": { "auto_verify": true } }Тогда task done сам запустит verify-гейты внутри транзакции — старое поведение v1.3. Полезно для CI-окружений, где один длинный шаг лучше двух.
Pytest fast lane (v1.5.x). Дефолтная конфигурация pytest (pyproject.toml → [tool.pytest.ini_options] → addopts = "-m 'not slow'") пропускает тесты, помеченные @pytest.mark.slow (subprocess-тяжёлый bootstrap, MCP integration, e2e, stress). Это сокращает чистый прогон tausik verify на TAUSIK с ~12 минут до ~1.5 минут. Три escape-hatch'а, когда нужна полная батарея:
# 1. Прямой pytest, override addopts
pytest --override-ini='addopts=' tests/
# 2. Только marker-фильтр (перекрывает наследуемый -m 'not slow')
pytest -m '' tests/ # все тесты
pytest -m 'slow' tests/ # только slow (CI nightly)
# 3. Через verify-гейт — выставите env var до вызова tausik
TAUSIK_VERIFY_FULL=1 .tausik/tausik verify --task my-taskЧтобы пометить новый тест slow: file-level pytestmark = pytest.mark.slow (предпочтительно для целых файлов) или per-test @pytest.mark.slow. Применяй когда тест запускает subprocess'ы, ходит в сеть/MCP или спит > 200 ms — всё, что выпадает за бюджет интерактивного verify в < 60 с.
Терминология: Глоссарий verify / QG — opt-out, bypass и тестовый shim.
Шлюзы качества
gates status # Все gates и их конфигурация
gates list # Список gates с состоянием вкл/выкл
gates enable <name> # Включить gate
gates disable <name> # Выключить gateГейт path_artifact (block, на commit) связывает ПУТИ с АРТЕФАКТАМИ: при gates.path_artifact.map = [{"paths": ["scripts/**"], "artifacts": ["CHANGELOG.md"]}] проиндексированная правка в scripts/ требует содержательной правки CHANGELOG.md в том же коммите (только пробелы не считаются). Карта по умолчанию пуста — гейт ничего не охраняет и говорит об этом; нечитаемый набор файлов блокирует.
Гейт ruff_format (block, на verify и commit) гоняет ruff format --check по Python-файлам задачи. Файлы, расходившиеся на момент его появления, заморожены в tausik/gates.json → ruff_format.legacy_unformatted и пропускаются; перечень только сокращается — отформатировал файл из перечня, убери его из перечня той же правкой.
Гейт test_dedupe (block, на task-done и commit) краснеет на РОСТЕ числа КОПИЙ — тестов, являющихся одним и тем же кодом, если отбросить формат, комментарии и имя самой функции. База — храповик в закоммиченном tausik/gates.json (groups/tests/copies), существующий долг не блокирует. Предмет — РАЗЛИЧИМОСТЬ, а не количество: гейт не считает, сколько тестов в репозитории, и удалением тестов его удовлетворить нельзя.
Числа groups/tests — ОБЪЯВЛЕННЫЙ ОСТАТОК о похожести, а не долг от копипасты. Замер: из 286 групп 284 (99,3%) различаются ровно в тех частях, которые подпись стирает — именах, строках, числах, — то есть это один контракт на разных входах. Поэтому краснеет copies, а не форма. База без ключа copies читается как ноль: проект, принявший храповик раньше, проверку приобретает, а не теряет.
Отчёт печатает вердикт у КАЖДОЙ группы (COPY или PARALLEL) — вердикт записан, а не оставлен читателю. Полный отчёт по группам:
python scripts/audit_pytest_dedupe.py # markdown-отчёт по группам
python scripts/audit_pytest_dedupe.py --json # то же машиночитаемоRENAR drift-детекторы (§4.11)
RENAR §4.11 определяет 8 классов дрифта. Реализованы 2 (рекомендация R4 аудита), оба в warning-режиме — находки не блокируют, агент читает листинг и реагирует.
drift # Запустить все реализованные детекторы
drift --detector schema # Только drift-1 (схема артефактов)
drift --detector provenance # Только drift-7 (провенанс TC↔требование)
drift --detector supersession # ADR-007: delta-ADAPT на superseded-родителе
drift --detector standard # Сдвиг САМОГО стандарта RENAR (корпус против наших объявлений)
drift --detector senar # Сдвиг SENAR: заявленная редакция против выпущенной, форма Core (ключ senar_standard_corpus)drift-1 (schema) — ре-валидация SPEC/ADAPT против closed-lists + cross-field инвариантов, которые DB CHECK выразить не может:
delta_n ↔ parent_adapt(delta_n>0 без parent_adapt / delta_n=0 с parent_adapt),approved ↔ подпись архитектора(§7.5; клиентская подпись отозвана ADR-011 — сохранившиеся записи НАЗЫВАЮТСЯ находкойsignature-role-withdrawn, а не стираются), пустая version. Ловит прямые правки БД и пробелы миграций.drift-7 (TC↔requirement provenance) — у TAUSIK нет first-class TC; единица верификации — задача (её acceptance_criteria = «TC»), связанная со SPEC (требование) через
task_specs. Два сигнала:stale-verification(done-задача, но SPEC отредактирован после связывания → верификация устарела) иdeprecated-requirement(незавершённая задача на deprecated-SPEC).standard (сдвиг корпуса) — единственный детектор, который сверяет не базу с нашими объявлениями, а НАШИ ОБЪЯВЛЕНИЯ С САМИМ СТАНДАРТОМ: закрытые списки (типы SPEC §8.3, категории находок §7.4.4, статусы ADAPT §7.8.1), редакцию корпуса и принятые ADR, которых наш репозиторий не упоминает. Источник — локальный клон, путь задаётся ключом
renar_standard_corpusв.tausik/config.json; без него команда печатает «standard corpus: NOT CHECKED» и НЕ выдаёт «дрейфа нет». Предложенные ADR находкой не считаются никогда.
Также подключены как gates renar_drift_schema / renar_drift_provenance (severity=warn, trigger=task-done). Детектор standard гейтом не подключён: корпус есть не на каждой машине. Остальные 5 классов — вне scope.
RENAR conformance (§13.4)
renar conformance # Сгенерировать RENAR-CONFORMANCE.yaml (в stdout)
renar conformance --write # Записать в RENAR-CONFORMANCE.yaml в корне проекта
renar conformance --assessor <id>
renar export [--out DIR] [--check] # Сериализовать specs+adapts+conformance в дерево renar/; --check — CI drift-гейт (exit 1 при stale)Self-assessment-манифест со всеми mandatory-полями §13.4.2. Уровень RENAR-1..5 вычисляется честно из live-БД (§13.4.3), не декларативно: нарушение любой mandatory clause → pre_adoption: true + level: null (паттерн kai, аудит аудите принятия). Секция assessment-evidence показывает raw-counts + per-signal met/unmet и где именно заблокирован уровень — агенту видно, что нужно до следующего уровня. Машинные клаузы (closed-lists, V1–V6, QG-0/QG-2, schema-validation hook = наш drift-1) подтверждаются capability; data-клаузы (adapt-per-tz) — только при наличии артефактов.
Периодический аудит (SENAR Rule 9.5)
audit check # Просрочен ли аудит: закрытий с последней отметки ≥ audit_every_closures (17)
audit mark # Отметить аудит выполненным сейчас (сессия не нужна)
audit vendors [--json] # Аудит клонированных vendor skill repos (read-only): классифицирует
# как 'installed' (в installed_skills config) или 'vendored_unused'
# (кандидат на `skill repo remove`). Никогда не удаляет.
audit research [--min-age-days N] [--json]
# Аудит docs/{en,ru}/research/ на устаревшие непривязанные файлы
# (по умолчанию >30 дней, без ссылок в tests/scripts/CHANGELOG/README).
# Read-only — показывает кандидатов на перенос в docs/_archive/research/.
audit evidence [--json] [--no-git]
# Разрешаются ли ещё ссылки на тесты в закрытых задачах.
# Read-only, НИКОГДА не блокирует: переименовать тест законно,
# задача команды — сделать распад ВИДИМЫМ.Четыре корзины audit evidence, и они намеренно не сводятся в одно число «сломано»:
| корзина | что означает |
|---|---|
ROTTED | цель БЫЛА в истории git и исчезла — переименование или удаление после закрытия. Ссылка распалась, покрытие может быть цело. |
NEVER_EXISTED | цели в истории НЕ БЫЛО — ссылка выдумана при закрытии. |
ILLUSTRATIVE | это ПРИМЕР, а не ссылка: tests/foo.py, tests/test_does_not_exist.py, tests/https://github.com/Kibertum/tausik-core/blob/v1.11.0/scripts/prod.py. Такие имена цитируют задачи, чей ПРЕДМЕТ — сама форма ссылки. |
UNKNOWN_HISTORY | git не ответил — вердикт удержан, а не угадан. Появляется при --no-git. |
Корзина ILLUSTRATIVE отделена в #209 по замеру: из 25 записей в NEVER_EXISTED примерами были 13, настоящими — 3, то есть шапка завышала реальный распад вдвое рядом с 22 записями ROTTED, которые читатель приучался пролистывать. Примеры считаются отдельно, а не выбрасываются: число, молча теряющее записи, — следующая версия той же болезни. Правило исключения печатается рядом с каждой записью. Понятие «путь-пример» общее с детектором stale_file команды memory lint (scripts/illustrative_paths.py) — второй список заглушек и был причиной расхождения детекторов.
Ревью (SENAR Rule 10.15) — v1.5
Учёт L1/L2/L3 ревью-прогонов и метрика ADR (Adversarial Defect Rate).
review record --task <slug> --type {L1|L2|L3} \
[--critical N] [--warnings N] [--reason "..."] [--notes "..."]
# --critical > 0 без --reason отказывается: CRITICAL записывается
# с причиной (SENAR 1.5 §10.15(f); шкала: severity-scale.md)
[--reviewer-model M] [--author-model M]
# только L3: обе модели пишутся в notes; пара одного семейства,
# отсутствие ревьюера или неизвестный автор — отказ (SENAR Rule 4).
# --author-model по умолчанию — модель текущей сессии.
review list [--task <slug>] [--type {L1|L2|L3}] [--limit N] [--json]
review metrics # ADR = critical_findings / L3_reviewed_tasks * 100Скилл /review сам вызывает review record --type L3 (он запускает 6 adversarial-агентов в отдельном контексте). В tausik metrics появляется блок Adversarial Review, как только есть хотя бы один L3-прогон.
Обход гейта записывается (SENAR 1.5 §8.6(j))
Правка артефакта задачи по маршруту, перед которым гейт не стоит — ВКЛЮЧАЯ прямую правку супервизором, — допускает эффект, объявленный QG-0, без положительного вердикта; §8.6(h) считает это обходом независимо от намерения. Это НЕ запрет, а регулируемое исключение, и оно остаётся доступным. Требуется ЗАПИСЬ — в каждом случае.
Признанный случай один — среда, в которой нельзя запустить ни одного агента и не на что переключиться (SENAR 1.5 §4.1, редакция от 07.09.2026; пересматривается по §10.13 при смене поколения моделей). CLI печатает его в отказе по-английски: The one recognized case is an environment in which no agent can be run and there is nothing to switch to.
SENAR 1.4 §4.1 перечислял пять случаев. Остальные четыре — агент застрял, агент прошёл большую часть пути, правка дешевле подготовки контекста, срочный хотфикс — решаются агентскими средствами там, где агентская среда есть. Если они всё же случились, они записываются. Относится ли случай к признанному, решает старший, одобряющий обход.
events emit-supervision --vector direct_edit --task <slug> \
--rationale "на этом хосте нет среды агента, переключиться не на что" \
--risk-accepted "правка уходит без scoped verify" \
--remediation "перепрогнать verify и перезакрыть задачу" \
--approved-by "владелец"Запись без --rationale ОТКЛОНЯЕТСЯ (exit 2): обход без причины — это форма, заполненная не глядя. Отклоняется ЗАПИСЬ, а не правка: правка уже произошла.
ПОЧЕМУ МЕТРИКА СЧИТАЕТСЯ ИЗ ЗАПИСЕЙ, А НЕ ИЗ САМООТЧЁТА (§9.2, §9.3): самоотчётная цифра есть УТВЕРЖДЕНИЕ, а не измерение; она не удовлетворяет ни §8.6(c), ни §8.6(d), и стандарт не может требовать от гейтов того, чего не требует от собственной меры соблюдения.
ДВЕ ЧАСТОТЫ ВЛОЖЕНЫ И НЕ СКЛАДЫВАЮТСЯ (§8.6(i)): ручные вмешательства — ПОДМНОЖЕСТВО обходов, поэтому metrics печатает их как «из них», а не второй строкой итога. Сложенные, они дадут двойной счёт, и порог сработает на команде, которая не делает ничего плохого.