openspec(#116): план 2 — sync-statements и statements-check в docs, подключение копии arm_statements к хостам

Скрипт docs/scripts/sync-statements.sh (--check 0/10/20/2, --in-place, --dry-run, обычный режим
с worktree/issue/PR), помощник statements_check.py, тест на синтетических репозиториях; три
bounded PR подключения копии в arm-core, tabular-parser, scan-gateway. Решения P2-1…P2-21,
открытые O-1…O-4; закрыты O-1…O-4 плана 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
Andrey Epifancev 2026-10-18 12:29:02 +04:00
parent 0b7d691735
commit 87b32a0db3

View File

@ -0,0 +1,716 @@
# План 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/<issue>-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/<issue>-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 <dir>`. Docs — каталог, где лежит скрипт
(`$HERE/..`), или `--docs <dir>`.
- 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 <noreply@anthropic.com>` и
`Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W`; PR — строкой
`🤖 Generated with [Claude Code](https://claude.com/claude-code)`. Слияние — только по команде человека.
- Находки и решения несут маркеры (`T<n>.<k>`, `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 <pyproject.toml> <requirements.txt>` и `lines <statement_lines.yaml>
<statement_code_orders.yaml>`; печатает находки по строке, примечания — с префиксом `примечание: `; код `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 <repo> <msg> <branch>`; интерпретатор —
`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 — зависимости пакета и коды словаря имён против перечня (#<issue>)"
```
---
### 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 <dir>`, `--ref <ref>` (источник,
по умолчанию `origin/main`), `--host-ref <ref>` (база worktree хоста, по умолчанию `origin/main`), `--source <dir>`,
`--docs <dir>`, `--core-dir <dir>` (по умолчанию `$WS/arm-core`), `--wt-root <dir>` (по умолчанию `$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 <host> 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>»,
работа продолжается. `--ref` не разрешается в источнике — код 2.
- Функция `evaluate <copy_dir> <requirements_file> <lines_file|""> <host_name>` — печатает находки и выставляет
`EVAL_CODE` (0/10/20); используется задачами 3–4 для проверки дерева после раскладки и дерева `--host-ref`.
- Итоговая строка `--check` (её ищут тесты и `Makefile` хоста):
`statements-check <хост>: 0 — копия синхронна с arm-statements@<ref> (<v7>)`;
`statements-check <хост>: 10 — ПРЕДУПРЕЖДЕНИЕ: <причины через «; »>`;
`statements-check <хост>: 20 — расхождение, копию не принимать`; код возврата = число в строке.
`evaluate` проверяет по порядку (печатает всё найденное):
| # | Условие | Сообщение (суть) | Код |
|---|---|---|---|
| 1 | нет каталога `api/arm_statements/` | «копии нет: разложите `sync-statements.sh --in-place --host-dir <dir>`» | 20 (остальное не проверяется) |
| 2 | нет `VERSION`, не 40 hex, коммита нет в источнике | «VERSION … нет в истории arm-statements» (+ «сделайте fetch», если fetch не удался) | 20 (3–4 не проверяются) |
| 3 | коммит `VERSION` не предок `--ref` | «чужая версия: <v7> не из истории <ref>» | 20 |
| 4 | файлы копии ≠ `git archive <VERSION> arm_statements` (извлечь во временный каталог; `diff -rq` с исключениями `VERSION`, `__pycache__`, `*.pyc`, `.pytest_cache`, `.mypy_cache`, `.ruff_cache`, `.DS_Store`) | «прямая правка копии:» и по строке на файл — путь относительно `arm_statements/` (изменён / только в копии / только в источнике) | 20 |
| 5 | `statements_check.py deps <pyproject.toml коммита VERSION> <requirements_file>` = 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 <ref>` | «копия отстала: <v7>, <ref> — <r7>», список `git log --format=' %h %s' <VERSION>..<ref>` | 10 |
Код 1 помощника — код скрипта 1 с его сообщением. Режим `--check` читает рабочее дерево `--host-dir` (незакоммиченная
правка тоже `20`); `requirements_file` — `<host>/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 <host>/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 <host> checkout -- . && git -C <host> 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 (#<issue>)"
```
---
### 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 <host_root> <commit>` — удаляет `api/arm_statements/` целиком, извлекает
`git -C "$SRC" archive <commit> arm_statements | tar -x -C <host_root>/api`, пишет `VERSION` (`<полный хэш>\n`); затем
две защиты, любая — код 1 со списком:
- игнорируемые файлы: `git -C <host_root> 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 <ref>:arm_statements/rules/statement_code_orders.yaml`) —
находка — отказ `20`, ничего не пишется;
3. копия уже на `--ref` и равна ему — «раскладывать нечего», переход к шагу 5;
4. `lay_copy`;
5. `evaluate` итога; печать итоговой строки как у `--check`; код возврата = `EVAL_CODE`.
Подсказка в конце: `закоммитьте api/arm_statements отдельным коммитом: git -C <host> 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 <fresh> --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 и путей для контура (#<issue>)"
```
---
### 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 <ref>`, `T7` = 7 символов.
2. Состояние хоста на `--host-ref`: нет `<host-ref>:api/arm_statements` — код 2 «копии нет: подключение хоста — вручную,
`--in-place` в worktree задачи подключения (dev/checks.md)». Иначе — дерево `git archive <host-ref> api` во временный
каталог и `evaluate` по нему (`requirements` — оттуда же); `20` — отказ `20` с находками, worktree не создаётся.
3. Зависимости и словарь имён против `TARGET` (как шаг 2 `--in-place`) — находка — отказ `20`.
4. `VERSION` на `--host-ref` = `TARGET` — «копия уже на <T7>, раскладывать нечего», код 0 (предупреждение о перечне
кодов, если есть).
5. «Сначала ядро»: для хоста ≠ `arm-core` — `VERSION` в `<core-dir>@origin/main`; его нет, или он ≠ `TARGET` и
`TARGET` не его предок — предупреждение `ядро (arm-core@origin/main) ещё не на <T7>: по порядку выкладки сначала
ядро` (не блокирует; строка уходит и в описание PR).
6. Worktree `$WT_ROOT/statements-sync-<T7>/<хост>` на `tmp/statements-sync-<T7>` от `--host-ref`; существует worktree
или ветка — код 2; `touch .bounded`.
7. Не `--dry-run`: issue (`tea issues create --login home --repo <Gitea-репозиторий> --title "arm_statements: копия
пакета arm-statements@<T7>" --description …` — версия копии было/стало, коммиты `<VERSION>..<TARGET>`, «bounded,
выполнена `docs/scripts/sync-statements.sh`»); ветка переименовывается в `feat/<issue>-statements-sync-<T7>`.
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@<T7> (#<issue>)`, тело — диапазон, коммиты источника, итог
`statements-check`, трейлер `Co-Authored-By: sync-statements.sh <noreply@aepif.ru>`; `push -u`; PR (`tea pulls
create … --head <ветка> --base <host-ref без origin/>`), тело: «Закрывает #<issue>», было/стало, коммиты, итог `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-<S2_7>/arm-core`; ветка `tmp/statements-sync-<S2_7>`; есть `.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; предупреждение «сначала ядро» (#<issue>)"
```
---
### 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 (#<issue>)"
```
- [ ] **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/<issue>-sync-statements --base main`; описание:
«Закрывает #<issue>», что делает скрипт, коды, итог `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#<issue>; `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/<issue>-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@<v7> (#<issue>)"
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 (#<issue>)"
git -C "$W/arm-core" push -u origin feat/<issue>-arm-statements-copy
tea pulls create --login home --repo RSHB/arm-core --head feat/<issue>-arm-statements-copy --base main --title "Подключение копии пакета arm_statements (#<issue>)" --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@<v7>`; руками не править | то же | то же |
| `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.