docs: spec for the exact integration candidate and the input manifest

Issue: #492
User-Visible: no
This commit is contained in:
Codex
2026-09-09 00:42:01 +03:00
parent c9a0690c2d
commit 96ecf962ed
2 changed files with 175 additions and 0 deletions
+174
View File
@@ -0,0 +1,174 @@
# #492 — Точный кандидат интеграции и полный manifest входов selection/reuse
- **Issue:** https://github.com/Matysh/houseplan-card/issues/492
- **Тип / приоритет:** infra, tech-debt / P1
- **Трек:** полный — четыре поверхности протокола CI (слияние, реюз, классификация, отбор мутантов), без файлов класса A
- **Оценка:** ценность для разработки 9/10; сложность 6/10; риск 4/10 (протокол, который сам себя проверяет, — ошибка в нём делает зелёный ничего не значащим)
- **Связано:** аудит 2026-09-08 §11 п.3 (I1/I2); #208 (реюз), #312 (материал ревью), #364 (dev ушёл), #430 (оснастка смоков), #475 (отбор по гарду), #479 (тяжёлые гейты), #481 (журнал), `PROCESS.md` §7.2, §8, §10.3, §11.4
## 1. Проблема
Зелёный вердикт и зелёный Validate обязаны означать одно: *именно этот код* проверен *всеми проверками, чьи входы менялись*. Сейчас это верно не всегда — четыре механизма, подтверждённые чтением `ea6061e9`.
**1. Сливается не то дерево, что проверено.** `process.yml` фиксирует материал ревью (`steps.material`), после зелёного вердикта сверяет вершину ветки с материалом (#312), затем делает `git rebase origin/dev` и сразу `push HEAD:dev`. Если `dev` продвинулся за время ревью (28 августа — четыре раза за день), результат ребейза — новое дерево, которое никто не проверял: ни ревью (материал другой), ни Validate (на этот SHA он не бежал). Предупреждение #364 пишет комментарий и не блокирует. Минимальный git-эксперимент аудита: чистый ребейз, вердикт принят, поведение `20 → 40`.
**2. Реюз бэкенда не видит часть своих входов.** Job `backend` исполняет: pytest `tests_backend/`, `python -m unittest discover -s scripts/support-relay/tests`, ruff/mypy по `pyproject.toml`, порог покрытия. Тесты читают `scripts/sh3d-convert/golden/*.json`, `scripts/sh3d-convert/convert.mjs` (константы версий), `scripts/dump-config-schema.py`, `scripts/config-schema.json`. Ключ реюза (`HARNESS.backend` в `gate-reuse.mjs`) знает `tests_backend`, `custom_components/**/*.py`, `pytest.ini`, baseline и `pyproject.toml` — и **не знает** relay, converter, schema. Прямой случай: правка только `scripts/support-relay/relay.py` даёт `backend=true` в `changes` (регэксп классификатора), но ключ реюза не меняется — job отменяется как переиспользованная, и тесты relay не бегут. Классификатор `backend` при этом не видит `pyproject.toml` и `scripts/sh3d-convert/`: правка только их не запускает job вовсе.
**3. Golden и перф не видят протокол харнеса.** `demo/golden/run.mjs` импортирует `../serve.mjs` и `../bundle-freshness.mjs`; бенчмарки — `serve.mjs`, `editor-runtime-compat.mjs`, `bundle-freshness.mjs`; `serve.mjs` отдаёт страницу `demo/srv/demo.html`. Ключи `golden` и `performance_smoke` держат только `demo/golden/**` и `demo/performance/** + два benchmark`; `serve.mjs` есть лишь у `smoke` (#430), `demo/srv/demo.html` и compat-хелперы — ни у кого. Обратный перекос: `sourceFingerprint` (`src/**`) подмешан во **все** ключи, и любая правка UI сбрасывает реюз бэкенда, который UI не исполняет.
**4. Отбор мутантов теряет обёртки и новые определения.** `guardFiles()` берёт из строки гарда только токены-пути. 10 из 35 гардов `scripts/backend-test-guard.mjs` не передают третий аргумент и работают по умолчанию с `tests_backend/test_ha_import_export.py` — правка этого файла не отбирает их (#475 промахивается ровно так, как описано в аудите). `scripts/trail-resume-test-guard.mjs` держит пути тестов внутри себя — свидетели `vacuum-trail-*` не отбираются и не меняют отпечаток журнала (#481) при правке `test_trails.py`. Смоки импортируют `serve.mjs` и фикстуры — их изменение тоже не отбирает. При диффе только по `scripts/mutation-gate.mjs` job `changed_mutants` запускается (#475 r1), но `selectChangedMutants` пересекает дифф с файлами патчей и гардов — новый мутант, не трогающий их, не выбирается: «реестр изменился» не превращается в «новый свидетель прогнан».
**5. Ночной прогон рапортует об очереди, не о результате.** `nightly.yml` делает `gh workflow run validate.yml … -f full=true` и завершается успехом в момент постановки в очередь; красный полный прогон не делает ночной workflow красным.
## 1.1. Сценарий
Персоны: автор задачи, ревьюер, обслуживающий чат, владелец, читающий статусы. Все они читают «зелёный» как «проверено». Задача — сделать это чтение верным на четырёх швах, не удлиняя обычный путь: ветка, не пересекающаяся с движением `dev`, и правка, не трогающая входов job, проходят так же быстро, как сейчас.
## 1.2. Что человек увидит до и после
До: вердикт зелёный → `S8-merged`, в `dev` — ребейзнутое дерево без прогона; правка relay зелёная за секунды, потому что тесты relay не бежали; новый мутант в реестре зелёный, потому что не выбран.
После: если `dev` ушёл — конвейер публикует точный кандидат в ветку, ждёт Validate на этом SHA и только потом двигает `dev` с `--force-with-lease` на ожидаемую базу; в issue — строка «кандидат `abc1234` (ребейз на `dev@def5678`), Validate зелёный, слито». Правка relay/converter/schema/serve.mjs/demo.html запускает и не переиспользует те job, которые их исполняют. Новый или изменённый мутант бежит на первом же пуше. Ночной workflow красный, когда красный Validate.
## 2. Скоуп
1. **Слияние точного кандидата** (§4): проверка нового дерева перед push в `dev`, безопасная обработка повторного движения `dev`, равенство patch-id проверенного и сливаемого диффа.
2. **Единый manifest входов** (§5): один модуль объявляет входы каждой проверки по категориям source / tests / fixtures / config / toolchain / protocol; из него выводятся и классификация (`changes`), и ключ реюза; неизвестный исполняемый вход расширяет проверки; лишняя зависимость бэкенда от `src/**` снимается после доказанной полноты.
3. **Замыкание входов гарда мутанта** (§6): обёртки объявляют свои входы, импорты и фикстуры гардов входят в отбор и отпечаток; изменённые/новые определения реестра отбираются явно.
4. **Ночной прогон ждёт результат** (§7).
5. **Отрицательные тесты протокола** (§8): по каждому представительному входу доказано отсутствие false-green.
## 3. Не-скоуп
- Продуктовый код (`src/**`, `custom_components/**/*.py`) — если замыкание найдёт дефект продукта, он заводится отдельным issue.
- Перечень и содержание самих проверок Validate (какие job существуют, их бюджеты).
- Замена ревью-конвейера или изменение правил §7.2 о повторном ревью: при чистом ребейзе с равным patch-id вердикт остаётся в силе; при изменившемся диффе — повторное ревью по существующим правилам.
- Полный ночной набор (#479) остаётся полным; реюз на кандидатах по-прежнему невозможен (версия входит в manifest source).
- Отбор мутантов по транзитивным зависимостям **внутри `src/**`** — сторона патча остаётся точечной (§6.4).
## 4. Слияние точного кандидата
### 4.1. Решение выносится в скрипт
Логика шага «Слить ветку в dev» переезжает из shell в `scripts/merge-candidate.mjs` с чистой функцией `decideMerge(state)` и исполняющей обёрткой; `process.yml` вызывает скрипт. Состояние: `material` (SHA материала), `materialBase` (merge-base материала с `dev` на момент фиксации), `actual` (вершина ветки), `devNow`, `candidate` (результат ребейза `actual` на `devNow`), patch-id диффов `materialBase..material` и `devNow..candidate`, результат Validate на `candidate`, номер попытки.
### 4.2. Алгоритм
1. Проверка #312 как сейчас: `actual` = `material` либо `material` + один коммит документа ревью.
2. `devNow == materialBase` → `dev` не двигался, `candidate == actual` → push `HEAD:dev` c `--force-with-lease=refs/heads/dev:$devNow` (fast-forward). Как сейчас, плюс lease.
3. `dev` двигался → ребейз (конфликт — как сейчас, `S6-in-progress`). Сравнение patch-id: если дифф ветки после ребейза отличается от проверенного (контекст/содержимое патча изменились из-за соседних правок), кандидат публикуется в ветку, задача возвращается в `S7-code-review` с комментарием «дифф изменился при ребейзе, нужен новый заход ревью (§7.2)» — без слияния.
4. Patch-id равен → кандидат публикуется в ветку задачи (`--force-with-lease=refs/heads/$BRANCH:$actual`); пуш PAT-ом запускает Validate на `candidate`. Скрипт находит прогон по SHA (`gh run list --workflow validate.yml --commit`) и ждёт завершения (`gh run watch --exit-status`, лимит 45 мин; отсутствие прогона в течение 3 мин — ошибка, не «зелёный»).
5. Validate красный → `S6-in-progress`, комментарий с ссылкой на прогон: «кандидат после ребейза на `dev@…` красный».
6. Validate зелёный → `git push --force-with-lease=refs/heads/dev:$devNow candidate:dev`. Lease отклонён (`dev` двинулся снова) → `fetch`, новая попытка с п.3; не более **3** попыток, после — `S6-in-progress` с комментарием «dev движется быстрее слияния, повторить».
7. `S8-merged` — только после успешного push (инвариант «метка не врёт» сохраняется).
Что считается проверкой нового дерева: Validate на ветке — лёгкий набор плюс диффозависимые гейты (`changed_mutants` по `dev..candidate`, перф-профили по диффу). Тяжёлые гейты по #479 остаются за кандидатом релиза; это сознательно: слияние не превращает каждый ребейз в 20-минутный прогон, а проверяет ровно то, что проверяет обычный пуш той же дельты.
### 4.3. Что видно в issue
Один комментарий на слияние: `материал abc1234 · dev@def5678 → кандидат 0123abc · Validate <ссылка> зелёный · слито`. Комментарий #364 («dev продвинулся») остаётся — теперь он предваряет проверку, а не констатирует риск.
## 5. Единый manifest входов
### 5.1. Модуль
`scripts/check-inputs.mjs` экспортирует `CHECKS` — по одной записи на проверку Validate: `frontend`, `backend`, `changed_mutants`, `integration` (hacs + hassfest), `smoke`, `golden`, `performance_smoke`, `docs`. Каждая запись — категории входов, каждая категория — список путей/предикатов:
| категория | что это | примеры |
|---|---|---|
| `source` | код, который проверка исполняет или собирает | `src/**` для frontend/smoke/golden/perf; `custom_components/**/*.py` для backend/integration; `scripts/sh3d-convert/*.mjs`, `scripts/support-relay/**/*.py` для backend |
| `tests` | сами тесты и их обёртки | `test/**`, `tests_backend/**`, `scripts/support-relay/tests/**`, `scripts/*-guard.mjs` |
| `fixtures` | данные, которые тесты читают | `demo/fixtures/**`, `demo/golden/**` (сценарии и эталоны), `scripts/sh3d-convert/golden/**`, `scripts/config-schema.json` |
| `config` | конфиги инструментов | `pyproject.toml`, `pytest.ini`, `tsconfig*.json`, `rollup.config.mjs`, `scripts/backend-coverage-baseline.txt`, `demo/performance/budgets-*.json` |
| `toolchain` | что определяет среду и версии | `package.json`, `package-lock.json`, `tests_backend/requirements.txt`, сам `.github/workflows/validate.yml` (секция job — см. 5.4) |
| `protocol` | харнес и протокол job | `demo/serve.mjs`, `demo/srv/**` (кроме `assets/` класса D), `demo/bundle-freshness.mjs`, `demo/*-runtime-compat.mjs`, `demo/guard/**`, `scripts/gate-reuse.mjs`, `scripts/classify-changes.mjs`, `scripts/check-inputs.mjs`, `scripts/mutation-gate.mjs` для `changed_mutants` |
Функции: `inputsOf(check) → string[]` (развёрнутый по `git ls-files` список), `checksAffectedBy(files) → Set<check>`, `coverage(files) → { covered, unknown }`.
### 5.2. Классификация из manifest
`classify-changes.mjs` перестаёт держать свои регэкспы: выход `X = 'true'` ⇔ `checksAffectedBy(diff)` содержит X. Подклассификаторы `perf_iso` / `perf_interaction` остаются как есть (фильтры по `source` перф-смока). **Неизвестный исполняемый вход** — изменённый файл, который `coverage` не относит ни к одной проверке и который по расширению/пути исполняем (`*.mjs|*.js|*.ts|*.py|*.json|*.yml|*.yaml|*.html|*.toml|*.txt` под `scripts/`, `demo/`, `test/`, `tests_backend/`, `.github/`, `custom_components/`, `src/`) — включает **все** выходы (`classifyAll`) и пишет в summary «неизвестный вход: <файл> — расширен до полного набора». Документация (`docs/**`, `*.md` вне `docs`) и class-D копии бандла — известные *не*-входы, они не расширяют.
### 5.3. Ключ реюза из manifest
`gate-reuse.mjs`: ключ job = `job:` + хеш содержимого файлов `inputsOf(job)` (канонизация переводов строк как сейчас). `HARNESS` и подмешивание `sourceFingerprint` уходят; `sourceFingerprint` остаётся у бандла и скриншотов (#245) — там его место. Следствие: `backend` перестаёт зависеть от `src/**`, но это включается **последним коммитом**, после того как §8.3 (полнота) зелёный, и с записью в CHANGELOG-у-инфры (`docs/TESTING.md`). Маркер падения (#386) и маркеры успеха не меняют формат — меняется только способ вычисления ключа, старые маркеры просто не совпадут один раз.
### 5.4. Секция job в toolchain
Правка `validate.yml` меняет протокол только той job, чью секцию правят. `inputsOf` для категории `toolchain` включает `validate.yml` целиком — простое и честное решение: правка workflow гоняет всё. Отдельная «секция job» не вырезается (YAML-парсинг ради экономии одного прогона — лишняя сложность).
### 5.5. Полнота
`test/check-inputs.test.mjs` держит **лист покрытия**: каждый файл из `git ls-files`, исполняемый по критерию §5.2, обязан входить в manifest хотя бы одной проверки или в явный список `NOT_AN_INPUT` (с причиной: `demo/docs/**` — съёмка документации вне Validate, `scripts/release-*.mjs` — релизные, `scripts/*.md`). Новый скрипт в `scripts/` без записи в manifest — красный тест, не тихое расширение.
## 6. Замыкание входов гарда мутанта
### 6.1. Обёртки объявляют входы
Каждый `scripts/*-guard.mjs`, используемый в `guard:` реестра, экспортирует `export const GUARD_INPUTS = [...]` — файлы, которые он запускает или читает (для `backend-test-guard.mjs` — умолчание `tests_backend/test_ha_import_export.py` **плюс** третий аргумент, если передан). `guardInputs(guard)` = файлы-токены из строки (как `guardFiles`) ∪ `GUARD_INPUTS` каждой найденной обёртки (читаются статически регэкспом, без исполнения). Тест реестра: у каждой обёртки из гардов есть `GUARD_INPUTS`, и умолчание в коде совпадает с объявлением.
### 6.2. Импорты и фикстуры гарда
Для каждого файла гарда `.mjs` — транзитивное замыкание относительных `import`/`export … from` внутри `test/`, `demo/`, `scripts/` (не `src/`, не `node_modules`); для `.py` — `from tests_backend.x import`, `import tests_backend.x` и всегда `tests_backend/conftest.py`. Фикстуры — строковые литералы путей `demo/fixtures/…`, `demo/golden/…`, `scripts/sh3d-convert/golden/…`, `test/fixtures/…` в файлах замыкания. Замыкание кэшируется на прогон (реестр — 582 мутанта, сотни гардов; чтение файлов один раз).
### 6.3. Отбор и отпечаток
`selectChangedMutants` и `witnessFingerprint` используют `guardInputs` + замыкание §6.2. Отпечаток включает содержимое всех файлов замыкания — правка `test_trails.py` меняет отпечаток `vacuum-trail-*`, правка `serve.mjs` — всех смок-свидетелей.
### 6.4. Изменённые определения реестра
Если дифф содержит `scripts/mutation-gate.mjs`, отбор дополнительно берёт мутантов, чьё определение (`id`, `guard`, `patches`, `because`) добавлено или изменено относительно базы: реестр базы читается через `git show <base>:scripts/mutation-gate.mjs` во временный файл и импортируется как модуль (реестр — данные, побочных эффектов при импорте нет — это закреплено тестом). Удалённые мутанты — предупреждение в summary. Сторона патча остаётся точечной (не-скоуп): мутант, патчащий `a.ts`, не отбирается правкой `b.ts` — это зона полного прогона (ночь/кандидат), как и было.
## 7. Ночной прогон
`nightly.yml` после `gh workflow run` находит запущенный прогон (по `workflow_dispatch`, ветке `dev`, времени ≥ момента запуска; до 3 мин ожидания появления) и `gh run watch <id> --exit-status` с лимитом job 90 мин. Красный Validate → красный nightly; сообщение о падении в summary — ссылка на дочерний прогон.
## 8. Отрицательные тесты протокола
8.1. **Manifest**: для каждой проверки и каждой непустой категории — представительный реальный файл; тест подменяет его содержимое (через инъекцию `read` в функцию ключа) и утверждает: ключ реюза меняется, `checksAffectedBy` содержит проверку. Список представителей: `scripts/support-relay/relay.py`, `scripts/sh3d-convert/golden/*.json`, `scripts/config-schema.json`, `pyproject.toml`, `demo/serve.mjs`, `demo/srv/demo.html`, `demo/editor-runtime-compat.mjs`, `demo/golden/baselines/*.png`, `demo/performance/budgets-*.json`, `tests_backend/requirements.txt`, `.github/workflows/validate.yml`.
8.2. **Мутанты**: правка `tests_backend/test_ha_import_export.py` отбирает все 10 обёрток без третьего аргумента; правка `tests_backend/test_trails.py` отбирает `vacuum-trail-*` и меняет их отпечаток; правка `demo/serve.mjs` отбирает все смок-свидетели; дифф только по реестру с новым мутантом отбирает ровно его.
8.3. **Полнота**: лист покрытия §5.5 зелёный на текущем `git ls-files`.
8.4. **Слияние**: `decideMerge` — таблица случаев: `dev` не двигался → push; двигался, patch-id равен, Validate зелёный → push с lease; lease отклонён дважды → повтор, трижды → `S6`; patch-id отличается → `S7`; Validate красный → `S6`; прогон не найден → ошибка, не push. Плюс git-эксперимент аудита в temp-репозитории (`20 → 40`): скрипт требует Validate, не пушит.
8.5. **Мутанты на протокол** (реестр `mutation-gate.mjs`, каждый с отрицательным прогоном): `manifest-drops-protocol-category` (ключ без `protocol` → 8.1 падает), `classify-unknown-input-is-unaffected` (неизвестный вход не расширяет → 8.1 падает), `guard-inputs-ignore-wrapper-defaults` (→ 8.2 падает), `registry-diff-not-selected` (→ 8.2 падает), `merge-pushes-unvalidated-candidate` (→ 8.4 падает), `nightly-does-not-wait` — контрактный тест workflow (`test/nightly-workflow.test.mjs`) по образцу `validate-workflow.test.mjs`.
## 9. Совместимость и откат
Маркеры реюза старого формата ключа не совпадают — один лишний полный прогон после слияния, дальше как обычно. `process.yml` читается конвейером из ветки по умолчанию — **зеркало в `main` только после слияния в `dev`** (правило #454). Откат — revert коммитов инфры; продуктового кода нет.
## 10. Критерии приёмки
- AC1. При `dev`, продвинувшемся за время ревью, в `dev` попадает SHA, для которого есть зелёный Validate; без него слияния нет; `S8-merged` ставится только после push.
- AC2. Повторное движение `dev` между Validate и push не приводит к силовой перезаписи: lease, повтор, лимит 3, затем `S6-in-progress` с комментарием.
- AC3. Дифф с изменившимся patch-id после ребейза не сливается — задача в `S7-code-review`.
- AC4. Ключ реюза каждой job вычисляется из manifest; для каждого представителя §8.1 доказано изменение ключа и классификации.
- AC5. Неизвестный исполняемый вход расширяет прогон до полного набора и виден в summary; лист покрытия §5.5 зелёный.
- AC6. `backend` не зависит от `src/**` — включено последним коммитом после AC4–AC5.
- AC7. Обёртки гардов объявляют `GUARD_INPUTS`; отбор и отпечаток видят обёртки, импорты и фикстуры (§8.2).
- AC8. Новый/изменённый мутант при диффе только по реестру отбирается и бежит.
- AC9. `nightly.yml` красный при красном дочернем Validate.
- AC10. Шесть мутантов §8.5 пойманы штатным раннером; `docs/TESTING.md` и `PROCESS.md` §10.3 описывают точный кандидат и manifest.
## 10.1. UX, модель данных, i18n
Не затрагиваются.
## 10.2. Риски и меры
- Ожидание Validate внутри ревью-job удлиняет слияние на 5–8 мин **только** когда `dev` двигался; лимит ожидания 45 мин с возвратом в `S6`, не зависанием.
- Пуш кандидата в ветку PAT-ом запускает Validate — если PAT когда-нибудь заменят на `GITHUB_TOKEN`, прогон не стартует; шаг ждёт 3 мин и падает с явной ошибкой «прогон не появился», не с ложным зелёным.
- Замыкание импортов расширяет отбор мутантов: правка `serve.mjs` отберёт все смок-свидетели (десятки минут в 3 шардах). Это цена честности; журнал #481 не смягчает — отпечаток тоже меняется. Оценить фактическое время на первом прогоне; при необходимости — четвёртый шард отдельным issue.
- Лист покрытия начнёт краснеть на каждом новом скрипте — намеренно: запись в manifest занимает одну строку.
## 11. Release-артефакты
Не user-visible. `docs/TESTING.md` (реюз, manifest, отбор мутантов), `PROCESS.md` §10.3 (слияние точного кандидата), зеркало `process.yml`/`nightly.yml` в `main` после слияния.
## 12. Затронутые файлы
`scripts/check-inputs.mjs` (новый), `scripts/classify-changes.mjs`, `scripts/gate-reuse.mjs`, `scripts/mutation-gate.mjs` (guardInputs, замыкание, registry diff, шесть мутантов), `scripts/backend-test-guard.mjs`, `scripts/trail-resume-test-guard.mjs` (+ остальные обёртки — `GUARD_INPUTS`), `scripts/merge-candidate.mjs` (новый), `.github/workflows/process.yml`, `.github/workflows/nightly.yml`, `.github/workflows/validate.yml` (summary неизвестных входов), `test/check-inputs.test.mjs` (новый), `test/merge-candidate.test.mjs` (новый), `test/nightly-workflow.test.mjs` (новый), `test/gate-reuse.test.mjs`, `test/classify-changes.test.mjs`, `test/mutation-gate.test.mjs`, `docs/TESTING.md`, `PROCESS.md`.
## 13. Принятые предположения
- Validate на ветке (лёгкий набор + диффозависимые гейты) — достаточная «проверка нового дерева и затронутых взаимодействий» для слияния; тяжёлые гейты — за кандидатом релиза (#479).
- `HP_PROCESS_TOKEN` остаётся PAT (пуш им запускает workflow).
- Порядок реализации: §5 + §8.1/8.3 → §6 + §8.2 → §4 + §8.4 → §7 → AC6 последним. Один issue, одна ветка, одно ревью.
+1
View File
@@ -28,6 +28,7 @@ GitHub Issues и GitHub Projects (v2) остаются единственным
| Issue | ТЗ |
|---|---|
| [#492](https://github.com/Matysh/houseplan-card/issues/492) Точный кандидат интеграции и полный manifest входов selection/reuse | [492-exact-candidate-and-input-manifest.md](492-exact-candidate-and-input-manifest.md) |
| [#490](https://github.com/Matysh/houseplan-card/issues/490) Атомарный recovery и live-состояние сводной панели | [490-summary-recovery-live-state.md](490-summary-recovery-live-state.md) |
| [#486](https://github.com/Matysh/houseplan-card/issues/486) Панель House Plan в боковом меню HA | [486-house-plan-panel.md](486-house-plan-panel.md) |
| [#489](https://github.com/Matysh/houseplan-card/issues/489) Объявленный `data-hp`-контракт для UI и E2E | [489-data-hp-contract.md](489-data-hp-contract.md) |