# План 2 — `sync-statements` и `statements-check`: копия пакета `arm_statements` в хостах. План реализации > **Для исполнителей-агентов:** обязательный навык — superpowers:subagent-driven-development (в плане 9 задач: по > `~/.claude/CLAUDE.md` — SDD, контроллер — фоновый сабагент на модели sonnet; исполнители задач — sonnet: готового кода > в плане нет, есть интерфейсы, сценарии тестов и правила спеки; финальное ревью каждой ветки — opus). Шаги размечены > чекбоксами (`- [ ]`). **Goal:** скрипт `docs/scripts/sync-statements.sh` раскладывает пакет `arm_statements` из репозитория `arm-statements` копией в `api/arm_statements/` хостов (arm-core, tabular-parser, scan-gateway) и проверяет свежесть копии (`--check`: коды `0` / `10` / `20` / `2`); три хоста подключают копию (линтеры, `testpaths`, `statements-check` в `make check-full`). **Architecture:** по образцу `scripts/sync-web.sh`: bash-скрипт ведёт git-часть (источник, worktree, issue, PR), а разбор `pyproject.toml` и YAML делает маленький помощник `scripts/statements_check.py` (stdlib `tomllib` + PyYAML). Точка синхронизации не ищется по истории, как у `sync-web`, — она записана в `api/arm_statements/VERSION`: копия сравнивается с деревом `arm_statements/` коммита из `VERSION` (без самого `VERSION`); отличие — прямая правка (`20`). Три режима раскладки: обычный (новый worktree хоста, issue, проверки, коммит, PR), `--dry-run` (worktree и раскладка без issue и PR) и `--in-place` (раскладка в рабочее дерево существующего worktree без git-операций — для подключения хоста и для PR фичи, которому нужна новая версия пакета). Тест — `scripts/test_sync_statements.sh` на синтетических репозиториях без сети, `tea` и Docker (как `test_sync_web.sh`). **Tech Stack:** bash (совместимость с bash 3.2 macOS), git, BSD/GNU `diff`/`tar`, Python 3.11 (`tomllib`), PyYAML 6.0.3, GNU make, `tea` (Gitea), uv (окружения хостов). **Spec:** `openspec/changes/unified-statement-normalization/` (arm-ka-docs, ветка `feat/116-unified-normalization`): `specs/statement-normalization/spec.md` — требования «Пакет общего кода `arm_statements` и его копии» и «`sync-statements` — копии пакета в потребителях» со сценариями; `specs/quality-checks/spec.md` — `make check` и `make check-full`; `proposal.md` — «Порядок выкладки»; `dev/checks.md` — «Копия общего кода `arm_statements` и `sync-statements`»; `context/03-architecture.md` — «Общий код нормализации ББ/ОФР». Справка — `design.md` S-27, S-30, S-58, S-70, Р-1. План пакета — `plans/2026-10-18-plan-1-arm-statements.md` (структура репозитория, `VERSION`, P1-1…P1-3, O-1…O-4). ## Предусловия - **П-1 (docs, задачи 1–5).** Контроллер: issue в `RSHB/arm-ka-docs` (`tea issues create --login home --repo RSHB/arm-ka-docs`, заголовок «Скрипт sync-statements: копия пакета arm_statements в хостах (#116)», ссылка на #116 и этот план); worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/sync-statements/docs` на временной ветке от `origin/main`, затем ветка `feat/-sync-statements`; в worktree — `codegraph init`; `make venv`. `$D` = этот путь. Ветка **не** `feat/116-unified-normalization`: по `proposal.md` (шаг 1) PR скрипта сливается раньше docs PR #117. - **П-2 (задача 6).** Правки спеки, `design.md` и раздела «Копия общего кода» `dev/checks.md` — в ветке `feat/116-unified-normalization` (worktree `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs`), пока PR #117 открыт; если он уже слит — в ветке П-1 до её PR. - **П-3 (хосты, задачи 7–9).** Старт — после слияния PR плана 1 в `RSHB/arm-statements` `main` (в `origin/main` есть `arm_statements/` и `pyproject.toml`) и PR задач 1–5 в docs `main`. Чекаут `arm-statements` — `/Users/andrey.epifancev/projects/awp-ca/arm-statements` (П-2 плана 1). Зеркальный workspace `/Users/andrey.epifancev/projects/awp-ca/.wt/statements-adopt/` (`$W`): worktree хостов `$W/arm-core`, `$W/tabular-parser`, `$W/scan-gateway` (каждый — своя issue и ветка `feat/-arm-statements-copy` от `origin/main`), `$W/docs` — **отсоединённый** worktree arm-ka-docs на `origin/main` (в нём ничего не коммитится), симлинки `fixtures`, `reference-old-arm`, `CLAUDE.md`, `.claude` на основной workspace (`context/00-workspace-layout.md`). В каждом worktree хоста — `.bounded` (bounded-задача: без OpenSpec change и плана хоста) и `codegraph init`. - **П-4.** Docs PR #117 для задач 7–9 не обязателен: без `contracts/statement-code-orders.yaml` в docs `statements-check` даёт `10` (P2-9), это вписывается в описание PR. ## Global Constraints - Копия лежит в `api/arm_statements/` хоста; маркер — `api/arm_statements/VERSION`: полный 40-символьный хэш коммита `arm-statements` и перевод строки; пишет его только скрипт. В источнике файла `VERSION` нет, в сравнении он не участвует. - Коды `--check` — дословно спека: `0` — копия равна дереву `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов равна `docs/contracts/statement-code-orders.yaml`; `10` — копия равна дереву коммита из `VERSION`, но в `--ref` есть более новый, или копия перечня отличается от файла docs (называется файл); `20` — прямая правка копии, `VERSION` нет в истории источника, зависимость пакета (`[project].dependencies` его `pyproject.toml`) отсутствует в зависимостях хоста, или (только `arm-core`) код `api/rules/statement_lines.yaml` отсутствует в перечне своей редакции **копии пакета**; `2` — нет checkout источника или хоста, неизвестный аргумент. Файлы и коды расхождения печатаются. Если причин несколько — печатаются все, код — наибольший по тяжести (`20` > `10` > `0`). - Источник по умолчанию — `arm-statements` рядом с workspace: кандидаты `$HERE/../../../../arm-statements` (скрипт в `.wt/<задача>/docs`), затем `$HERE/../../arm-statements`; ссылка — `origin/main`. Хост — `--host arm-core|tabular-parser|scan-gateway` (каталог `$WS/<имя>`) или `--host-dir `. Docs — каталог, где лежит скрипт (`$HERE/..`), или `--docs `. - Gitea-репозитории хостов: `arm-core` → `RSHB/arm-core`, `tabular-parser` → `RSHB/arm-tabular-parser`, `scan-gateway` → `RSHB/arm-scan-gateway`. `tea` — флаги `--login home --repo …` **после** подкоманды (arm-ka-docs#110). - `statements-check` хоста входит в `make check-full` и **не** входит в `make check` и пайплайн платформы. - Скрипты совместимы с bash 3.2 (macOS): без `mapfile`, `declare -A`, `${var,,}`; `set -euo pipefail`; BSD и GNU `diff`/`tar`/`sed` (без `sed -i`). - Конфиг линтеров пакета в копию не кладётся (P1-1): к копии применяется конфиг хоста. Правки копии под хост запрещены. - Пины инструментов во всех хостах — как в пакете: `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906`. Зависимость пакета `PyYAML==6.0.3` уже есть в `api/requirements.txt` всех трёх хостов (проверено 18.10.2026); `api/requirements.txt` хостов («платформенные зависимости, не меняем») этим планом не меняется. - Язык сообщений скриптов, комментариев, коммитов, 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)`. Слияние — только по команде человека. - Находки и решения несут маркеры (`T.`, `C-n`/`I-n`/`M-n`, `R-n`, `O-n`); решения этого плана — `P2-n` (раздел в конце); задача 6 переносит их в `design.md`. ## Review Focus 1. **Кэши в копии после `make check` хоста** — `__pycache__/`, `*.pyc`, `.pytest_cache/`, `.mypy_cache/`, `.ruff_cache/`, `.DS_Store` внутри `api/arm_statements/` не делают копию «прямой правкой»: `--check` = `0`. Тест — задача 2. 2. **`.gitignore` хоста прячет файл пакета** — у хостов шаблонный `.gitignore` (`lib/`, `build/`, `out`, `dist`, `coverage`, `logs`, `*.spec`, `*.log`…): разложенный файл есть на диске, но в коммит не попадёт, и в чужом клоне `--check` даст `20`. Раскладка (`--in-place`, обычный режим) завершается кодом 1 и называет игнорируемые файлы. Тест — задача 3. 3. **Добавленный и удалённый файл копии**, а не только изменённый, — `20` с именем файла. Тест — задача 2. 4. **Запуск из `Makefile` хоста с `--host-dir .`** из worktree `.wt/<задача>/<хост>`: относительный путь, имя хоста по `origin` (`arm-tabular-parser` → `tabular-parser`), проверка словаря имён — только у `arm-core` (у scan-gateway тоже есть `api/rules/statement_lines.yaml`, его не проверять). Тест — задача 2. 5. **`VERSION` с ветки фичи `arm-statements`** (коммит есть в источнике, но не предок `--ref`) — `20` «чужая версия», а не `10`. Тест — задача 2. --- ## Структура файлов | Файл | Репозиторий | Ответственность | |---|---|---| | `scripts/statements_check.py` | docs | помощник: зависимости пакета против `requirements.txt` хоста; коды словаря имён против перечня | | `scripts/sync-statements.sh` | docs | разбор аргументов, поиск источника и хоста, `--check`, раскладка (`--in-place`, `--dry-run`, обычный режим) | | `scripts/test_sync_statements.sh` | docs | сценарии на синтетических репозиториях | | `Makefile` | docs | `venv` (+ PyYAML), `test-sync-statements` (в `check`), `sync-statements`, `statements-check` | | `dev/checks.md` | docs | таблица «Проверки этого репозитория» (ветка П-1); раздел «Копия общего кода» (задача 6) | | `api/arm_statements/**`, `api/pyproject.toml`, `api/pytest.ini`, `api/requirements-dev.txt`, `Makefile`, `CLAUDE.md` | хосты | подключение копии (задачи 7–9) | --- ### Task 1: Помощник `statements_check.py` и каркас теста **Files:** - Create: `$D/scripts/statements_check.py`, `$D/scripts/test_sync_statements.sh` - Modify: `$D/Makefile` (`venv`, `PY`, новая цель `test-sync-statements`) **Interfaces:** - Produces (`scripts/statements_check.py`, только stdlib + `yaml`, Python ≥ 3.11): - `normalize_name(name: str) -> str` — PEP 503: `re.sub(r"[-_.]+", "-", name).lower()`; - `requirement_name(line: str) -> str | None` — имя из строки требования по `^[A-Za-z0-9][A-Za-z0-9._-]*` (экстры `[crypto]`, версии, маркеры отбрасываются); пустая строка, комментарий, строка с `-` в начале (`-r`, `--index-url`) — `None`; - `missing_dependencies(pyproject_text: str, requirements_text: str) -> tuple[list[str], list[str]]` — (имена из `[project].dependencies` пакета, которых нет среди имён `requirements.txt`; примечания «версии различаются: пакет `PyYAML==6.0.3`, хост `PyYAML==6.0.4`»); нет `[project].dependencies` — пустой список; - `codes_outside_orders(lines_doc: object, orders_doc: object) -> list[str]` — для каждой записи `editions[] = {id, forms: {ФОРМА: [{code, name}]}}` словаря имён: код, которого нет в объединении вариантов `orders.editions[id][ФОРМА]` (`full` ∪ `simplified`) перечня; сообщение `код 1120 формы BALANCE редакции fsbu4 нет в перечне копии пакета`; редакции или формы нет в перечне — сообщение по каждому её коду; - CLI `main(argv: list[str]) -> int`: `deps ` и `lines `; печатает находки по строке, примечания — с префиксом `примечание: `; код `0` — нет находок, `20` — есть, `1` — файл не читается или не той формы (сообщение об ошибке в stderr), `2` — неверные аргументы. - Produces (`Makefile`): `PY` — без изменений смысла; `venv` ставит `jsonschema pyyaml`; цель `test-sync-statements: STATEMENTS_PY=$(PY) scripts/test_sync_statements.sh`. Строки `PY` (комментарий), `venv` и комментарий над ней — **дословно как в ветке `feat/116-unified-normalization`** (иначе конфликт при слиянии #117): «`# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML)`», «`# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml)`», «`uv pip install --python $(VENV)/bin/python jsonschema pyyaml`». - Produces (`scripts/test_sync_statements.sh`): каркас как у `test_sync_web.sh` — `WORK=$(mktemp -d)` с `trap`, `check <0|1> <описание>`, `put <путь> [текст]`, `commit_push `; интерпретатор — `PY=${STATEMENTS_PY:-python3}`, в начале проверка `"$PY" -c 'import tomllib, yaml'` (нет — `exit 1` с подсказкой `make venv`); раздел «помощник» в начале, следующие задачи дописывают свои разделы; в конце `ok: sync-statements.sh …`. - [ ] **Step 1: Write the failing test** — раздел «помощник» в `test_sync_statements.sh`: файлы в `$WORK/h/`. | Проверка | Вход | Ожидание | |---|---|---| | deps: есть | pyproject `[project]\ndependencies = ["PyYAML==6.0.3"]`; requirements `aiohttp==3.14.3\nPyYAML==6.0.3` | код 0, пустой вывод | | deps: регистр и `_` | requirements `pyyaml==6.0.3` | код 0 | | deps: нет | requirements `aiohttp==3.14.3\n# PyYAML==6.0.3\n-r other.txt` | код 20, в выводе `PyYAML` | | deps: другая версия | requirements `PyYAML==6.0.4` | код 0, в выводе `примечание:` и `6.0.4` | | deps: без `[project]` | pyproject `[tool.ruff]\nline-length = 120` | код 0 | | deps: экстры хоста | pyproject `dependencies = ["PyJWT==2.8.0"]`, requirements `PyJWT[crypto]==2.8.0` | код 0 | | lines: все коды есть | словарь: `66n` `BALANCE` `1110`, `1100`; `fsbu4` `BALANCE` `1110`; перечень: обе редакции `BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}`, `INCOME: {full: ["2110"], simplified: ["2110"]}` | код 0 | | lines: код вне перечня | в `fsbu4` `BALANCE` словаря добавлен `1120` | код 20, в выводе `1120` и `fsbu4` | | lines: код только в `simplified` | перечень `fsbu4 BALANCE: {full: ["1110"], simplified: ["1100"]}`, словарь `fsbu4` `1100` | код 0 | | битый YAML | словарь `editions: [` | код 1 | | аргументы | без аргументов | код 2 | Синтетический перечень — в форме `contracts/statement-code-orders.yaml` (`version: 1`, `repeats`, `editions: {66n: {BALANCE: {full, simplified}, INCOME: …}, fsbu4: …}`); словарь — в форме `arm-core/api/rules/statement_lines.yaml` (`editions: - {id: 66n, forms: {BALANCE: [{code: "1110", name: "…"}]}}`). Запуск — `"$PY" "$HERE/statements_check.py" …`. - [ ] **Step 2: Run test to verify it fails** Run: `cd $D && make venv && make test-sync-statements` Expected: FAIL — `statements_check.py` не найден (строки `FAIL:`), код 1. - [ ] **Step 3: Implement** `scripts/statements_check.py` по интерфейсу; `tomllib.loads`, `yaml.safe_load`; docstring модуля — назначение и ссылка на спеку `statement-normalization`. Правка `Makefile` (П-1 ветки — `origin/main`: `.PHONY` дополнить `test-sync-statements`; `check` пока не трогать). - [ ] **Step 4: Run test to verify it passes** Run: `cd $D && make test-sync-statements` Expected: все строки `ok:`, код 0. - [ ] **Step 5: Commit** ```bash git -C "$D" add scripts/statements_check.py scripts/test_sync_statements.sh Makefile git -C "$D" commit -m "feat(scripts): помощник statements_check.py — зависимости пакета и коды словаря имён против перечня (#)" ``` --- ### Task 2: `sync-statements.sh --check` **Files:** - Create: `$D/scripts/sync-statements.sh` - Modify: `$D/scripts/test_sync_statements.sh` (раздел «--check») **Interfaces:** - Consumes: CLI `statements_check.py deps|lines` (задача 1). - Produces (`scripts/sync-statements.sh`): - Аргументы: `--check`, `--in-place`, `--dry-run`, `--host <имя>`, `--host-dir `, `--ref ` (источник, по умолчанию `origin/main`), `--host-ref ` (база worktree хоста, по умолчанию `origin/main`), `--source `, `--docs `, `--core-dir ` (по умолчанию `$WS/arm-core`), `--wt-root ` (по умолчанию `$WS/.wt`), `-h`. `--check` важнее `--dry-run` и `--in-place` при любом порядке (как у `sync-web.sh`); `--in-place` вместе с `--dry-run` — код 2; ни `--host`, ни `--host-dir` — код 2; неизвестное имя `--host` — код 2. - Шапка-комментарий в стиле `sync-web.sh`: назначение, точка синхронизации (`VERSION`), режимы, коды, `-h` печатает её. - Имя хоста: `--host`; иначе по `git -C remote get-url origin` — последний компонент без `.git`: `arm-core` → `arm-core`, `arm-tabular-parser` → `tabular-parser`, `arm-scan-gateway` → `scan-gateway`; иначе имя каталога; не из трёх — код 2. - Интерпретатор помощника: `STATEMENTS_PY` → `$DOCS/.venv/bin/python` → `$HOST/api/.venv/bin/python` → `python3`; первый, у которого `import tomllib, yaml` проходит; ни одного — код 2 с подсказкой `make -C docs venv`. - `git -C "$SRC" fetch -q origin`; ошибка fetch — предупреждение «fetch не удался, сравниваю с локальным », работа продолжается. `--ref` не разрешается в источнике — код 2. - Функция `evaluate ` — печатает находки и выставляет `EVAL_CODE` (0/10/20); используется задачами 3–4 для проверки дерева после раскладки и дерева `--host-ref`. - Итоговая строка `--check` (её ищут тесты и `Makefile` хоста): `statements-check <хост>: 0 — копия синхронна с arm-statements@ ()`; `statements-check <хост>: 10 — ПРЕДУПРЕЖДЕНИЕ: <причины через «; »>`; `statements-check <хост>: 20 — расхождение, копию не принимать`; код возврата = число в строке. `evaluate` проверяет по порядку (печатает всё найденное): | # | Условие | Сообщение (суть) | Код | |---|---|---|---| | 1 | нет каталога `api/arm_statements/` | «копии нет: разложите `sync-statements.sh --in-place --host-dir `» | 20 (остальное не проверяется) | | 2 | нет `VERSION`, не 40 hex, коммита нет в источнике | «VERSION … нет в истории arm-statements» (+ «сделайте fetch», если fetch не удался) | 20 (3–4 не проверяются) | | 3 | коммит `VERSION` не предок `--ref` | «чужая версия: не из истории » | 20 | | 4 | файлы копии ≠ `git archive arm_statements` (извлечь во временный каталог; `diff -rq` с исключениями `VERSION`, `__pycache__`, `*.pyc`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.DS_Store`) | «прямая правка копии:» и по строке на файл — путь относительно `arm_statements/` (изменён / только в копии / только в источнике) | 20 | | 5 | `statements_check.py deps ` = 20 | его вывод | 20 | | 6 | хост `arm-core` и есть `api/rules/statement_lines.yaml`: `statements_check.py lines <словарь> <копия>/rules/statement_code_orders.yaml` = 20 | его вывод | 20 | | 7 | нет `$DOCS/contracts/statement-code-orders.yaml` или он не равен байтово `<копия>/rules/statement_code_orders.yaml` | «перечень кодов в docs отличается от копии: contracts/statement-code-orders.yaml» (или «нет в docs») | 10 | | 8 | `VERSION` ≠ `git rev-parse ` | «копия отстала: , — », список `git log --format=' %h %s' ..` | 10 | Код 1 помощника — код скрипта 1 с его сообщением. Режим `--check` читает рабочее дерево `--host-dir` (незакоммиченная правка тоже `20`); `requirements_file` — `/api/requirements.txt`. - [ ] **Step 1: Write the failing test** — раздел «--check» в `test_sync_statements.sh`. Подготовка: `arm-statements.git` (bare) + клон `$WORK/arm-statements`, ветка `main`: `pyproject.toml` (`[project]`, `name = "arm-statements"`, `dependencies = ["PyYAML==6.0.3"]`), `README.md`, `arm_statements/__init__.py`, `arm_statements/steps.py`, `arm_statements/rules/statement_code_orders.yaml` (синтетический перечень задачи 1), `arm_statements/tests/__init__.py`, `arm_statements/tests/test_steps.py`; коммит S1, push. `$WORK/docs/contracts/ statement-code-orders.yaml` — байтовая копия перечня S1 (не git). Хосты — bare + клон: `$WORK/arm-core` (origin `arm-core.git`; `api/requirements.txt` с `PyYAML==6.0.3`; `api/rules/statement_lines.yaml` — словарь задачи 1), `$WORK/tabular-parser` (origin `arm-tabular-parser.git`), `$WORK/scan-gateway` (origin `arm-scan-gateway.git`; `api/rules/statement_lines.yaml` с кодом `9999` в `fsbu4`). В каждый хост копия кладётся вручную в тесте: `git -C "$SRC" archive S1 arm_statements | tar -x -C /api` и `VERSION` = полный хэш S1; коммит, push. `COMMON=(--source "$WORK/arm-statements" --docs "$WORK/docs" --wt-root "$WORK/wt" --core-dir "$WORK/arm-core")`. | Проверка | Действие | Ожидание | |---|---|---| | синхронно | `--check --host-dir $WORK/arm-core` | 0, вывод содержит `statements-check arm-core: 0` | | имя по origin, словарь не у scan-gateway | `--check --host-dir $WORK/scan-gateway` | 0, вывод содержит `scan-gateway`, нет `9999` | | `--host-dir .` из каталога хоста | `(cd $WORK/tabular-parser && "$SYNC" --check --host-dir . "${COMMON[@]}")` | 0, `statements-check tabular-parser: 0` | | кэши | в копии tabular-parser `__pycache__/steps.cpython-311.pyc`, `tests/.pytest_cache/x`, `.mypy_cache/x`, `.DS_Store` | 0 | | изменён файл | дописать строку в `api/arm_statements/steps.py` arm-core (без коммита) | 20, в выводе `steps.py` и `прямая правка` | | добавлен файл | новый `api/arm_statements/extra.py` (правку `steps.py` откатить) | 20, в выводе `extra.py` | | удалён файл | удалить `api/arm_statements/tests/test_steps.py` | 20, в выводе `test_steps.py` | | нет `VERSION` | удалить `VERSION` | 20 | | неизвестный `VERSION` | `VERSION` = `0000…` (40 нулей) | 20 | | чужая версия | в источнике ветка `feat/x` с коммитом F (правка `steps.py`), push; копия arm-core = дерево F, `VERSION` = F | 20, в выводе `чужая версия` | | нет зависимости | arm-core: `api/requirements.txt` без PyYAML (копия = S1) | 20, в выводе `PyYAML` | | код словаря вне перечня | arm-core: в `fsbu4` словаря `1120` | 20, в выводе `1120` и `fsbu4` | | перечень в docs новее | `$WORK/docs/contracts/statement-code-orders.yaml` дописан `# новый код` | 10, в выводе `contracts/statement-code-orders.yaml`; затем вернуть файл | | перечня нет в docs | `--docs $WORK/docs-empty` (пустой каталог) | 10 | | отстала | в источнике коммит S2 (правка `README.md`), push | 10, в выводе `копия отстала` и тема S2 | | правка + отставание | правка `steps.py` arm-core при S2 | 20 (не 10) | | нет источника | `--source $WORK/nope` | 2 | | нет хоста | `--host-dir $WORK/nope` | 2 | | неизвестный аргумент | `--bogus` | 2 | | нет `--host` / `--host-dir` | только `--check` | 2 | | `--in-place --dry-run` | оба флага | 2 | Каждая проверка после себя возвращает хосты к копии S1 (`git -C checkout -- . && git -C clean -fdq`). Перед «отстала» и после неё порядок проверок не меняется: проверки с S2 идут последними. - [ ] **Step 2: Run test to verify it fails** Run: `cd $D && make test-sync-statements` Expected: FAIL — строки `FAIL:` раздела «--check» (скрипта нет). - [ ] **Step 3: Implement** разбор аргументов, поиск workspace/источника/хоста (как в `sync-web.sh`: `WS` по кандидатам `$HERE/../../../..` и `$HERE/../..`, где рядом есть `arm-statements/.git`), выбор интерпретатора, `evaluate` и режим `--check`. Режимы раскладки пока завершаются `die "режим не реализован" 1`. - [ ] **Step 4: Run test to verify it passes** Run: `cd $D && make test-sync-statements` Expected: все строки `ok:`, код 0. - [ ] **Step 5: Commit** ```bash git -C "$D" add scripts/sync-statements.sh scripts/test_sync_statements.sh git -C "$D" commit -m "feat(scripts): sync-statements.sh --check — свежесть копии arm_statements в хосте, коды 0/10/20/2 (#)" ``` --- ### Task 3: Раскладка `--in-place` **Files:** - Modify: `$D/scripts/sync-statements.sh`, `$D/scripts/test_sync_statements.sh` (раздел «--in-place») **Interfaces:** - Consumes: `evaluate` (задача 2). - Produces: функция `lay_copy ` — удаляет `api/arm_statements/` целиком, извлекает `git -C "$SRC" archive arm_statements | tar -x -C /api`, пишет `VERSION` (`<полный хэш>\n`); затем две защиты, любая — код 1 со списком: - игнорируемые файлы: `git -C ls-files --others --ignored --exclude-standard -- api/arm_statements` после исключения кэшей (`__pycache__`, `*.pyc`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`) непуст — «файлы копии игнорируются .gitignore хоста, в коммит не попадут:»; - содержимое, на котором остановится перенос в контур (`prepare-contour-snapshot.sh`, шаг 8): `grep -rIn "/Users/\|/home/[a-z]\|git.aepif.ru\|uv venv\|uv pip"` по копии — «копия сломает снимок для контура:». - Режим `--in-place` (хост — `--host` или `--host-dir`; рабочее дерево, без git-операций): 1. если копия есть — `evaluate` против её `VERSION`; код 20 по условиям 1–4 (правка, чужая версия) — отказ `20`, ничего не пишется; 2. зависимости `--ref` (`pyproject.toml` коммита `--ref`) против `api/requirements.txt` хоста и (только `arm-core`) словарь имён против перечня коммита `--ref` (`git show :arm_statements/rules/statement_code_orders.yaml`) — находка — отказ `20`, ничего не пишется; 3. копия уже на `--ref` и равна ему — «раскладывать нечего», переход к шагу 5; 4. `lay_copy`; 5. `evaluate` итога; печать итоговой строки как у `--check`; код возврата = `EVAL_CODE`. Подсказка в конце: `закоммитьте api/arm_statements отдельным коммитом: git -C add api/arm_statements`. - [ ] **Step 1: Write the failing test** — раздел «--in-place». Новый хост без копии: bare `$WORK/fresh/arm-tabular-parser.git` + клон `$WORK/fresh/tabular-parser` (имя хоста выводится из origin — `tabular-parser`), `api/requirements.txt` с `PyYAML==6.0.3`, коммит, push; ниже он — «fresh». | Проверка | Действие | Ожидание | |---|---|---| | первичная раскладка | хост без `api/arm_statements/`, `--in-place --host-dir --ref origin/main` (S2) | 0; `VERSION` = полный хэш S2; `diff -r` копии (без `VERSION`) с `git archive S2 arm_statements` пуст; `git status` хоста не менялся вне `api/arm_statements` | | повтор | ещё раз | 0, в выводе `раскладывать нечего` | | обновление | копия arm-core на S1, `--in-place --host-dir $WORK/arm-core` | 0, `VERSION` = S2 | | отказ при правке | в копии arm-core правка `steps.py`, `--in-place` | 20; `steps.py` с правкой на месте, `VERSION` не изменился | | отказ без зависимости | `api/requirements.txt` fresh без PyYAML | 20; копия не тронута | | `.gitignore` прячет файл | в источнике коммит S3: `arm_statements/tests/golden/case.json`; в fresh `.gitignore` строка `*.json`; `--in-place` | 1; в выводе `case.json` и `.gitignore` | | содержимое для контура | в источнике коммит S4: строка `# /Users/someone` в `steps.py`; `--in-place` в fresh без `*.json` в `.gitignore` | 1; в выводе `/Users/` | Коммиты S3/S4 в источнике делаются на отдельной ветке `test-guards` и передаются через `--ref origin/test-guards`, чтобы `origin/main` (S2) остался для задачи 4. - [ ] **Step 2: Run test to verify it fails** Run: `cd $D && make test-sync-statements` Expected: FAIL — строки `FAIL:` раздела «--in-place». - [ ] **Step 3: Implement** `lay_copy` и режим `--in-place` по интерфейсу. - [ ] **Step 4: Run test to verify it passes** Run: `cd $D && make test-sync-statements` Expected: все строки `ok:`, код 0. - [ ] **Step 5: Commit** ```bash git -C "$D" add scripts/sync-statements.sh scripts/test_sync_statements.sh git -C "$D" commit -m "feat(scripts): sync-statements.sh --in-place — раскладка копии в рабочее дерево хоста с защитой от .gitignore и путей для контура (#)" ``` --- ### Task 4: Обычный режим и `--dry-run`: worktree, issue, проверки хоста, PR **Files:** - Modify: `$D/scripts/sync-statements.sh`, `$D/scripts/test_sync_statements.sh` (раздел «синхронизация») **Interfaces:** - Consumes: `evaluate` (задача 2), `lay_copy` (задача 3). - Produces — обычный режим (без `--check`/`--in-place`), по шагам `sync-web.sh`: 1. `fetch` источника и хоста; `TARGET` = `git -C "$SRC" rev-parse `, `T7` = 7 символов. 2. Состояние хоста на `--host-ref`: нет `:api/arm_statements` — код 2 «копии нет: подключение хоста — вручную, `--in-place` в worktree задачи подключения (dev/checks.md)». Иначе — дерево `git archive api` во временный каталог и `evaluate` по нему (`requirements` — оттуда же); `20` — отказ `20` с находками, worktree не создаётся. 3. Зависимости и словарь имён против `TARGET` (как шаг 2 `--in-place`) — находка — отказ `20`. 4. `VERSION` на `--host-ref` = `TARGET` — «копия уже на , раскладывать нечего», код 0 (предупреждение о перечне кодов, если есть). 5. «Сначала ядро»: для хоста ≠ `arm-core` — `VERSION` в `@origin/main`; его нет, или он ≠ `TARGET` и `TARGET` не его предок — предупреждение `ядро (arm-core@origin/main) ещё не на : по порядку выкладки сначала ядро` (не блокирует; строка уходит и в описание PR). 6. Worktree `$WT_ROOT/statements-sync-/<хост>` на `tmp/statements-sync-` от `--host-ref`; существует worktree или ветка — код 2; `touch .bounded`. 7. Не `--dry-run`: issue (`tea issues create --login home --repo --title "arm_statements: копия пакета arm-statements@" --description …` — версия копии было/стало, коммиты `..`, «bounded, выполнена `docs/scripts/sync-statements.sh`»); ветка переименовывается в `feat/-statements-sync-`. 8. `lay_copy "$WT" "$TARGET"`; `evaluate` по worktree — допустимы `0` и `10` только по перечню кодов (условие 7), иначе код 1, worktree оставлен. 9. `--dry-run`: печать «dry-run: issue, проверки, коммит и PR не выполнялись» и команды удаления worktree и ветки; код 0. 10. Проверки хоста: `make -C "$WT" venv` и `make -C "$WT" check`; ошибка — хвост вывода (40 строк), worktree оставлен, код 1. 11. Коммит `chore(arm_statements): копия arm-statements@ (#)`, тело — диапазон, коммиты источника, итог `statements-check`, трейлер `Co-Authored-By: sync-statements.sh `; `push -u`; PR (`tea pulls create … --head <ветка> --base `), тело: «Закрывает #», было/стало, коммиты, итог `make check` и `statements-check` (предупреждение — дословно), предупреждение «сначала ядро» (если было), раздел `## Rulings I made` (R-1 — копия не правится, перенос проревьюенного в arm-statements кода; R-2 — bounded без OpenSpec change), строка `🤖 Generated with [Claude Code](https://claude.com/claude-code)`. 12. Итог: коммит, ветка, issue, PR, «исполнителю: ревью диапазона не нужно — код проревьюен в arm-statements; merge и «закрывай» по команде человека». - [ ] **Step 1: Write the failing test** — раздел «синхронизация» (хосты arm-core и tabular-parser с копией S1, `origin/main` источника = S2). | Проверка | Действие | Ожидание | |---|---|---| | dry-run создаёт worktree | `--dry-run --host-dir $WORK/arm-core` | 0; есть `$WORK/wt/statements-sync-/arm-core`; ветка `tmp/statements-sync-`; есть `.bounded`; `VERSION` = S2; `diff -r` копии с `git archive S2 arm_statements` пуст | | dry-run не трогает хост | после него | `git -C $WORK/arm-core status --porcelain` пуст, `VERSION` хоста = S1 | | повтор dry-run | ещё раз | ≠ 0, в выводе `уже существует`; затем worktree и ветка удаляются | | сначала ядро | `--dry-run --host-dir $WORK/tabular-parser` (у arm-core `origin/main` копия S1) | 0, в выводе `сначала ядро`; worktree удалить | | уже синхронно | копия arm-core на S2 закоммичена и запушена, `--dry-run` | 0, `раскладывать нечего`, worktree не создан | | отказ при прямой правке на `origin/main` | tabular-parser: правка `api/arm_statements/steps.py` закоммичена и запушена, `--dry-run` | 20, в выводе `steps.py`, worktree не создан | | хост без копии | fresh-хост без `api/arm_statements` на `origin/main`, `--dry-run` | 2, в выводе `--in-place` | | `tea`: флаги после подкоманды | `grep -nE '(^|[^[:alnum:]_-])tea[[:space:]]+-' sync-statements.sh` | пусто | - [ ] **Step 2: Run test to verify it fails** Run: `cd $D && make test-sync-statements` Expected: FAIL — строки `FAIL:` раздела «синхронизация». - [ ] **Step 3: Implement** обычный режим и `--dry-run` по интерфейсу (`tea` и `make` в тесте не вызываются — тест гоняет только `--dry-run`, как `test_sync_web.sh`). - [ ] **Step 4: Run test to verify it passes** Run: `cd $D && make test-sync-statements` Expected: все строки `ok:`, код 0. - [ ] **Step 5: Commit** ```bash git -C "$D" add scripts/sync-statements.sh scripts/test_sync_statements.sh git -C "$D" commit -m "feat(scripts): sync-statements.sh — worktree хоста, issue, make check и PR; --dry-run; предупреждение «сначала ядро» (#)" ``` --- ### Task 5: Цели `make` docs, `dev/checks.md`, PR docs **Files:** - Modify: `$D/Makefile`, `$D/dev/checks.md` (только таблица раздела «Проверки этого репозитория (`docs`)») **Interfaces:** - Produces (`Makefile`, `.PHONY` дополнен): - `sync-statements: STATEMENTS_PY=$(PY) scripts/sync-statements.sh $(SYNC_ARGS)` — комментарий: «Копия пакета arm_statements в хост одной командой (worktree, issue, PR); SYNC_ARGS="--host arm-core [--check|--dry-run|--in-place]"»; - `statements-check` — `--check` по трём хостам рядом (`arm-core tabular-parser scan-gateway`): по строке итога на хост, затем сводка; `0` и `10` — зелёные, любой другой код — цель завершается кодом 1 после проверки всех трёх. Комментарий: «обзор свежести копий во всех хостах; в check не входит — читает соседние репозитории»; - `check: validate-contracts validate-openspec test-snapshot test-sync-web test-sync-statements`. - Produces (`dev/checks.md`): строка `make check` таблицы — «все проверки ниже и `test-snapshot`, `test-sync-web`, `test-sync-statements`»; новые строки после неё: `make test-sync-statements` (сценарии `sync-statements.sh` на синтетических репозиториях: коды `--check`, `--in-place`, `--dry-run`, отказ при прямой правке), `make sync-statements` (`SYNC_ARGS` как выше), `make statements-check` (обзор по трём хостам). Строки `make venv` и `make validate-contracts` **не трогать** (их меняет PR #117 — без конфликта). - [ ] **Step 1: Проверить, что `statements-check` docs пока красный** — до подключения хостов копий нет. Run: `cd $D && make statements-check` Expected (после реализации Step 2): три строки `statements-check <хост>: 20 — …` с причиной `копии нет`, цель — код 2 от `make`. До Step 2 — `No rule to make target`. - [ ] **Step 2: Implement** цели `Makefile` и строки таблицы `dev/checks.md`. - [ ] **Step 3: Run checks** Run: `cd $D && make check && make statements-check; echo rc=$?` Expected: `make check` — все цели зелёные (в т. ч. `ok: sync-statements.sh …`); `statements-check` — три строки «копии нет», `rc=2`. - [ ] **Step 4: Commit** ```bash git -C "$D" add Makefile dev/checks.md git -C "$D" commit -m "feat(make): sync-statements, statements-check и test-sync-statements в check (#)" ``` - [ ] **Step 5: Финальное ревью ветки (opus), PR** — `git -C "$D" fetch && git -C "$D" rebase origin/main`, `make check`, `tea pulls create --login home --repo RSHB/arm-ka-docs --head feat/-sync-statements --base main`; описание: «Закрывает #», что делает скрипт, коды, итог `make check`, решения `P2-1…P2-21` в разделе `## Rulings I made`, таблица находок с маркерами и статусами. --- ### Task 6: Спека, `design.md` и раздел «Копия общего кода» (ветка `feat/116-unified-normalization`) **Files:** - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/specs/statement-normalization/spec.md` - Modify: `…/unified-statement-normalization/design.md` (раздел `## Decisions made without discussion`) - Modify: `/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/dev/checks.md` (раздел «Копия общего кода `arm_statements` и `sync-statements`», абзац под таблицей «Состав `make check-full`») - Modify: `…/unified-statement-normalization/proposal.md` («Порядок выкладки», шаг 1) **Interfaces:** - Consumes: номера issue и PR docs (задача 5). - [ ] **Step 1: Спека** — в требовании «`sync-statements` — копии пакета в потребителях» после предложения «Без `--check` скрипт раскладывает…» добавить: «С `--in-place` скрипт SHALL раскладывать копию коммита `--ref` в рабочее дерево указанного хоста (существующий worktree) без issue, коммита и PR — для подключения хоста и для PR фичи, которой нужна новая версия пакета; при прямой правке копии или недостающей зависимости — отказ с кодом `20`, ничего не пишется.» и сценарий: ``` #### Scenario: Копия в PR фичи - **WHEN** PR scan-gateway нужна версия пакета новее, чем в `main` хоста - **THEN** `sync-statements.sh --in-place --host-dir .` в worktree фичи раскладывает копию и пишет `VERSION`, копия коммитится отдельным коммитом этого PR; `--check` после раскладки — `0` ``` - [ ] **Step 2: `dev/checks.md`** — в разделе «Копия общего кода…»: снять «**Пока не реализовано:** …», вместо него — «Скрипт и цели docs — arm-ka-docs#; `statements-check` сервисов появляется с PR подключения копии в каждом хосте (план 2, задачи 7–9) — до шага 3 выкладки»; в пункт «Скрипт» добавить `--in-place` (как в спеке), `--dry-run`, обзор `make -C docs statements-check`; в пункт «Где копия» — что конфиг линтеров пакета в копию не кладётся, а хост держит `arm_statements` в строгом блоке `mypy` и в `testpaths` (P2-1, P2-3). Абзац «`statements-check` появляется в сервисе вместе с копией… (шаги 3 и 5 выкладки)» — заменить на «вместе с копией пакета (PR подключения хоста)». - [ ] **Step 3: `proposal.md`** — шаг 1: «`arm-statements` — пакет с тестами; `sync-statements` в docs; копия пакета подключается к хостам (линтеры, `testpaths`, `statements-check`) — код сервисов её ещё не вызывает». - [ ] **Step 4: `design.md`** — решения `P2-1…P2-21` раздела ниже с датой 18.10.2026 (формат раздела — как у решений P1-n: решение, обоснование, цена). - [ ] **Step 5: Проверить** — `cd /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs && openspec validate unified-statement-normalization --strict` — `valid`. - [ ] **Step 6: Commit и push** ```bash G=/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs git -C "$G" add openspec/changes/unified-statement-normalization dev/checks.md git -C "$G" commit -m "openspec(#116): sync-statements — режим --in-place, подключение копии к хостам, решения P2-n" git -C "$G" pull --rebase && git -C "$G" push ``` --- ### Task 7: arm-core — подключение копии `arm_statements` **Files (worktree `$W/arm-core`, ветка `feat/-arm-statements-copy`, issue «Подключение копии пакета arm_statements (arm-ka-docs#116, план 2)»):** - Create: `api/arm_statements/**` (скриптом, руками не править) - Modify: `Makefile`, `api/pyproject.toml`, `api/pytest.ini`, `api/requirements-dev.txt`, `CLAUDE.md` **Interfaces:** - Consumes: `../docs/scripts/sync-statements.sh` (задачи 2–4, из `$W/docs` = docs `origin/main`). - Produces для планов 3 (часть Б) и далее: `import arm_statements` из `api/`; `mypy src scripts arm_statements`; тесты пакета в `make check`; цель `statements-check` в `check-full`. Правки (точные значения): | Файл | Правка | |---|---| | `Makefile` | `lint`: `$(BIN)/mypy src scripts arm_statements`; новые `ARM_KA_DOCS ?= ../docs` и цель `statements-check` (рецепт ниже); `check-full: check statements-check dbml-check check-infra test-integration e2e docker-build web-check`; `.PHONY` + `statements-check` | | `api/pyproject.toml` | `[tool.ruff.lint.isort] known-first-party = ["src", "scripts", "arm_statements"]`; в строгий блок `[[tool.mypy.overrides]]` `module = ["src.models.*", "src.domain.*", "arm_statements", "arm_statements.*"]` + комментарий «копия общего кода (sync-statements, не править) — в строгом режиме, как в репозитории arm-statements» | | `api/pytest.ini` | `testpaths =` строки `tests` и `arm_statements/tests` | | `api/requirements-dev.txt` | `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906` вместо строк с `>=`; комментарий «ruff, mypy, types-PyYAML — те же версии, что в arm-statements (копия пакета проверяется конфигом хоста)» | | `CLAUDE.md` | в «Правила репозитория»: «`api/arm_statements/` — копия пакета `arm-statements`, напрямую не править: обновление — `../docs/scripts/sync-statements.sh` (`--in-place` в PR фичи), свежесть — `make statements-check` (в `check-full`)» | Рецепт `statements-check` (одинаковый в трёх хостах): ```make # Свежесть копии api/arm_statements/ (../docs/dev/checks.md): 0 — синхронна; 10 — отстала или перечень кодов в docs # новее (цель зелёная, предупреждение вписать в описание PR); 20 и 2 — красная. Читает ../docs и ../arm-statements, # поэтому только в check-full, не в check ARM_KA_DOCS ?= ../docs statements-check: @rc=0; $(ARM_KA_DOCS)/scripts/sync-statements.sh --check --host-dir . || rc=$$?; \ if [ $$rc -eq 10 ]; then echo "statements-check: ПРЕДУПРЕЖДЕНИЕ — впишите в описание PR"; \ elif [ $$rc -ne 0 ]; then exit $$rc; fi ``` - [ ] **Step 1: Цель без копии — красная.** Добавить в `Makefile` только `ARM_KA_DOCS`, `statements-check` и `.PHONY`. Run: `make -C $W/arm-core statements-check; echo rc=$?` Expected: `statements-check arm-core: 20 — …`, «копии нет», `rc=2` (код make). - [ ] **Step 2: Разложить копию.** Run: `cd $W/arm-core && ../docs/scripts/sync-statements.sh --in-place --host-dir .` Expected: `statements-check arm-core: 0` (или `10` только с причиной «перечень кодов в docs…», пока docs PR #117 не слит); `api/arm_statements/VERSION` = `git -C /Users/andrey.epifancev/projects/awp-ca/arm-statements rev-parse origin/main`. - [ ] **Step 3: Правки таблицы выше** (кроме уже сделанного в Step 1). - [ ] **Step 4: Проверки.** Run: `make -C $W/arm-core venv check` Expected: зелёный; `mypy src scripts arm_statements` без ошибок. Run: `cd $W/arm-core/api && .venv/bin/pytest --collect-only -q arm_statements/tests | tail -1` и `cd /Users/andrey.epifancev/projects/awp-ca/arm-statements && make venv >/dev/null && .venv/bin/pytest --collect-only -q | tail -1` Expected: одинаковое число `tests collected`. Run: `make -C $W/arm-core statements-check` Expected: код 0, строка `0` или `10` (только перечень кодов). Run: `make -C $W/arm-core docker-build && docker run --rm arm-core python -c "from arm_statements import default_rules, statements_version; default_rules(); print(statements_version())"` Expected: печатается хэш из `api/arm_statements/VERSION` (словари `rules/*.yaml` в образе). - [ ] **Step 5: `make check-full`** (`make up` при необходимости). Expected: зелёный; итог и предупреждение `statements-check` (если было) — в описание PR. - [ ] **Step 6: Commit (два коммита) и PR.** ```bash git -C "$W/arm-core" add api/arm_statements git -C "$W/arm-core" commit -m "chore(arm_statements): копия пакета arm-statements@ (#)" git -C "$W/arm-core" add Makefile api/pyproject.toml api/pytest.ini api/requirements-dev.txt CLAUDE.md git -C "$W/arm-core" commit -m "build: копия arm_statements в линтерах и тестах, statements-check в check-full (#)" git -C "$W/arm-core" push -u origin feat/-arm-statements-copy tea pulls create --login home --repo RSHB/arm-core --head feat/-arm-statements-copy --base main --title "Подключение копии пакета arm_statements (#)" --description "…" ``` Описание PR: что подключено, что код сервиса пакет ещё не вызывает (это план 3, часть Б), итог `check-full`, `## Rulings I made` (P2-1…P2-4, P2-20), строка Claude Code. --- ### Task 8: tabular-parser — подключение копии **Files (worktree `$W/tabular-parser`, issue в `RSHB/arm-tabular-parser`):** как в задаче 7. Шаги — задачи 7 с такими отличиями: | Файл | Правка | |---|---| | `Makefile` | `lint`: `$(BIN)/mypy src arm_statements`; `check-full: check statements-check test-integration docker-build` | | `api/pyproject.toml` | `known-first-party = ["src", "arm_statements"]`; **новый** блок `[[tool.mypy.overrides]] module = ["arm_statements", "arm_statements.*"]` с флагами строгого блока arm-core (`disallow_any_generics`, `disallow_subclassing_any`, `disallow_untyped_calls`, `disallow_untyped_defs`, `disallow_incomplete_defs`, `disallow_untyped_decorators`, `warn_return_any`, `warn_unused_ignores`, `no_implicit_reexport`, `strict_equality`, `extra_checks` = `true`) и комментарием про `strict = true` в overrides mypy 2.x (как у arm-core) | | `api/requirements-dev.txt` | `ruff==0.16.10`, `mypy==2.4.0`, **добавить** `types-PyYAML==6.0.12.20260906` | | `api/pytest.ini`, `CLAUDE.md` | как у arm-core | Образ — `tabular-parser` (`docker run --rm tabular-parser python -c …`). `.dockerignore` не менять (исключает `api/tests`, но не `api/arm_statements`). Словари `api/rules/statement_*.yaml` сервиса этим PR **не** удаляются — это план 4. --- ### Task 9: scan-gateway — подключение копии **Files (worktree `$W/scan-gateway`, issue в `RSHB/arm-scan-gateway`):** как в задаче 7, без `requirements-dev.txt` (пины уже `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906`). | Файл | Правка | |---|---| | `Makefile` | `lint`: `$(BIN)/mypy src arm_statements`; в `check-full` после `$(MAKE) check` — `$(MAKE) statements-check`; в `help` — строка `statements-check - свежесть копии api/arm_statements (sync-statements --check), входит в check-full`; `lint` в `help` — `mypy src arm_statements` | | `api/pyproject.toml` | `known-first-party = ["src", "arm_statements"]`; в строгий блок `module` добавить `"arm_statements", "arm_statements.*"` | | `api/pytest.ini`, `CLAUDE.md` | как у arm-core | Образ — `scan-gateway:dev`. `per-file-ignores` `"tests/**"` на копию не распространяется — так и должно быть (P1-3). --- ## Что должны включить планы 3, 4, 5 Подготовку хостов делает этот план (задачи 7–9, P2-5); планы хостов стартуют от `main` с копией. Им остаётся: | | План 3 (arm-core, часть Б) | План 4 (tabular-parser) | План 5 (scan-gateway) | |---|---|---|---| | Старт | после слияния PR задачи 7 (у плана 3 это уже записано: «PR плана 2 в arm-core слит до части Б») | после слияния PR задачи 8 | после слияния PR задачи 9 | | Нужна версия пакета новее, чем в `main` хоста | `../docs/scripts/sync-statements.sh --in-place --host-dir .` в worktree фичи; копия — отдельным коммитом `chore(arm_statements): копия пакета arm-statements@`; руками не править | то же | то же | | `make check-full` перед PR | `statements-check` — `0` или `10`; `10` (отстала / перечень кодов) — в описание PR | то же | то же | | `service.statements_version` | — (ядро читает `statements_version()` для `STATEMENTS_VERSION_MISMATCH`) | результат пишет `statements_version()` пакета | то же | | Словари сервиса | `api/rules/statement_checks.yaml` удалить (копия §7), `statement_lines.yaml` остаётся словарём имён — `statements-check` сверяет его коды с перечнем копии | `api/rules/statement_checks.yaml`, `statement_headers.yaml`, `statement_periods.yaml` удалить (переехали в пакет, P1-13) | `api/rules/ocr_lookalikes.yaml`, `statement_labels.yaml`, `statement_lines.yaml` — по плану 5 (в пакет переезжают двойники и подписи) | | Не делать | не добавлять `per-file-ignores`, `exclude` или ослабляющие overrides для `arm_statements`; не менять пины ruff/mypy без пакета; `api/conftest.py` с `autouse` не заводить; `api/arm_statements` не добавлять в `.dockerignore`; Dockerfile не менять | то же | то же | --- ## Решения автора плана без обсуждения (P2-n) Переносятся в `design.md` задачей 6 (дата 18.10.2026). - **P2-1 (O-1 плана 1).** Файл конфига линтеров в копию не кладётся; «конфиг пакета подключён к хосту» — это: ruff хоста уже совпадает с пакетом (`line-length = 120`, `py311`, `E F I UP B PL`), в isort `known-first-party` добавлен `arm_statements`; в `mypy` хоста модули `arm_statements.*` входят в строгий блок (те же флаги, что `strict = true` пакета). Цена: блок флагов повторён в трёх хостах; если хост когда-нибудь станет строже пакета, копия покраснеет в хосте — чинить в `arm-statements`. - **P2-2 (O-2).** Пины `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906` во всех хостах (у tabular-parser и arm-core были `>=`; в их окружениях уже стоят эти версии — пины ничего не переформатируют). `pytest` не закрепляется: тесты пакета не зависят от его версии. Цена: обновление ruff/mypy — согласованно, сначала пакет. - **P2-3 (O-3).** `testpaths = tests arm_statements/tests` в `pytest.ini` хостов; `conftest` хостов не меняется: у них нет `api/conftest.py`, а `tests/conftest.py` действует только на `tests/`; `env`, `asyncio_mode`, `--strict-markers` тестам пакета не мешают. Проверка — равное число собранных тестов пакета в хосте и в `arm-statements`. - **P2-4 (O-4).** Dockerfile хостов не меняется: `COPY api/ ./` уже кладёт `api/arm_statements/rules/*.yaml`, `.dockerignore` их не исключает; платформа уже возит `api/rules/*.yaml` и `api/contracts/` (сервисы читают их с диска в контуре). Проверка — запуск `default_rules()` в собранном образе (задачи 7–9). - **P2-5.** Подключение хостов — три bounded PR этого плана (задачи 7–9), а не части планов 3/4/5: правки одинаковые и механические, план 3 уже ждёт «PR плана 2 в arm-core», первая раскладка проверяет скрипт на настоящих хостах, а планы 4/5 стартуют от `main` с копией. Цена: ещё три PR и копия в `main` хостов до того, как её вызывает код сервиса. - **P2-6.** Режим `--in-place` (раскладка в существующий worktree без issue, коммита и PR) — для подключения хоста и для PR фичи, которой нужна новая версия пакета; спека дополнена (задача 6). Цена: второй путь раскладки, мимо отдельного PR синхронизации. - **P2-7.** Скрипт — bash по образцу `sync-web.sh` (spec называет `sync-statements.sh`), разбор TOML/YAML — помощник `statements_check.py` (Python 3.11 + PyYAML); интерпретатор — `STATEMENTS_PY` → `docs/.venv` → `api/.venv` хоста → `python3`; подходящего нет — код `2`. Цена: `make venv` docs ставит PyYAML (как и в PR #117). - **P2-8.** В `--check` отсутствие копии — `20` («копии нет»): хост, в котором вызывают проверку, обязан держать копию. В обычном режиме хост без копии — `2` с указанием на `--in-place`: подключение хоста — ручная задача с правкой конфигов. - **P2-9.** Нет `contracts/statement-code-orders.yaml` в docs — `10` (docs старше пакета; так до слияния PR #117). - **P2-10.** Зависимости сверяются по имени (PEP 503) с `api/requirements.txt` хоста (его ставит образ); разные версии — примечание, код не меняют. Цена: несовпадение версии PyYAML ловят только тесты пакета в хосте. - **P2-11.** «Копия отстала» — буквально по спеке: `VERSION` ≠ `--ref`, даже если новые коммиты источника не трогают `arm_statements/` (README, Makefile). Цена: лишние предупреждения `10` после таких коммитов (см. O-1). - **P2-12.** Ошибка `fetch` в `--check` не фатальна (предупреждение, сравнение с локальными ссылками): проверка перед PR не должна краснеть без сети, если копия на месте. - **P2-13.** Сравнение — по файлам рабочего дерева (`diff -rq`), без `VERSION` и кэшей (`__pycache__`, `*.pyc`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.DS_Store`); биты режима не сравниваются. Незакоммиченная правка — тоже `20`. - **P2-14.** Обычный режим перед коммитом гоняет `make venv` и `make check` хоста в новом worktree (как `sync-web.sh` — проверки фронта). Цена: минута на окружение. - **P2-15.** «Сначала ядро» при синхронизации сервиса — предупреждение (в выводе и в описании PR), не отказ: порядок важен для смены формата результата, а не для каждой правки пакета. - **P2-16.** `make -C docs statements-check` — обзор по трём хостам рядом (`0`/`10` — зелёные); в `make check` docs не входит: читает соседние репозитории. - **P2-17.** Имя хоста — `--host` или последний компонент `origin` (`arm-core`, `arm-tabular-parser`, `arm-scan-gateway`), иначе имя каталога; сверка словаря имён — только у `arm-core`. - **P2-18.** Раскладка отказывает (код 1), если файлы копии игнорируются `.gitignore` хоста или содержат то, на чём остановится снимок для контура (`/Users/`, `/home/…`, `git.aepif.ru`, `uv venv`, `uv pip`). - **P2-19.** Строки `make venv` docs — дословно как в ветке `feat/116-unified-normalization`, строки таблицы `dev/checks.md`, которые меняет PR #117, не трогаются: два PR сливаются в любом порядке без конфликта. - **P2-20.** В `CLAUDE.md` каждого хоста — правило «копию не править, обновлять скриптом»: агенты читают его первым. Шаблоны `docs/repo-templates/` не меняются (разошлись с репозиториями давно). - **P2-21.** Ветка docs плана — от `origin/main` (шаг 1 выкладки раньше PR #117); спека, `design.md` и раздел «Копия» `dev/checks.md` правятся в `feat/116-unified-normalization` (задача 6). ## Открытые вопросы (O-n) - **O-1.** P2-11: считать ли копию синхронной, если коммиты после `VERSION` не меняют `arm_statements/` и `[project].dependencies`? Спека говорит «это коммит `--ref`»; смягчение уберёт шум, но поменяет требование. - **O-2.** Есть ли в Nexus банка `ruff==0.16.10`, `mypy==2.4.0`, `types-PyYAML==6.0.12.20260906` (цель `venv` снимка контура ставит `requirements-dev.txt` из Nexus)? У scan-gateway эти пины уже есть — вероятно, да; подтвердить при следующем переносе. - **O-3.** `prepare-contour-snapshot.sh` проверяет отставание `arm-core/web` (`sync-web --check`), но не свежесть копии `arm_statements`. Добавить шаг `sync-statements --check` в подготовку снимка (предупреждение в `SNAPSHOT.md`) — по решению владельца; в этом плане нет. - **O-4.** `CLAUDE.md` workspace (таблица репозиториев) и `context/00-workspace-layout.md` не знают про `arm-statements/`; правит план 1 (П-2) или отдельная правка docs — решить контроллеру плана 1.