Andrey Epifancev 47e6372cbe chore(openspec): архив unified-statement-normalization (#116)
Спеки применены: 5 новых capability (arm-core, arm-web, scan-gateway,
statement-normalization, tabular-recognition-service) с Purpose, изменения
quality-checks и recognition-contract. Ссылки в 05-domain-rules, DBML v5.19,
entities, dev/checks.md и dev/qa-statements-v2.md ведут в архив.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 09:25:43 +04:00

69 KiB
Raw Blame History

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

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 не разрешается в источнике — код 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 «чужая версия: не из истории » 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> «копия отстала: , — », список 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

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

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 — «копия уже на , раскладывать нечего», код 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/>), тело: «Закрывает #», было/стало, коммиты, итог 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

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

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; описание: «Закрывает #», что делает скрипт, коды, итог 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

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 (одинаковый в трёх хостах):

# Свежесть копии 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.
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.