arm-ka-docs/openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-2-sync-statements.md
Andrey Epifancev 87b32a0db3 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
2026-10-18 12:29:02 +04:00

717 lines
69 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# План 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.