Проблема обновления зависимости в том, что одна version в diff часто выглядит безопасной без доказательств. Цена скрыта за этой простотой: новая версия может сменить transitive tree, peer range, optional branch, lifecycle behavior или предположение о Node runtime. Если merge делают только потому, что advisory страшный, риск переносится в следующий запуск: service не стартует, feature ведёт себя иначе, а откат не подготовлен. Значит, вопрос не «обновили ли пакет», а «какие доказательства нужны, чтобы выпустить именно это изменение».
Нельзя ответить на него фразой «engines подходят». Поле engines в package.json сообщает заявленный диапазон runtime; в документации npm 8 указано, что без engine-strict оно обычно advisory и даёт warning. Даже строгая проверка range не исполняет application path, не проверяет native addon, browser bundle, peer dependency или внешний protocol. Runtime compatibility — отдельная проверка в названном окружении. Версия в metadata может быть входом для gate, но не доказательством его результата.
Соберите candidate change до запуска команд
Перед npm install надо назвать границы change. Какой package меняется: direct или transitive? Какая fromVersion зафиксирована в baseline lockfile? Какой toVersion предлагается? Что может измениться рядом: другие records, integrity, source URL, peer resolution, scripts, generated output? Какой runtime поддерживает сервис и где это проверяется? У этих вопросов нет одного универсального ответа, но без них невозможно отличить small patch от resolver rewrite. Задача update должна содержать не только target version, но и expected evidence.
Учебный input в этом sidecar задаёт demo-parser 1.0.0 → 1.0.1 и одинаковую метку node-18-demo у проекта и candidate. Функция planSyntheticSafeUpdate возвращает labelMatches: true, но сразу ставит compatibility и safety в not-proven-by-fixture. Это ключевое свойство примера. Совпадение двух строк проверяемо в памяти; совместимость настоящего процесса — нет. Если убрать это различие, fixture начнёт имитировать результат, которого не получал.
| Gate | Что проверяется | Артефакт PASS/FAIL | Чего недостаточно |
|---|---|---|---|
| Scope и baseline | package, from/to version, owner, advisory context | reviewable change record | что resolver выбрал ожидаемое дерево |
| Lockfile diff | добавленные, удалённые и changed records | diff конкретного commit | что install и app tests прошли |
| Clean install | воспроизводимость tree при зафиксированных правилах | CI log и exit status | что runtime path совместим |
| Project tests | выбранные application contracts | test report с environment | все modes и integrations |
| Runtime smoke | старт и named scenario в declared environment | наблюдаемый log/trace/result | отсутствие всех уязвимостей |
| Rollback | как вернуться к baseline change | план и проверяемая процедура | что уже применённые внешние эффекты исчезли |
Fixture называет gate, но не исполняет его
Ниже можно увидеть предел модели. Все четыре gates в synthetic input помечены как declared. Это значит только, что автор учебной записи не забыл назвать lockfile diff, clean install, project tests и runtime smoke. У каждого gate поле execution равно not-executed-by-fixture. Даже когда все поля true, plan.safety остаётся not-proven-by-fixture. Такой контракт полезен для review: он запрещает переносить булево поле из task template в утверждение о реальной проверке.
import {
planSyntheticSafeUpdate,
syntheticDependencyInput,
} from './web/scripts/upgrade-2023-04.mjs';
const plan = planSyntheticSafeUpdate(syntheticDependencyInput);
console.log(plan.runtime.labelMatches); // true
console.log(plan.runtime.compatibility); // not-proven-by-fixture
console.log(plan.gates[3].execution); // not-executed-by-fixture
console.log(plan.safety); // not-proven-by-fixture
const rollback = plan.rollback;
console.log(rollback.version); // 1.0.0, только synthetic label
Запуск node web/scripts/upgrade-2023-04.mjs --verify-fixture проверяет двадцать две assertions о synthetic object. Среди них есть отрицательная ветка с incomplete runtime gate, candidate, который не начинается от advisory version, и отсутствующим project contract. Нет assertion, который запускает npm ci, downloader, test runner или Node process приложения. Это специально: иначе учебный файл зависел бы от сети, локального cache и чужой конфигурации, а его результат пришлось бы выдавать за evidence, которого он не способен собрать.
Runtime-совместимость проверяется в контексте
Первый контекст — version policy: declared Node range, package manager version, OS, CPU, libc и доступные build tools. Второй — dependency semantics: peer dependencies, optional dependencies, postinstall, native code, exports и module format. Третий — приложение: startup path, configuration, migration, network contract и feature, ради которой package подключён. У одного update могут быть разные владельцы этих частей. Хороший gate называет, кто даёт environment и какой scenario должен завершиться, а не прячет весь риск за названием команды.
Документация npm 8 для npm ci полезна здесь именно ограничением: команда требует существующий lockfile, не переписывает его и может завершиться ошибкой, если manifest и lockfile не соответствуют друг другу. Это помогает воспроизводимо проверить install contract для определённого репозитория. Но успех npm ci не доказывает startup сервиса или использование конкретной функции. Поэтому clean install идёт до project tests, а runtime smoke — после них. Если проект не умеет безопасно выполнить smoke, это не повод объявлять range совместимым; это повод уменьшить change, подготовить environment или добавить наблюдаемую проверку.
Маршрут: симптом → причина → проверка → действие
- Симптом. В задаче есть только «обновить package до версии X» и ссылка на advisory. Нет baseline, diff дерева, runtime или rollback.
- Причина. Версия воспринимается как локальная строка, хотя update меняет resolution и может пересечь границы installer, build и приложения.
- Проверка. Зафиксируйте baseline commit, candidate manifest и lockfile diff. Отдельно назовите package manager, flags, supported runtime и scenario, который обязан пройти.
- Действие. На CI выполните clean install с теми же правилами, затем targeted tests и controlled runtime smoke. Сохраните для каждого gate ссылку на log или report, а не итоговую фразу «всё зелёное».
- Проверка модели. Запустите fixture: он должен показать, что named gate не равен execution, а matching runtime labels не равны compatibility. PASS не разрешает merge.
- Откат. До выпуска запишите baseline lockfile и условия возврата. Если update меняет data format, migration или внешний protocol, одного version rollback недостаточно — остановите задачу и добавьте отдельный release plan.
Выберите размер доказательства, а не размер страха
Не каждый advisory требует одинакового rollout. Но масштаб проверки выбирают по scope изменения и известной границе воздействия, а не по уверенности формулировки. Если update затрагивает только development tool и не попадает в release artifact, proof может ограничиться воспроизводимой установкой и checks pipeline. Если component участвует в server startup или обрабатывает внешние данные, нужен сценарий этой границы. Если затронуты native bindings или protocol, заранее договоритесь о compatible environments и способе убрать candidate. При недостатке информации корректный статус — pending evidence, а не «безопасный patch».
NIST SP 800-218 рекомендует встроить практики безопасной разработки в жизненный цикл, а не ждать отдельного большого аудита после каждой строки. В таком подходе dependency update оставляет след: source advisory, owner, diff, named checks, реальные результаты, decision и rollback. Форма может быть короткой, но ключевые переходы должны быть наблюдаемы. Нельзя заменять evidence чужим badge, statement package manager или результатом учебной функции. Эти вещи полезны только на своём уровне.
Ограничение модели и следующий шаг
planSyntheticSafeUpdate сравнивает строки demo runtime и хранит четыре declared gates. Он не читает engines, не решает semver range, не вызывает npm ci, не собирает bundle, не устанавливает package, не запускает test и не открывает network. rollbackSyntheticSafeUpdate возвращает только version label внутри object; он не меняет deployment и честно ставит deploymentEffect в not-assessed-by-fixture. Поэтому модель не умеет признать update совместимым или безопасным даже при двадцати зелёных assertions.
Следующий шаг — применить тот же gate к одному настоящему update, не перенося synthetic values. В task создайте baseline, candidate, expected lockfile diff, environment, test scenario, owner и rollback condition. После каждого фактического запуска приложите реальный artifact результата и подпишите его границу: install, test или runtime. Тогда advisory закрывается не из-за смены строки, а потому что команда может показать, что именно проверила и чего пока не проверяла.
Проверяемые источники
- npm Docs v8.19.4 (Legacy): package.json и поле engines, 26 октября 2022 — официальная legacy-документация npm 8, доступная до апреля 2023: engines задаёт заявленный диапазон runtime и без engine-strict обычно даёт предупреждение. Поле не заменяет запуск приложения.
- npm Docs v8.19.4 (Legacy): npm ci, 26 октября 2022 — официальная legacy-документация npm 8, доступная до апреля 2023: команда требует lockfile и не переписывает его. Успешный запуск всё равно надо зафиксировать отдельно от учебного контракта.
- NIST SP 800-218 SSDF Version 1.1, Final, 3 февраля 2022 — официальный финальный документ, доступный до апреля 2023. Он задаёт практики безопасной разработки и работы с компонентами, но не аттестует отдельный выпуск.
- GitHub Changelog: GitHub Advisory Database, 14 ноября 2019 — первичное сообщение GitHub о базе advisory, сопоставленных с пакетами dependency graph. Оно описывает источник данных, но не подтверждает применимость advisory к конкретному приложению.