# План 0 — тест идентичности копий контракта на окне перехода v1 → v2. План реализации > **Для исполнителей-агентов:** обязательный навык — superpowers:subagent-driven-development или superpowers:executing-plans > (в плане 5 задач: по `~/.claude/CLAUDE.md` — executing-plans, Native). Шаги размечены чекбоксами (`- [ ]`). **Goal:** тест идентичности `api/contracts/` в arm-core, tabular-parser и scan-gateway сверяет каждый файл копии с файлом docs своего MAJOR (правило S-53), чтобы `make check` всех трёх оставался зелёным и до, и после слияния docs PR #117 (контракт v2.0). **Architecture:** правило сравнения — один модуль `api/tests/contract_identity.py` (чистые функции над двумя каталогами, только stdlib) и один файл сценарных тестов на синтетических каталогах; оба файла побайтно одинаковы в трёх репозиториях (пакета для общего кода тестов нет, `arm-statements` ещё не существует). Существующий `test_vendored_contracts_are_identical_to_docs` в каждом репозитории сохраняет поиск docs и пропуск, а вместо сравнения `rglob`-наборов вызывает `identity_problems`. Шаг 0 выкладки (`proposal.md`, «Порядок выкладки»): три PR сливаются до docs PR #117. **Tech Stack:** Python 3.11, pytest, ruff, mypy (`make check`), git worktree, `tea`, OpenSpec 1.13. **Spec:** `openspec/changes/unified-statement-normalization/` (репозиторий arm-ka-docs, ветка `feat/116-unified-normalization`): `specs/recognition-contract/spec.md` — требование «Копия контракта в сервисе идентична источнику» и его сценарии; `specs/quality-checks/spec.md` — `make check`; `proposal.md` — «Порядок выкладки», шаг 0; `design.md` — решение S-53. Справка по проверкам — `dev/checks.md` («Внешние ресурсы», «Проверки этого репозитория», «Известные расхождения реализаций»). ## Global Constraints - Пути — абсолютные. Workspace: `/Users/andrey.epifancev/projects/awp-ca` (не git). Git-команды — только `git -C <абсолютный путь worktree>`; cwd оболочки никогда не стоит в основном checkout на `main` (хук guard-main). - Зеркальный workspace плана: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/` — worktree `arm-core/`, `tabular-parser/`, `scan-gateway/` (ветки `feat/-contract-identity-window`), `docs/` — **отсоединённый** worktree arm-ka-docs только для проверок (в нём ничего не коммитится), симлинки `fixtures`, `reference-old-arm`, `CLAUDE.md`, `.claude` на основной workspace (`docs/context/00-workspace-layout.md`, «Worktree вместо веток»). - Одна фича = одна issue = одна ветка = один PR на репозиторий; ветки `feat/-contract-identity-window`; Gitea — `tea … --login home --repo RSHB/` (`RSHB/arm-core`, `RSHB/arm-tabular-parser`, `RSHB/arm-scan-gateway`). - Правило сравнения — дословно из спеки `recognition-contract`: схема копии `<имя>.schema.json` с `$id` `…/v` — с `docs/contracts/<имя>.v.schema.json`, **если такой файл есть**, иначе с основным `<имя>.schema.json`; пример `examples/<имя>.json` — вид по префиксу (`request-` — запрос, `result-message-` — конверт, `result-` — файл результата), `M` — MAJOR копии схемы этого вида, пара — `examples/v/<имя>.json`, если он есть, иначе `examples/<имя>.json`; прочие файлы копии (`<имя>.v.schema.json`, `examples/v/*.json`) — с одноимённым файлом docs, нет его — расхождение; набор: каждой схеме верхнего уровня docs (`contracts/*.schema.json` без `.v`) и каждому примеру `contracts/examples/*.json` соответствует файл копии; файлы docs `*.v.schema.json` и `examples/v/` требуются в копии, только если копия держит этот MAJOR; файлы другого MAJOR не сверяются. У `recognition-request` файла `.v1` нет. - `$id` схем в docs имеют вид `arm-ka/<имя>/v` (`arm-ka/recognition-result/v1`, `…/v2`). - Сверяются только `*.json` (как сейчас); `contracts/statement-code-orders.yaml` в копии сервисов не лежит. - Поиск docs: каталог из `ARM_KA_DOCS`, иначе соседний `docs` рядом с репозиторием; нет каталога `contracts` — `pytest.skip` с текстом `docs недоступен: тест идентичности контракта пропущен (задайте ARM_KA_DOCS или положите docs рядом)`. - Файлы `api/tests/contract_identity.py` и сценарного теста (`api/tests/test_contract_identity.py` в arm-core, `api/tests/unit/test_contract_identity.py` в tabular-parser и scan-gateway) побайтно одинаковы в трёх репозиториях; импорт — `from tests.contract_identity import …` (работает во всех трёх, `rootdir` — `api/`). - Линтер одинаков в трёх репозиториях: ruff `line-length = 120`, `select = ["E", "F", "I", "UP", "B", "PL"]`; `PLR2004` игнорируется в тестах только у scan-gateway — сравнений с числовыми литералами в новых файлах нет. - Копии `api/contracts/**` в этом плане не меняются (они побайтно равны docs main и `.v1`-файлам ветки `feat/116-unified-normalization` — сверено 18.10.2026). - `make check` не требует Docker, сети и запущенной инфраструктуры (`quality-checks`). - Язык сообщений теста, комментариев, коммитов, issue и PR — русский. Коммиты заканчиваются строками `Co-Authored-By: Claude Opus 5.5 ` и `Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W`; PR — строкой `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. - Слияние — только по команде человека; три PR сливаются **до** docs PR #117. Описание PR #117 не трогать. - Находки и решения несут маркеры (`T.`, `C-n`/`I-n`/`M-n`, `R-n`, `O-n`); решения этого плана — `P0-n` (ниже). ### Проверка на двух состояниях docs Состояние **A** — docs `main` (контракт 1.x, файлов `.v1` нет); состояние **B** — ветка `feat/116-unified-normalization` (основные схемы v2.0, `*.v1.schema.json`, `examples/v1/`). Переключается отсоединённый worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs`, так что все три репозитория видят его как `../docs` без переменных окружения, а правки других агентов в `.wt/normalization/docs` проверку не задевают: ```bash git -C /Users/andrey.epifancev/projects/awp-ca/docs fetch origin git -C /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs checkout --detach origin/main # A git -C /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs checkout --detach origin/feat/116-unified-normalization # B ``` После проверки B worktree всегда возвращается в A. Запасной путь для B без переключения — `ARM_KA_DOCS=<каталог> make check` (arm-core и tabular-parser — сейчас, scan-gateway — после Task 3). Признак настоящей проверки, а не пропуска: `cd api && .venv/bin/pytest -rs -k vendored_contracts <файл test_contract.py>` печатает `PASSED`, а не `SKIPPED`. ### Решения плана, принятые без обсуждения (переносятся в `design.md` задачей 4) - **P0-1.** Правило живёт копией в трёх репозиториях (`api/tests/contract_identity.py` + сценарный тест), а не общим пакетом и не скриптом docs. Эталон — копия arm-core (пишется первой); расхождение копий ловит шаг сверки `shasum` задачи 5 и ревью; соседние репозитории в `make check` не читаются. Цена ошибки: правка правила — три PR; разошедшиеся копии до сверки не видны. - **P0-2.** «Копия держит MAJOR N» — в копии есть хотя бы один файл `*.v.schema.json` или `examples/v/*.json`. Тогда все файлы docs `*.v.schema.json` и `examples/v/*.json` обязательны в копии. Цена: ядро, положившее один файл `.v1`, обязано положить все. - **P0-3.** Неопределимая пара — ошибка теста с именем файла, а не пропуск: пример с неизвестным префиксом, схема без `$id` вида `…/v`, пример, вида которого нет среди схем копии. Цена: новый вид примера в docs требует правки правила. - **P0-4.** scan-gateway получает поиск docs через `ARM_KA_DOCS` в тесте идентичности (спека требования называет `ARM_KA_DOCS`); поиск `fixtures` в scan-gateway не меняется. Цена: расхождение в `dev/checks.md` сужается до fixtures. - **P0-5.** tabular-parser и scan-gateway получают OpenSpec change `contract-identity-window` с MODIFIED требования «Копия контракта идентична источнику» (их текущий текст — «байт в байт равны `../docs/contracts/`» и «модели валидируют примеры docs» — на окне неверен). arm-core change не получает: его спеки тест идентичности не описывают, правило — в спеке docs `recognition-contract`. Цена: у arm-core при закрытии нет `openspec archive`. ## Review Focus - **Новый пример верхнего уровня в docs v2 без пары в `examples/v1/`** (например, отдельный пример OCR в PR #117): копия сервиса v1 его не имеет — по правилу набора тест сервиса краснеет на окне, вопреки сценарию «посторонний PR не краснеет» (O-1). Ожидание — сообщение называет файл; тест `test_new_top_level_docs_example_is_reported` (задача 1); задача 5 повторяет проверку B на последнем коммите ветки перед слиянием #117. - **docs удалил `.v1` раньше, чем ядро — свои копии** (нарушение порядка шага 6): тест ядра падает и называет файл `.v1`; тест `test_window_copy_without_docs_pair_fails` (задача 1). - **Схема копии без `$id` или с `$id` без `/v`**: падение с именем файла, а не исключение без контекста или молчаливое сравнение с основным файлом; тест `test_schema_without_major_fails` (задача 1). - **Префикс `result-message-` начинается с `result-`**: конверт и файл результата разных MAJOR не должны спутаться; тест `test_example_kind_by_longest_prefix` (задача 1). - **docs не найден или `ARM_KA_DOCS` указывает не туда**: пропуск с причиной, зелёный `make check` без ложной «идентичности»; в scan-gateway до задачи 3 `ARM_KA_DOCS` игнорировался — тесты `test_docs_contracts_dir_uses_arm_ka_docs` и `test_docs_contracts_dir_defaults_to_sibling_docs` (задача 3). --- ### Task 1: arm-core — правило S-53 и сценарии (эталон копий) Модель исполнителя: sonnet (новый код). **Files:** - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/contract_identity.py` - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract_identity.py` - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract.py:100-124` (раздел «идентичность вендоренных копий»; `_docs_contracts_dir()` через `default_dbml_path()` остаётся) - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/CLAUDE.md:155-157` (строка о копиях контракта) **Interfaces:** - Consumes: — - Produces (`tests/contract_identity.py`, используют задачи 2, 3 и план 3 ядра): - `KIND_BY_PREFIX: tuple[tuple[str, str], ...] = (("request-", "recognition-request"), ("result-message-", "recognition-result-message"), ("result-", "recognition-result"))` — порядок значим, первый подходящий префикс; - `schema_major(schema_path: Path) -> int` — MAJOR из `$id` (`re` `/v(\d+)$`); `ValueError` с текстом `"<имя файла>: в $id нет MAJOR (…/v)"`, если `$id` нет или он не того вида; - `example_kind(file_name: str) -> str` — имя схемы вида; `ValueError` `": вид примера не определён по префиксу (request-, result-message-, result-)"`; - `docs_pair(copy_rel: str, copy_dir: Path, docs_dir: Path) -> str` — относительный POSIX-путь файла docs, с которым сверяется файл копии `copy_rel`; `ValueError` с именем файла, если пара неопределима (в т. ч. «нет схемы `<вид>.schema.json` в копии»); - `held_window_majors(copy_rels: set[str]) -> set[int]` — MAJOR, которые копия держит файлами `.v` (P0-2); - `identity_problems(copy_dir: Path, docs_dir: Path) -> list[str]` — все расхождения, каждое начинается с относительного пути файла; пустой список — копия идентична. - Тексты расхождений (на них опираются тесты): `": отличается от docs/contracts/ — скопируйте заново (cp), не правьте руками"`; `": нет пары в docs/contracts ()"`; `": есть в docs/contracts, нет в api/contracts"`; при `ValueError` от `docs_pair` — `": <текст исключения>"`. - [ ] **Step 1: Зеркальный workspace, issue, ветка, индекс.** cwd — `/Users/andrey.epifancev/projects/awp-ca`. ```bash WT=/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window mkdir -p $WT && cd $WT ln -s ../../fixtures fixtures; ln -s ../../reference-old-arm reference-old-arm; ln -s ../../CLAUDE.md CLAUDE.md; ln -s ../../.claude .claude git -C /Users/andrey.epifancev/projects/awp-ca/docs fetch origin git -C /Users/andrey.epifancev/projects/awp-ca/docs worktree add --detach $WT/docs origin/main git -C /Users/andrey.epifancev/projects/awp-ca/arm-core fetch origin git -C /Users/andrey.epifancev/projects/awp-ca/arm-core worktree add -b tmp/contract-window $WT/arm-core origin/main tea issues create --login home --repo RSHB/arm-core \ --title "Тест идентичности копии контракта по MAJOR (шаг 0 docs#116)" \ --description "Правило S-53 спеки recognition-contract (docs change unified-statement-normalization): копия сверяется с файлом docs своего MAJOR, если он есть, иначе с основным. Шаг 0 выкладки: слить до docs PR #117. План: openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-0-contract-identity.md" git -C $WT/arm-core branch -m feat/-contract-identity-window cd $WT/arm-core && codegraph init && make venv ``` Expected: `git -C $WT/arm-core branch --show-current` → `feat/-contract-identity-window`; `ls $WT/docs/contracts` без `*.v1.schema.json`. `` — номер созданной issue. - [ ] **Step 2: Красный базовый прогон текущего теста на состоянии B.** Переключить docs в B («Проверка на двух состояниях docs»), затем: Run: `cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract.py -k vendored_contracts -rs` Expected: FAIL `набор файлов контракта разошёлся: … только в docs/contracts: ['examples/v1/result-error.json', …, 'recognition-result.v1.schema.json']`. Вернуть docs в A. - [ ] **Step 3: Сценарные тесты** в `api/tests/test_contract_identity.py` (синтетические каталоги в `tmp_path`, реальный docs не нужен). Построители и наборы: ```python """Правило теста идентичности копии контракта (S-53, спека recognition-contract). Файл одинаков в arm-core, tabular-parser и scan-gateway; правка — сразу во всех трёх.""" import json from pathlib import Path import pytest from tests.contract_identity import docs_pair, example_kind, identity_problems, schema_major REQ, MSG, RES = "recognition-request", "recognition-result-message", "recognition-result" def _schema(name: str, major: int) -> str: return json.dumps({"$id": f"arm-ka/{name}/v{major}"}) def _example(major: int, mark: str) -> str: return json.dumps({"schema_version": f"{major}.0", "mark": mark}) def _v1_copy() -> dict[str, str]: # копия сервиса сегодня = docs main до #117 return { f"{REQ}.schema.json": _schema(REQ, 1), f"{MSG}.schema.json": _schema(MSG, 1), f"{RES}.schema.json": _schema(RES, 1), "examples/request-image.json": _example(1, "request"), "examples/result-message-done.json": _example(1, "message"), "examples/result-statement-excel.json": _example(1, "result"), } def _v2_main() -> dict[str, str]: # копия сервиса после шага 5 = docs после #117 без файлов окна; запрос остаётся v1 return { f"{REQ}.schema.json": _schema(REQ, 1), f"{MSG}.schema.json": _schema(MSG, 2), f"{RES}.schema.json": _schema(RES, 2), "examples/request-image.json": _example(1, "request"), "examples/result-message-done.json": _example(2, "message"), "examples/result-statement-excel.json": _example(2, "result"), } def _docs_window() -> dict[str, str]: # docs после #117 = копия ядра после шага 3 v1 = _v1_copy() return { **_v2_main(), f"{MSG}.v1.schema.json": v1[f"{MSG}.schema.json"], f"{RES}.v1.schema.json": v1[f"{RES}.schema.json"], "examples/v1/result-message-done.json": v1["examples/result-message-done.json"], "examples/v1/result-statement-excel.json": v1["examples/result-statement-excel.json"], } def _dirs(tmp_path: Path, copy: dict[str, str], docs: dict[str, str]) -> tuple[Path, Path]: """Пишет файлы в tmp_path/copy и tmp_path/docs (подкаталоги создаёт), возвращает (copy_dir, docs_dir).""" ``` Тесты и их утверждения (`c, d = _dirs(tmp_path, <копия>, )`): | Тест | Копия / docs | Утверждения | |---|---|---| | `test_step0_before_docs_merge_passes` | `_v1_copy()` / `_v1_copy()` | `identity_problems(c, d) == []`; `docs_pair(f"{RES}.schema.json", c, d) == f"{RES}.schema.json"` | | `test_stale_copy_fails_naming_file` | `_v1_copy()` / `_v1_copy()` с `RES`-схемой `json.dumps({"$id": "arm-ka/recognition-result/v1", "new": 1})` | одно расхождение; начинается с `f"{RES}.schema.json"`, содержит `"отличается"` | | `test_service_on_v1_after_v2_merge_passes` | `_v1_copy()` / `_docs_window()` | `[]`; пара `RES`-схемы — `f"{RES}.v1.schema.json"`, `MSG`-схемы — `f"{MSG}.v1.schema.json"` | | `test_request_v1_pairs_with_main_after_v2_merge` | `_v1_copy()` / `_docs_window()` | пара `f"{REQ}.schema.json"` — она же; пара `"examples/request-image.json"` — она же | | `test_previous_major_examples_pair_with_v1_dir` | `_v1_copy()` / `_docs_window()` | пары `examples/result-statement-excel.json` и `examples/result-message-done.json` — `examples/v1/<то же имя>` | | `test_service_on_v2_passes_and_ignores_v1_files` | `_v2_main()` / `_docs_window()` | `[]`; пара `RES`-схемы — `f"{RES}.schema.json"`; пара `examples/result-statement-excel.json` — она же | | `test_core_window_copy_passes` | `_docs_window()` / `_docs_window()` | `[]`; пары `f"{RES}.v1.schema.json"` и `examples/v1/result-statement-excel.json` — они же | | `test_core_window_copy_requires_every_v1_docs_file` | `_docs_window()` без `examples/v1/result-message-done.json` / `_docs_window()` | расхождение `"examples/v1/result-message-done.json: есть в docs/contracts, нет в api/contracts"` | | `test_window_copy_without_docs_pair_fails` | `_docs_window()` / `_v2_main()` (docs удалил `.v1` раньше ядра) | расхождения начинаются с `f"{RES}.v1.schema.json"` и `"examples/v1/result-statement-excel.json"`, содержат `"нет пары"` | | `test_top_level_docs_file_missing_in_copy_fails` | `_v1_copy()` без `examples/request-image.json` / `_v1_copy()` | `"examples/request-image.json: есть в docs/contracts, нет в api/contracts"` в списке | | `test_extra_copy_file_fails` | `_v1_copy()` + `examples/result-new.json` / `_v1_copy()` | расхождение начинается с `"examples/result-new.json"`, содержит `"нет пары"` | | `test_new_top_level_docs_example_is_reported` | `_v1_copy()` / `_docs_window()` + `examples/result-statement-ocr.json` (v2, без пары в `v1/`) | ровно одно расхождение, начинается с `"examples/result-statement-ocr.json"` (O-1) | | `test_unknown_example_prefix_fails` | `_v1_copy()` + `examples/foo.json` / то же | расхождение начинается с `"examples/foo.json"` и содержит `"вид примера"`; `pytest.raises(ValueError, match="вид примера")` для `example_kind("foo.json")` | | `test_schema_without_major_fails` | `_v1_copy()` с `RES`-схемой `json.dumps({"$id": "arm-ka/recognition-result"})` / `_v1_copy()` | есть расхождение, начинающееся с `f"{RES}.schema.json"` и содержащее `"$id"`; `pytest.raises(ValueError)` у `schema_major` | | `test_example_kind_by_longest_prefix` | копия: `_v1_copy()` с `RES`-схемой v2 и `examples/result-statement-excel.json` = `_example(2, "result")`; docs: та же копия + `examples/v1/result-message-done.json` = `_example(1, "message")` | `example_kind("result-message-done.json") == MSG`; пара `examples/result-message-done.json` — `examples/v1/result-message-done.json`; пара `examples/result-statement-excel.json` — она же | - [ ] **Step 4: Убедиться, что сценарии красные.** Run: `cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract_identity.py -q` Expected: ошибка сбора `ModuleNotFoundError: No module named 'tests.contract_identity'`. - [ ] **Step 5: Реализовать `api/tests/contract_identity.py`** (интерфейс — блок Produces). Шапка-докстринг: правило S-53, ссылка на спеку `recognition-contract`, «файл побайтно одинаков в arm-core, tabular-parser и scan-gateway (`api/tests/contract_identity.py`); правка — во всех трёх, сверка — `shasum -a 256`». Только stdlib (`json`, `re`, `pathlib`). Алгоритм: - `docs_pair`: путь вида `<имя>.v.schema.json` или `examples/v/<файл>.json` → он же; `<имя>.schema.json` в корне → `M = schema_major(copy_dir / copy_rel)`, кандидат `<имя>.v.schema.json`, если `(docs_dir / кандидат).is_file()`, иначе `copy_rel`; `examples/<файл>.json` (один уровень) → вид `example_kind(<файл>)`, схема вида `copy_dir / f"{вид}.schema.json"` (нет — `ValueError`), `M = schema_major(…)`, кандидат `examples/v/<файл>`, если он есть в docs, иначе `copy_rel`; любой другой путь → он же. - `identity_problems`: наборы `rglob("*.json")` обоих каталогов (относительные POSIX-пути); для каждого файла копии по порядку — пара (`ValueError` → `": <текст>"` в список, файл дальше не сверяется), пары нет в docs → «нет пары», байты различаются → «отличается»; затем обязательный набор docs: схемы корня без `.v`, `examples/*.json` одного уровня, для каждого `N` из `held_window_majors(<набор копии>)` — `*.v.schema.json` и `examples/v/*.json`; каждого нет в копии — «есть в docs/contracts, нет в api/contracts». Порядок списка — по пути (детерминированный вывод). - [ ] **Step 6: Сценарии зелёные.** Run: `cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract_identity.py -q` Expected: `15 passed`. - [ ] **Step 7: Перевести реальный тест на правило.** В `tests/test_contract.py` тело `test_vendored_contracts_are_identical_to_docs` после проверки `docs_dir.is_dir()` (пропуск и его текст — без изменений) — `problems = identity_problems(CONTRACTS_DIR, docs_dir)`; `assert problems == [], "\n".join(problems)`. Докстринг модуля: «вендоренные схемы `api/contracts/` идентичны `../docs/contracts/` по правилу MAJOR (S-53)». `CLAUDE.md:155-157`: дописать, что копия сверяется с файлом docs своего MAJOR (`*.v.schema.json`, `examples/v/`, если есть), правило — `tests/contract_identity.py`, одинаковый файл в трёх репозиториях. - [ ] **Step 8: `make check` на A и на B.** Run (A): `cd $WT/arm-core && make check`, затем `cd api && .venv/bin/pytest tests/test_contract.py -k vendored_contracts -rs` Expected: `make check` — код 0; тест — `PASSED`. Run (B): переключить docs в B, те же две команды, вернуть docs в A. Expected: код 0; `PASSED` (копия v1 сверилась с `.v1` и `examples/v1/`). - [ ] **Step 9: Commit** ```bash git -C $WT/arm-core add api/tests/contract_identity.py api/tests/test_contract_identity.py api/tests/test_contract.py CLAUDE.md git -C $WT/arm-core commit -m "test(#): тест идентичности копии контракта по MAJOR (S-53, шаг 0 docs#116)" \ -m "Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W" ``` --- ### Task 2: tabular-parser — копия правила, реальный тест, OpenSpec change Модель исполнителя: haiku (код готов в задаче 1, текст спеки — в плане). **Files:** - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/contract_identity.py` (побайтная копия файла arm-core из задачи 1) - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/unit/test_contract_identity.py` (побайтная копия `arm-core/api/tests/test_contract_identity.py`) - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/unit/test_contract.py:104-128` - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/CLAUDE.md:45-46,92` - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/openspec/changes/contract-identity-window/{proposal.md,tasks.md,specs/tabular-recognition-service/spec.md}` **Interfaces:** - Consumes: `identity_problems(copy_dir: Path, docs_dir: Path) -> list[str]` и остальное из задачи 1 (файл копируется, не переписывается). - Produces: — - [ ] **Step 1: Worktree, issue, ветка, индекс** — как задача 1, шаг 1, для `tabular-parser` (`--repo RSHB/arm-tabular-parser`, тот же заголовок и описание; симлинки и docs уже есть). - [ ] **Step 2: Красный базовый прогон на B:** `cd $WT/tabular-parser/api && .venv/bin/pytest tests/unit/test_contract.py -k vendored_contracts -rs` → FAIL «набор файлов контракта разошёлся … только в docs/contracts: ['examples/v1/…']». Вернуть A. - [ ] **Step 3: Скопировать оба файла** `cp` из `$WT/arm-core/api/tests/` (сценарный тест — в `tests/unit/`); проверить `shasum -a 256` — суммы равны суммам arm-core. - [ ] **Step 4: Сценарии зелёные:** `.venv/bin/pytest tests/unit/test_contract_identity.py -q` → `15 passed`. - [ ] **Step 5: Реальный тест** — как задача 1, шаг 7 (`_docs_contracts_dir()` с `ARM_KA_DOCS` остаётся). `CLAUDE.md`: в строках о копиях (`45-46`, `92`) — «идентичность по правилу MAJOR (S-53), `tests/contract_identity.py`». - [ ] **Step 6: OpenSpec change `contract-identity-window`.** `proposal.md` — Why (окно перехода v1 → v2, шаг 0 `proposal.md` docs change `unified-statement-normalization`), What Changes, Non-goals (копии `api/contracts/` и модели не меняются; переход на v2.0 — шаг 5, отдельный change), Impact (`api/tests/`). `tasks.md` — один пункт со ссылкой на этот план. Дельта `specs/tabular-recognition-service/spec.md`: ```markdown ## MODIFIED Requirements ### Requirement: Копия контракта идентична источнику Каждый файл `api/contracts/**/*.json` SHALL быть байт в байт равен своей паре в `../docs/contracts/` (или в `contracts` каталога из `ARM_KA_DOCS`) по правилу спеки docs `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»: схема с `$id` `…/v` — с `<имя>.v.schema.json`, если такой файл есть в docs, иначе с основным; пример — с `examples/v/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы docs другого `MAJOR` не сверяются. pydantic-модели SHALL валидировать все примеры копии `api/contracts/examples/*.json` и порождать JSON, проходящий схему копии. Проверка входит в `make check`. #### Scenario: `make check` - **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`) - **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением #### Scenario: Окно перехода на контракт 2.0 - **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, рядом `recognition-result.v1.schema.json` и `examples/v1/`, а копия сервиса — v1.2 - **THEN** копия сверяется с файлами `.v1` и `examples/v1/` (запрос — с основным файлом), тест проходит #### Scenario: Копия устарела - **WHEN** в docs изменён файл, с которым сверяется копия, а копия — нет - **THEN** тест идентичности падает, сообщение называет файл ``` Run: `cd $WT/tabular-parser && openspec validate contract-identity-window --strict` Expected: `Change 'contract-identity-window' is valid`. - [ ] **Step 7: `make check` на A и на B** — как задача 1, шаг 8 (файл `tests/unit/test_contract.py`). Expected: код 0 и `PASSED` в обоих состояниях. - [ ] **Step 8: Commit** — как задача 1, шаг 9 (файлы задачи; сообщение `test(#): …`). --- ### Task 3: scan-gateway — `ARM_KA_DOCS`, копия правила, реальный тест, OpenSpec change Модель исполнителя: haiku (новый код — три строки поиска docs, его тесты даны). **Files:** - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/contract_identity.py` (побайтная копия) - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/unit/test_contract_identity.py` (побайтная копия) - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/unit/test_contract.py:113-134` - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/CLAUDE.md:87-88` - Create: `/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/openspec/changes/contract-identity-window/{proposal.md,tasks.md,specs/image-recognition-gateway/spec.md}` **Interfaces:** - Consumes: файлы задачи 1. - Produces: `_docs_contracts_dir() -> Path` в `tests/unit/test_contract.py` — `Path(os.environ["ARM_KA_DOCS"]) / "contracts"`, если переменная задана и непуста, иначе `API_DIR.parent.parent / "docs" / "contracts"` (как в tabular-parser). - [ ] **Step 1: Worktree, issue, ветка, индекс** — как задача 1, шаг 1 (`--repo RSHB/arm-scan-gateway`). - [ ] **Step 2: Красный базовый прогон на B:** `cd $WT/scan-gateway/api && .venv/bin/pytest tests/unit/test_contract.py -k vendored_contracts -rs` → FAIL «набор файлов контракта разошёлся …». Вернуть A. - [ ] **Step 3: Тесты поиска docs** в `tests/unit/test_contract.py`: ```python def test_docs_contracts_dir_uses_arm_ka_docs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None: monkeypatch.setenv("ARM_KA_DOCS", str(tmp_path)) assert _docs_contracts_dir() == tmp_path / "contracts" def test_docs_contracts_dir_defaults_to_sibling_docs(monkeypatch: pytest.MonkeyPatch) -> None: monkeypatch.delenv("ARM_KA_DOCS", raising=False) assert _docs_contracts_dir() == API_DIR.parent.parent / "docs" / "contracts" ``` Run: `.venv/bin/pytest tests/unit/test_contract.py -k docs_contracts_dir -q` Expected: `test_docs_contracts_dir_uses_arm_ka_docs` FAIL (возвращается соседний `docs`). - [ ] **Step 4: Поиск docs через `ARM_KA_DOCS`** (`import os`; интерфейс — Produces); текст пропуска — общий из Global Constraints (с «задайте ARM_KA_DOCS»). Run тот же → `2 passed`. - [ ] **Step 5: Скопировать оба файла правила, сверить `shasum`, сценарии зелёные** — как задача 2, шаги 3–4. - [ ] **Step 6: Реальный тест** — как задача 1, шаг 7. `CLAUDE.md:87-88`: «идентичность по правилу MAJOR (S-53, `tests/contract_identity.py`); docs — `../docs` или `ARM_KA_DOCS`». - [ ] **Step 7: OpenSpec change `contract-identity-window`** — как задача 2, шаг 6, дельта `specs/image-recognition-gateway/spec.md` с тем же текстом требования и сценариями; в сценарии `make check` — «при доступном `../docs` (или `ARM_KA_DOCS`)» и «при недоступном docs тест идентичности пропущен с предупреждением». Run: `cd $WT/scan-gateway && openspec validate contract-identity-window --strict` → valid. - [ ] **Step 8: `make check` на A и на B** — как задача 1, шаг 8; дополнительно B через переменную без переключения: `cd $WT/scan-gateway && ARM_KA_DOCS=/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs make check` → код 0, тест идентичности `PASSED`. - [ ] **Step 9: Commit** — как задача 1, шаг 9. --- ### Task 4: docs — `dev/checks.md` и решения P0-n в `design.md` Модель исполнителя: haiku. Репозиторий — worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs`, ветка `feat/116-unified-normalization` (входит в PR #117; описание PR не трогать). **Files:** - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/dev/checks.md` — «Известные расхождения реализаций», пункт `ARM_KA_DOCS`/`ARM_KA_FIXTURES`; абзац о правиле теста идентичности в «Проверки этого репозитория» - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/design.md` — конец раздела `## Decisions made without discussion` **Interfaces:** - Consumes: решения P0-1…P0-5 (раздел «Решения плана»), результат задачи 3 (scan-gateway читает `ARM_KA_DOCS` в тесте идентичности). - Produces: — - [ ] **Step 1: `dev/checks.md`.** В пункте `ARM_KA_DOCS`/`ARM_KA_FIXTURES`: scan-gateway учитывает `ARM_KA_DOCS` в тесте идентичности контракта (с шага 0 #116), `ARM_KA_FIXTURES` — нет; фраза «docs/fixtures нужно класть рядом» — только про fixtures. В абзаце о правиле: «правило реализовано файлом `api/tests/contract_identity.py`, побайтно одинаковым в трёх репозиториях; правка — сразу во всех трёх». - [ ] **Step 2: `design.md`.** Подраздел `### Решения при планировании (18.10.2026)` в конце `## Decisions made without discussion`; P0-1…P0-5 — следующими свободными номерами списка (на 18.10.2026 последний — 97, S-74; перед записью перечитать — параллельные планы дописывают тот же раздел), маркер `P0-n` в тексте сохраняется, формат как у S-n: дата, решение, обоснование, цена ошибки. - [ ] **Step 3: Проверка:** `cd /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs && make check` → код 0. - [ ] **Step 4: Commit и push** (`git -C … pull --rebase` перед push; при запрете хука — `git pull` слиянием): ```bash git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs add dev/checks.md openspec/changes/unified-statement-normalization/design.md git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs commit -m "docs(#116): тест идентичности копии контракта — ARM_KA_DOCS в scan-gateway, решения плана 0 (P0-1…P0-5)" -m "Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W" git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs pull --rebase git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs push ``` --- ### Task 5: сверка трёх копий и итоговая проверка на A и B Модель исполнителя: haiku. Кода не пишет; результат — таблица для описаний трёх PR. **Files:** — (только чтение и прогоны) **Interfaces:** - Consumes: ветки задач 1–3. - Produces: таблица «репозиторий × состояние docs → код `make check`, число тестов, пропуски» для PR. - [ ] **Step 1: Копии правила совпадают.** ```bash shasum -a 256 /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/{arm-core,tabular-parser,scan-gateway}/api/tests/contract_identity.py shasum -a 256 /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract_identity.py \ /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/{tabular-parser,scan-gateway}/api/tests/unit/test_contract_identity.py ``` Expected: в каждой из двух групп три одинаковые суммы. - [ ] **Step 2: A — docs main.** `make check` в трёх worktree; тест идентичности в каждом — `PASSED`, не `SKIPPED`. Expected: три кода 0. - [ ] **Step 3: B — последний коммит `origin/feat/116-unified-normalization`** (после `fetch`). То же. Expected: три кода 0. Красный тест с расхождением «есть в docs/contracts, нет в api/contracts» по новому примеру верхнего уровня — это O-1: остановиться и поднять владельцу, правило не ослаблять. Вернуть docs в A. - [ ] **Step 4: Записать таблицу** в отчёт исполнителя; дальше — superpowers:finishing-a-development-branch: три PR (`tea pulls create --login home --repo RSHB/`), в каждом — «Closes #», ссылка на docs#116 и этот план, «**слить до docs PR #117** (шаг 0 выкладки)», таблица шага 2–3, список рулингов. Перед слиянием #117 шаг 3 повторяется на его последнем коммите. --- ## Самопроверка плана - Покрытие спеки: правило схем, примеров, прочих файлов и набора — задача 1 (шаг 5) и сценарии шага 3; сценарии требования «Копия устарела», «Сервис ещё пишет 1.x», «Сервис перешёл на 2.0», «Запрос v1.0 после слияния v2.0», «Примеры прежнего MAJOR», «Шаг 0 до слияния docs», «Ядро на окне перехода» — тесты таблицы задачи 1; `../docs` или `ARM_KA_DOCS`, пропуск с предупреждением — задачи 1–3 (scan-gateway — задача 3); «всех трёх потребителей до слияния v2.0» — задачи 1–3, порядок — задача 5, шаг 4; удаление MAJOR 1 «сначала ядро» — `test_window_copy_without_docs_pair_fails`; `quality-checks` — `make check` задач 1–3, 5; `dev/checks.md` — задача 4. - Интерфейсы: имена `identity_problems`, `docs_pair`, `example_kind`, `schema_major`, `held_window_majors`, `KIND_BY_PREFIX` одинаковы в задачах 1–3 и в Review Focus; сценарный тест импортирует только `docs_pair`, `example_kind`, `identity_problems`, `schema_major`. - Счёт тестов сценариев — 15 (таблица задачи 1), он же в ожиданиях шагов 6 (задача 1) и 4 (задача 2).