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