diff --git a/Makefile b/Makefile index d872437..a24b8e3 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,10 @@ PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) CHANGE ?= # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) DBML ?= model/data-model-v5.23.dbml +# Checkout библиотеки arm_statements (RSHB/pylib_ai_arm_statements) рядом с docs — для statements-orders-check +STATEMENTS_DIR ?= ../pylib_ai_arm_statements -.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-statements sync-statements statements-check check diagrams +.PHONY: venv validate-contracts validate-openspec test-snapshot statements-orders-check check diagrams # Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml) venv: @@ -25,24 +27,18 @@ validate-openspec: # Снимок для контура вырезает артефакты агента на любой глубине (dev/contour-transfer-guide.md §3); нужны git и bash test-snapshot: - STATEMENTS_PY=$(PY) scripts/test_prepare_contour_snapshot.sh + scripts/test_prepare_contour_snapshot.sh -# sync-statements.sh на синтетических репозиториях: помощник проверок, копирование пакета, отказы -test-sync-statements: - STATEMENTS_PY=$(PY) scripts/test_sync_statements.sh +# Перечень кодов форм: источник — contracts/statement-code-orders.yaml, в библиотеке — его байтовая копия +# arm_statements/rules/statement_code_orders.yaml. Расхождение или нет checkout рядом — предупреждение, цель зелёная: +# порядок правки «docs → библиотека → версия в сервисах», и до выпуска библиотеки docs законно впереди. +statements-orders-check: + @lib="$(STATEMENTS_DIR)/arm_statements/rules/statement_code_orders.yaml"; \ + if [ ! -f "$$lib" ]; then echo "statements-orders-check: ПРЕДУПРЕЖДЕНИЕ — нет $$lib (STATEMENTS_DIR), сверка пропущена"; \ + elif cmp -s contracts/statement-code-orders.yaml "$$lib"; then echo "statements-orders-check: перечень кодов совпадает с библиотекой"; \ + else echo "statements-orders-check: ПРЕДУПРЕЖДЕНИЕ — contracts/statement-code-orders.yaml отличается от $$lib (выпустите библиотеку с новым перечнем)"; fi -# Копия пакета arm_statements в хост одной командой (worktree, issue, PR); SYNC_ARGS="--host arm-core [--check|--dry-run]" -sync-statements: - STATEMENTS_PY=$(PY) scripts/sync-statements.sh $(SYNC_ARGS) - -# Обзор свежести копий во всех хостах; в check не входит — читает соседние репозитории. -# Коды 0 и 10 — зелёные, любой другой — цель завершается кодом 1 после проверки всех трёх хостов. -statements-check: - @rc=0; for h in arm-core tabular-parser scan-gateway; do \ - STATEMENTS_PY=$(PY) scripts/sync-statements.sh --check --host $$h || { c=$$?; [ $$c -eq 10 ] || rc=1; }; \ - done; exit $$rc - -check: validate-contracts validate-openspec test-snapshot test-sync-statements +check: validate-contracts validate-openspec test-snapshot statements-orders-check # Диаграммы model/diagrams/part01…09.{svg,png} из $(DBML); нужен npx (Node.js), зависимостей Python нет diagrams: diff --git a/context/00-workspace-layout.md b/context/00-workspace-layout.md index f7a6894..a02674e 100644 --- a/context/00-workspace-layout.md +++ b/context/00-workspace-layout.md @@ -15,7 +15,7 @@ ├── arm-core/ → git: arm-core ядро: FastAPI + React (монорепо по шаблону команды); фронт — `web/` ├── tabular-parser/ → git: arm-tabular-parser сервис разбора XLS/XLSX/XLSB (ОСВ, ББ, ОФР) ├── scan-gateway/ → git: arm-scan-gateway адаптер ИС Сканирование (PDF/PNG/JPG) -├── arm-statements/ → git: arm-statements пакет общего кода нормализации ББ/ОФР `arm_statements` (копируется в сервисы и ядро; план 1 change unified-statement-normalization) +├── pylib_ai_arm_statements/ → git: pylib_ai_arm_statements библиотека общего кода нормализации ББ/ОФР `arm_statements` (ставится в сервисы и ядро зависимостью; `make venv` — из этого checkout) ├── arm-web/ → git: arm-web прежний отдельный репозиторий фронта, заморожен с 21.10.2026 (см. ниже) └── .claude/ ← настройки workspace: хуки, разрешения (не в git или в arm-ka-docs/.claude-workspace) ``` @@ -46,7 +46,7 @@ связанные PR в других репозиториях. - **Worktree вместо веток в основном checkout.** Хуки запрещают запись в `main`; работа идёт в worktree. Для среза, затрагивающего несколько репозиториев, worktree собираются в зеркальный workspace `.wt/<срез>/` рядом с основным: - `docs/`, `arm-core/`, `tabular-parser/`, `scan-gateway/`, `arm-statements/` — worktree веток `feat/-`, а `fixtures/`, + `docs/`, `arm-core/`, `tabular-parser/`, `scan-gateway/`, `pylib_ai_arm_statements/` — worktree веток `feat/-`, а `fixtures/`, `reference-old-arm/`, `CLAUDE.md`, `.claude/` — симлинки на основной workspace. Так относительные ссылки `../docs`, `../fixtures` и хуки работают и в ветке. `.wt/` в git не попадает (workspace — не репозиторий). diff --git a/context/03-architecture.md b/context/03-architecture.md index 1d8a8bf..e18d401 100644 --- a/context/03-architecture.md +++ b/context/03-architecture.md @@ -164,19 +164,22 @@ advisory-lock по `(ЮЛ, sha256)` (двойной клик не проскак - **Нагрузка и транзакции.** Переход версии после загрузки выполняется в отдельной короткой транзакции (`session_transaction`), сбой не отменяет принятые файлы (R-10). Чтение (`GET …/osv`, сводка, экспорт) блокировок не берёт. -### Общий код нормализации ББ/ОФР — `arm-statements` (change `unified-statement-normalization`, целевое) +### Общий код нормализации ББ/ОФР — библиотека `arm_statements` (change `unified-statement-normalization`, AURORA-1677) Шаги 2–8 нормализации форм (раскладка, статусы строк, значения, код и наименование, сборка документа, проверки §7, -`needs_review`, статус и предупреждения) и их словари живут в отдельном репозитории `arm-statements` пакетом -`arm_statements/` (Р-1: платформа не собирает Python-пакеты). В tabular-parser, scan-gateway и arm-core пакет лежит -**копией** (`api/arm_statements/`, маркер версии — `api/arm_statements/VERSION`) с тестами, импортируется по имени -`arm_statements`; копию раскладывает и проверяет `sync-statements` в docs (`--check`: 0 — синхронно, 10 — отстала, 20 — -прямая правка, 2 — нет checkout; вызывается из `make check-full`, не из `make check`; механика — `dev/checks.md`). -Упорядоченные перечни кодов форм — данные `contracts/statement-code-orders.yaml`, в пакете — их байтовая копия. Сервисы держат только адаптер -шага 1 (json ИС / лист Excel → `FormCandidate`); ядро считает тем же кодом после правок аналитика и не держит своих -копий словаря §7. Словарь «код → наименование» — только в ядре. Правила — `05-domain-rules.md` §11; маркер версии -копии — `service.statements_version` результата → `recognition_job.statements_version`. Порядок выкладки — «сначала -ядро» (ядро принимает 1.x и 2.0). Пункты ниже описывают поведение до перехода (Срез 3b). +`needs_review`, статус и предупреждения) и их словари живут в Python-библиотеке: репозиторий `RSHB/pylib_ai_arm_statements` +(имя `pylib_ai*` — требование платформы), дистрибутив `pylib-ai-arm-statements`, импорт `arm_statements`. Версия — +семантическая (`VERSION` в корне библиотеки, тег `v<версия>`); библиотеку собирает и публикует в Nexus платформа +(Р-1 «платформа не собирает Python-пакеты» отменён 26.10.2026). tabular-parser, scan-gateway и arm-core держат в +`requirements.txt` одну и ту же строку `pylib_ai_arm_statements==<версия>`; на платформе она ставится из Nexus, вне банка +`make venv` ставит библиотеку из соседнего checkout `STATEMENTS_DIR` (механика — `dev/checks.md`, «Библиотека общего +кода»). Упорядоченные перечни кодов форм — данные `contracts/statement-code-orders.yaml`, в библиотеке — их байтовая копия +(сверяет `make -C docs statements-orders-check`). Сервисы держат только адаптер шага 1 (json ИС / лист Excel → +`FormCandidate`); ядро считает той же библиотекой после правок аналитика и не держит своих копий словаря §7. Словарь +«код → наименование» — только в ядре. Правила — `05-domain-rules.md` §11; версия библиотеки — `service.statements_version` +результата → `recognition_job.statements_version`. Новая версия библиотеки выкладывается одной поставкой ядра и обоих +сервисов. До 27.10.2026 библиотека лежала копиями `api/arm_statements/` в сервисах и раскладывалась скриптом +`sync-statements` (репозиторий `arm-statements` архивирован). ### Формы ББ/ОФР, проверки §7 и API экрана проверки (Срез 3b) diff --git a/context/05-domain-rules.md b/context/05-domain-rules.md index 74212f9..a36d19d 100644 --- a/context/05-domain-rules.md +++ b/context/05-domain-rules.md @@ -747,7 +747,7 @@ one_sided = ровно одна из amount_a, amount_b равна 0 (знак - **Межформенная** `1370(отчётная дата) − 1370(начало года) = 2400` сервисами не считается — только ядром (ниже). **Ядро.** При загрузке результата v2.0 ядро §7 **не пересчитывает**: `checks`, `needs_review` и статус документа -берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 копией +берутся из результата (§9.3, §9.5 п. 2). После правки аналитика (`PATCH …/statement`) ядро пересчитывает §7 библиотекой `arm_statements` по текущим суммам (`financial_statement_value.amount` после правок) — с теми же правилами, что в сервисах. Пересчёт идёт точкой входа пакета для пересчёта по сохранённым данным (спека `statement-normalization`): она же заново определяет вариант формы и редакцию документа для проверок ядра. Результат 1.x (окно выкладки) `checks` @@ -783,8 +783,8 @@ OQ-145); тем же правилом сервисы разрешают ничь быть несколько имён — синонимы и законные повторы кода в бланке, Р-20: 1220 «Налог на добавленную стоимость по приобретенным ценностям» и «НДС, начисленный по отгрузке», 1520 «Кредиторская задолженность» и «Обязательство по аренде»; загрузчик повтор кода не запрещает, сверка наименования — с любым именем кода, шаблонная строка берёт первое); -порядок и состав редакций ядро берёт из копии упорядоченного перечня кодов `arm_statements` (данные — -`contracts/statement-code-orders.yaml`; `sync-statements --check` сверяет, что все коды словаря имён есть в перечне); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не +порядок и состав редакций ядро берёт из упорядоченного перечня кодов библиотеки `arm_statements` (данные — +`contracts/statement-code-orders.yaml`; что все коды словаря имён есть в перечне, сверяет тест ядра `test_name_dictionary_codes_in_code_orders`); код, у которого есть хотя бы одна строка `ok` из файла, шаблоном не достраивается; строки `unreadable` / `missing` шаблонной строкой молча не заменяются — показываются отдельно со статусом. Наименование строки из источника хранится как распознано; экран показывает справочное имя по коду, файловое — рядом (Р-3, AURORA-1406 / 1522). @@ -1112,13 +1112,14 @@ attempt+1») устарела — исправить в следующей ве строк источника нет (первичка не мутируется, `detail` — способ вывести строку из сумм). Строки, созданные правкой: `row_index = null`, `source_ref = null`, `line_status = ok`, `line_code_original_value = null`. Правка `header.report_date` пересчитывает `period_date` колонок с `period_kind = relative:*` общим кодом (`null` — `null`). - После правки ядро пересчитывает `checks`, `needs_review` и статус копией `arm_statements` (§11.9): правленое или + После правки ядро пересчитывает `checks`, `needs_review` и статус библиотекой `arm_statements` (§11.9): правленое или подтверждённое значение и строка не имеют `needs_review`, акцепт снимает жёсткие нарушения; записи `document_issue` видов `relation` / `soft` заменяются целиком, предупреждения сервиса сохраняются, кроме снимаемых правкой (таблица §11.9; значения строк `detail` — без `needs_review`, Р-23). **Акцепт** сбрасывается правкой данных — суммы, кода или статуса строки, даты колонки, поля шапки; подтверждение ячейки или кода (`confirmed`) и подтверждение даты отчёта или колонки тем же значением суммы не меняют и акцепт не сбрасывают (решение владельца 18.10.2026, Р-25). Расхождение `statements_version` - задания и копии ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется). Один PATCH — + задания и версии библиотеки ядра — `info` `STATEMENTS_VERSION_MISMATCH` (у заданий 1.x, где версии нет, — не пишется; + у заданий до перехода на библиотеку в версии хэш коммита копии — расхождение ожидаемо, решение владельца 26.10.2026). Один PATCH — один пересчёт и одна запись `RECOGNIZED_DATA_EDIT` (`before/after` по кодам, статусам, родителям, подтверждениям). 10. **Исходный файл и комментарии (Срез 3b).** `GET /files/{id}/content` (`DOCUMENT_VIEW`, policy по ЮЛ) отдаёт исходный объект из S3 потоком через ядро (в контуре MinIO для пользователя может быть недоступен, а через ядро действует @@ -1378,10 +1379,11 @@ v2.0. Порядок выкладки — «сначала ядро» (ядро 5. **Очистка OCR — не отдельный шаг**, а первая стадия типовых разборов текста там, где уже известен смысл ячейки (дата, подпись колонки, код, разлепление, сумма). -**Где код.** Шаги 2–8, общие разборы и словари — пакет `arm_statements/` репозитория `arm-statements` (Р-1); в -tabular-parser, scan-gateway и arm-core он лежит копией (раскладывает и проверяет `sync-statements` в docs, прямые правки -копии запрещены, маркер версии — `service.statements_version`). Golden-тесты пакета — на синтетических и обезличенных -данных внутри пакета; тесты на `fixtures/` остаются в сервисах. Изменения правил ниже — только через этот параграф и +**Где код.** Шаги 2–8, общие разборы и словари — библиотека `arm_statements` (дистрибутив `pylib-ai-arm-statements`, +репозиторий `RSHB/pylib_ai_arm_statements`; AURORA-1677). tabular-parser, scan-gateway и arm-core подключают её +зависимостью с одной версией (`pylib_ai_arm_statements==<версия>`), маркер версии — `service.statements_version`; +поставка — `dev/checks.md`, «Библиотека общего кода». Golden-тесты библиотеки — на синтетических и обезличенных +данных внутри неё; тесты на `fixtures/` остаются в сервисах. Изменения правил ниже — только через этот параграф и словари пакета. ### §11.1 Источник OCR: ИС Сканирование (адаптер шага 1 в scan-gateway) @@ -1757,7 +1759,7 @@ false` всегда (Р-23, решение владельца 18.10.2026): ко `CODE_NOT_IN_EDITION`, не `needs_check`: пятизначные подстроки 1С и нестандартные коды клиента не блокируют автоподтверждение). В пакете — **упорядоченный перечень кодов** каждой формы по редакциям (66н, ФСБУ 4) и вариантам, без наименований, в порядке бланка (итоги после слагаемых: 1110 … 1190, 1100; 2110, 2120, 2100). **Данные перечня — -`contracts/statement-code-orders.yaml`** (источник; в пакете — его байтовая копия, сверяет `sync-statements --check`): +`contracts/statement-code-orders.yaml`** (источник; в библиотеке — его байтовая копия, сверяет `make -C docs statements-orders-check`): объединение кодов шаблонов банка (словарь ядра) и кодов печатных форм 1С и сканов из `fixtures/` (1105, 1130, 1140, 1215, 1231, 1530, 2420, 2460, 2530, 2930) с источником у каждого; код без образца в перечень не входит (кандидаты — OQ-168; временное правило утверждено владельцем 18.10.2026, Р-26: неуверенный такой код на скане — `needs_review` строки), diff --git a/contracts/recognition-result.schema.json b/contracts/recognition-result.schema.json index 19531dd..7590744 100644 --- a/contracts/recognition-result.schema.json +++ b/contracts/recognition-result.schema.json @@ -2,7 +2,7 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "arm-ka/recognition-result/v2", "title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v2.0", - "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются общим кодом arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.", + "description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Формы ББ/ОФР нормализуются библиотекой arm_statements (05-domain-rules.md §11): статусы строк, needs_review, проверки §7, статус документа и структурные предупреждения приходят готовыми; ядро хранит их как пришли и пересчитывает тем же кодом только после правок аналитика. Нормализация счетов ОСВ, резолв контрагентов, сверка ИНН и периода — в arm-core.", "$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (2.1, 2.2, …) — необязательные поля и новые значения enum; MAJOR (3.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает известный MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать новый MINOR), потому что закрытые enum отвергают незнакомые значения. Новый MAJOR — порядок «сначала ядро»: ядро на время перехода принимает и прежний, и новый MAJOR (два маппера), сервисы переходят после выкладки ядра. Changelog: 1.0 — исходный контракт; 1.1 — osvDocument.rows[].extra; 1.2 — DISCLOSURE, statementDocument.checks, границы периода ОСВ; 2.0 (RSHB/arm-ka-docs#116, change unified-statement-normalization) — line_code nullable и line_status (ok | unreadable | missing | detail), row_index, ref, parent_ref, line_code_text, needs_review у значения и строки, values[].text, header.<поле>_text и <поле>_confidence у всех полей шапки, header.needs_review_fields, columns[].period_kind и label_confidence, document.status и document.warnings у всех документов (включая ОСВ), statementDocument.form_variant, metrics.warnings и warnings — структуры с уровнем и привязкой, checks обязателен у BALANCE/INCOME со строками, нарушения с codes, удалён checks.duplicate_line_codes, service.statements_version, header.okei (+ _text, _confidence), смысл is_recognized у OCR отвязан от confidence, statementDocument.form_variant обязателен, OTHER удалён из statementDocument.document_type. Схема 1.2 на окне перехода лежит рядом — recognition-result.v1.schema.json (примеры — examples/v1/); удаляется вместе с маппером 1.x ядра после перехода обоих сервисов.", "type": "object", "required": ["schema_version", "job_id", "file_id", "service", "status", "documents"], @@ -16,7 +16,7 @@ "properties": { "name": { "enum": ["tabular-parser", "scan-gateway"] }, "version": { "type": "string" }, - "statements_version": { "type": "string", "minLength": 1, "description": "коммит репозитория arm-statements, из которого разложена копия общего кода; обязателен, если в documents есть statementDocument" } + "statements_version": { "type": "string", "minLength": 1, "description": "маркер VERSION библиотеки arm_statements, если он есть, иначе версия установленного дистрибутива pylib-ai-arm-statements; обязателен, если в documents есть statementDocument" } } }, "status": { "enum": ["done", "needs_check", "error"], "description": "done ⇔ все документы done и нет предупреждений файла уровня error; error — документов нет" }, diff --git a/contracts/statement-code-orders.yaml b/contracts/statement-code-orders.yaml index de9bce1..91ae4f4 100644 --- a/contracts/statement-code-orders.yaml +++ b/contracts/statement-code-orders.yaml @@ -7,10 +7,10 @@ # fixtures/statements-xlsx/templates/) и кодов печатных форм 1С и сканов, найденных в fixtures (источник — комментарий # у кода). Код без образца в fixtures сюда не добавляется: кандидаты — OQ-168. # -# Где живёт: этот файл — источник; в пакете arm_statements лежит его байтовая копия (rules/statement_code_orders.yaml), -# идентичность проверяет sync-statements --check (как копии схем контракта в сервисах). Ядро берёт порядок и состав -# редакций из копии пакета; sync-statements --check сверяет, что все коды словаря имён ядра есть в перечне своей -# редакции. Правка перечня — PR сюда, затем в arm-statements, затем sync-statements в потребителях («сначала ядро»). +# Где живёт: этот файл — источник; в библиотеке arm_statements лежит его байтовая копия (rules/statement_code_orders.yaml), +# идентичность проверяет make -C docs statements-orders-check. Ядро берёт порядок и состав редакций из библиотеки; что все +# коды словаря имён ядра есть в перечне своей редакции, сверяет тест ядра test_name_dictionary_codes_in_code_orders. +# Правка перечня — PR сюда, затем в библиотеку (новая версия), затем подъём версии в потребителях («сначала ядро»). # # Как читается (§11.9): четырёхзначные коды строк ok документа — последовательность; для каждой редакции берётся # наибольшая подпоследовательность, согласованная с порядком перечня варианта формы документа (form_variant); выбирается diff --git a/dev/.env.example b/dev/.env.example index 66d4208..b77285a 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -5,6 +5,9 @@ # Адрес привязки портов хоста: 127.0.0.1 — только эта машина; 0.0.0.0 — доступ с других машин (пароли по умолчанию dev) BIND_ADDR=127.0.0.1 +# Исходники библиотеки для сборки всех сервисов (путь относительно docs/dev). +STATEMENTS_DIR=../../pylib_ai_arm_statements + # PostgreSQL 16 (схема dkb создаётся миграциями arm-core, не compose) POSTGRES_DB=armka POSTGRES_USER=armka diff --git a/dev/checks.md b/dev/checks.md index 80906a1..9de3336 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -16,12 +16,12 @@ | Цель | Что делает | Нужен Docker / compose | |---|---|---| -| `make venv` | `api/.venv` на Python 3.11 через `uv`; ставит `requirements.txt` без строк `raisa_*` (пакеты Nexus банка вне банка недоступны, D-10) и `requirements-dev.txt` | нет | +| `make venv` | `api/.venv` на Python 3.11 через `uv`; ставит `requirements.txt` без строк `raisa_*` и `pylib_ai*` (пакеты Nexus банка вне банка недоступны, D-10), `requirements-dev.txt` и библиотеку `arm_statements` из `STATEMENTS_DIR` | нет | | `make lint` | `ruff check .`, `ruff format --check .`, `mypy src` (в `arm-core` — `mypy src scripts`) | нет | | `make test` | `pytest -m "not integration"`: юнит-тесты; **включает тест идентичности вендоренного контракта** `api/contracts/` с `../docs/contracts` | нет | | `make test-integration` | `pytest -m integration` против инфраструктуры compose (PostgreSQL 16, NATS, MinIO; бакет `arm-ka` должен существовать, его создаёт `make up`) | да, `make up` | | `make check` | `make lint` затем `make test`; останавливается на первой упавшей проверке (только при последовательном запуске: `lint` и `test` — пререквизиты, при `make -j` они идут параллельно) | нет | -| `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`) + `statements-check` (свежесть копии общего кода `arm_statements`, см. ниже); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker | +| `make check-full` | `make check` + `make test-integration` + сборка docker-образа (`docker build`, с библиотекой `arm_statements` из `STATEMENTS_DIR`, см. ниже); для `arm-core` — расширенный набор, см. ниже | да, `make up` и Docker | | `make up` | поднимает инфраструктуру из `../docs/dev/docker-compose.yml`; постоянные сервисы `postgres`, `nats`, `minio` | Docker | Сводка шагов `make check`: `ruff check .` → `ruff format --check .` → `mypy src` (в `arm-core` — `mypy src scripts`) → @@ -51,56 +51,37 @@ docker compose -f ../docs/dev/docker-compose.yml --project-name arm-ka run --rm | Репозиторий | Шаги по порядку | |---|---| -| `scan-gateway` | `check` → `statements-check` → `test-integration` → `docker-build` (образ `scan-gateway:dev`); интеграционный тест поднимает эмулятор ИС Сканирование в процессе теста | -| `tabular-parser` | `check` → `statements-check` → `test-integration` → `docker-build` (образ `tabular-parser`) | -| `arm-core` | `check` → `statements-check` → `dbml-check` → `check-infra` → `test-integration` → `docker-build` → `web-check` | +| `scan-gateway` | `check` → `test-integration` → `docker-build` (образ `scan-gateway:dev`); интеграционный тест поднимает эмулятор ИС Сканирование в процессе теста | +| `tabular-parser` | `check` → `test-integration` → `docker-build` (образ `tabular-parser`) | +| `arm-core` | `check` → `dbml-check` → `check-infra` → `test-integration` → `docker-build` → `web-check` | -`statements-check` появляется в сервисе вместе с копией пакета (PR подключения хоста); до этого цели нет. +### Библиотека общего кода `arm_statements` -### Копия общего кода `arm_statements` и `sync-statements` +Общий код нормализации ББ/ОФР (`05-domain-rules.md` §11) — Python-библиотека в репозитории `RSHB/pylib_ai_arm_statements`: +дистрибутив `pylib-ai-arm-statements`, импорт `arm_statements` (AURORA-1677; до 27.10.2026 — копии `api/arm_statements/` +в сервисах и скрипт `sync-statements`, история — архив change `unified-statement-normalization`). Раздел описывает поведение +по спеке `statement-normalization` (требование «Поставка библиотеки потребителям»). -Общий код нормализации ББ/ОФР (`05-domain-rules.md` §11) живёт в репозитории `arm-statements` пакетом `arm_statements/`; -в `arm-core`, `tabular-parser` и `scan-gateway` он лежит **копией** — прямые правки копии запрещены (Р-1, design -`openspec/changes/archive/2026-10-19-unified-statement-normalization/design.md`). Скрипт и цели docs (`make -C docs sync-statements`, -`statements-check`, `test-sync-statements`) — arm-ka-docs#120; `statements-check` сервисов появляется с PR подключения -копии в каждом хосте (план 2, задачи 7–9) — до шага 3 выкладки. Раздел описывает поведение по спеке -`statement-normalization` (требование «`sync-statements`…»). - -- **Где копия:** `api/arm_statements/` (рядом с `api/src/`; импорт `import arm_statements` — `api/` в `sys.path`, как для - `src`). Пакет приносит свои тесты (`api/arm_statements/tests/`); конфиг линтеров пакета в копию не кладётся — хост держит - `arm_statements` в строгом блоке `mypy` и в `testpaths` (P2-1, P2-3); `ruff`, `pytest` и `mypy` хоста - охватывают копию: `ruff` и `pytest` — по каталогу `api/` (тесты пакета подключены как `testpaths`), `mypy` — `MYPY_PATHS` - хоста дополняется `arm_statements` (`mypy src arm_statements`, в `arm-core` — `mypy src scripts arm_statements`). -- **Маркер версии:** `api/arm_statements/VERSION` — полный хэш коммита `arm-statements`, из которого разложена копия; - пишет только скрипт. Сервис при старте читает его и пишет в результат `service.statements_version`; ядро сравнивает его - с `statements_version` задания при пересчёте после правки (`STATEMENTS_VERSION_MISMATCH`, `info`). -- **Скрипт:** `docs/scripts/sync-statements.sh`, запуск из workspace или `make -C docs - sync-statements SYNC_ARGS=…`. Источник — локальный checkout `arm-statements` рядом (`../arm-statements`) или - `--source `, ссылка — `origin/main` или `--ref `; хост — `--host arm-core|tabular-parser|scan-gateway` или - `--host-dir `. Без `--check` — раскладывает копию нужного коммита в новый worktree хоста на ветке фичи и пишет - `VERSION`, заводит issue и PR в хосте; `--dry-run` — то же без issue, проверок, коммита и PR. С `--in-place` — - раскладывает копию в рабочее дерево существующего worktree хоста без git-операций (подключение хоста; PR фичи, - которой нужна новая версия пакета; при прямой правке копии или недостающей зависимости — отказ `20`, ничего не - пишется). Обзор по трём хостам рядом — `make -C docs statements-check`; сборка снимка для контура - (`prepare-contour-snapshot.sh`) вызывает `--check` для хостов, где копия уже есть (Р-42). -- **`--check`, коды возврата** (файл `VERSION` в сравнении не участвует — в источнике его нет): `0` — копия равна дереву - `arm_statements/` коммита из `VERSION`, это коммит `--ref`, и копия перечня кодов равна docs; `10` — копия равна дереву - коммита из `VERSION`, но в `--ref` есть более новый (копия отстала), или байтовая копия перечня кодов - `arm_statements/rules/statement_code_orders.yaml` отличается от `docs/contracts/statement-code-orders.yaml` (перечень в - docs правился раньше `arm-statements` или пересинхронизации хоста) — предупреждение, не ошибка; `20` — копия отличается - от дерева коммита из `VERSION` (прямая правка), `VERSION` нет в истории источника, зависимости пакета (список - `dependencies` его `pyproject`) отсутствуют в зависимостях хоста, либо (только `arm-core`) код словаря имён - `api/rules/statement_lines.yaml` отсутствует в перечне своей редакции копии пакета; `2` — нет checkout - `arm-statements` или хоста, неизвестный аргумент. Файлы расхождения перечисляются в выводе. Порядок правки перечня - кодов: PR в docs → PR в `arm-statements` → `sync-statements` в хостах («сначала ядро»); до конца цепочки хосты получают - `10`. -- **Конфиг линтеров и тесты пакета:** пакет проходит самый строгий конфиг `ruff` / `mypy` среди хостов (в своём - репозитории), потому что конфиг хоста применяется к копии, а править копию нельзя; тесты пакета — со своим - `conftest.py`, `autouse`-фикстуры `conftest` хоста ограничены каталогом `tests/` хоста. -- **`statements-check` в `make check-full`:** `../docs/scripts/sync-statements.sh --check --host-dir .` (docs — `../docs` - или `ARM_KA_DOCS`); код `0` и `10` — цель зелёная (при `10` предупреждение печатается, в описание PR вписывается, что копия - отстала), `20` и `2` — цель красная. В `make check` и в пайплайн платформы не входит: тест, читающий соседний - репозиторий, уже ломал `main` (arm-core #33). +- **Версия:** семантическая, файл `VERSION` в корне библиотеки и тег `v<версия>` на коммите выпуска; опубликованная версия + не пересобирается с другим содержимым. В Nexus библиотеку публикует платформа. +- **Подключение:** в `api/requirements.txt` каждого сервиса — `pylib_ai_arm_statements==<версия>`; на платформе ставится из + Nexus. Вне банка `make venv` пропускает строки `pylib_ai*` (как `raisa_*`) и ставит библиотеку из checkout + `STATEMENTS_DIR` (по умолчанию `../pylib_ai_arm_statements`); после правки библиотеки — `make install`. Обе цели + сначала выполняют `statements-version`: `VERSION` checkout ≠ версии в `requirements.txt` — ошибка, checkout не на теге + `v<версия>` — предупреждение. `STATEMENTS_TAG` по умолчанию берётся из `requirements.txt`. +- **Сборка образа:** `make docker-build` передаёт checkout контекстом `statements` и отказывает, если `HEAD` checkout не + на теге `STATEMENTS_TAG`. compose (`dev/docker-compose.yml`) собирает все три сервиса с `STATEMENTS_DIR` из + `dev/.env` (по умолчанию `../../pylib_ai_arm_statements` от `docs/dev`). +- **Линтеры и тесты:** библиотека проверяется своим `make check` (ruff, mypy, pytest) в своём репозитории; `make check` + сервиса её исходники не обходит, `arm_statements` для `ruff` сервиса — сторонний пакет. +- **Маркер версии:** `service.statements_version` результата — версия установленного дистрибутива (`importlib.metadata`); + ядро сравнивает её с `statements_version` задания при пересчёте после правки (`STATEMENTS_VERSION_MISMATCH`, `info`). + У заданий до перехода в версии хэш коммита копии — расхождение ожидаемо (решение владельца 26.10.2026). +- **Перечень кодов форм:** источник — `contracts/statement-code-orders.yaml`, в библиотеке — байтовая копия + `arm_statements/rules/statement_code_orders.yaml`. Порядок правки: PR в docs → PR в библиотеку и новая версия → подъём + версии в сервисах («сначала ядро»). Сверку docs с библиотекой делает `make -C docs statements-orders-check` (входит в + `make -C docs check`, расхождение — предупреждение); что все коды словаря имён ядра есть в перечне — тест ядра + `test_name_dictionary_codes_in_code_orders`. Для `arm-core` дополнительно: @@ -181,10 +162,8 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу | `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` и `PyYAML` — зависимостями скриптов (после обновления репозитория — перезапустить) | | `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против текущих схем JSON Schema 2020-12, примеры `contracts/examples/v1/` — против схем прежнего MAJOR `*.v1.schema.json` (окно перехода на контракт 2.0), форма упорядоченных перечней кодов `contracts/statement-code-orders.yaml` (коды — 4 цифры формы; в варианте `full` код стоит 1 + (вхождений в `repeats`) раз, в `simplified` — не чаще); код 1 при любой ошибке | | `make validate-openspec` | `openspec validate --all --strict` — все спеки и незаархивированные change'и (один change: `make validate-openspec CHANGE=`) | -| `make check` | все проверки выше и `test-snapshot`, `test-sync-statements` | -| `make test-sync-statements` | сценарии `sync-statements.sh` на синтетических репозиториях: коды `--check`, `--in-place`, `--dry-run`, отказ при прямой правке | -| `make sync-statements` | копия пакета `arm_statements` в хост одной командой (worktree, issue, PR); `SYNC_ARGS="--host arm-core [--check\|--dry-run]"` | -| `make statements-check` | обзор свежести копий по трём хостам рядом (`arm-core`, `tabular-parser`, `scan-gateway`): строка итога на хост; в `check` не входит | +| `make check` | все проверки выше, `test-snapshot` и `statements-orders-check` | +| `make statements-orders-check` | `contracts/statement-code-orders.yaml` против байтовой копии в библиотеке (`STATEMENTS_DIR`, по умолчанию `../pylib_ai_arm_statements`); расхождение или нет checkout — предупреждение, код 0 | Без `make`: `python scripts/validate_contracts.py` (нужны `jsonschema` и `PyYAML`) и `openspec validate --strict`. Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их @@ -196,7 +175,7 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу (`*.v.schema.json`, `examples/v/` у ядра) — с одноимёнными; файлы docs другого MAJOR сервис не сверяет. Правило вводится во все три репозитория до слияния контракта v2.0 в docs (шаг 0 выкладки; спека `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»). Ядро держит копии обоих MAJOR. Правило реализовано файлом `api/tests/contract_identity.py`, побайтно одинаковым в трёх репозиториях; правка — сразу во всех трёх. Перечень кодов `contracts/statement-code-orders.yaml` -копируется не в сервисы, а в пакет `arm_statements` (сверяет `sync-statements --check`). +копируется не в сервисы, а в библиотеку `arm_statements` (сверяет `make statements-orders-check`). ## Порядок перед PR diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index dd9e413..1d3fa63 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -64,16 +64,12 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour Что делает скрипт (репозиторий-источник не меняется): -0. Для каждого сервиса, где в `` есть `api/arm_statements/`: проверяет свежесть копии пакета (`sync-statements.sh --check` - по дереву ``). Отстаёт — предупреждение и абзац в `SNAPSHOT.md`; копия правлена напрямую или чужая — снимок не - делается (код 1); нет `arm-statements` рядом или подходящего Python — предупреждение, снимок делается. Нет - `api/arm_statements/` — проверка молча пропускается. 1. `git archive ` → `/tmp/contour//` — дерево коммита без `.git` и без игнорируемых файлов. 2. Удаляет `.claude/`, `.codegraph/`, `CLAUDE.md`, `openspec/config.yaml`, `docs/superpowers/` на любой глубине (в т.ч. `web/` arm-core; опустевший `docs/` тоже), `Dockerfile` и `.dockerignore` — в корне. 3. Правит `Makefile`: цель `venv` → `python3.11 -m venv` + `pip install -r requirements-dev.txt` с `setup/pip.conf` - (переменная `PYTHON`, по умолчанию `python3.11`); удаляет цель `docker-build` и её упоминания в `check-full`, `.PHONY`, - `help`. + (переменная `PYTHON`, по умолчанию `python3.11`); удаляет цели `docker-build`, `install` и `statements-version` + (установка библиотеки `arm_statements` из соседнего checkout и её сверка) и их упоминания в `check-full`, `.PHONY`, `help`. 4. Заменяет ссылки на внешний Gitea на `RSHB/#`. 5. Только arm-core: урезает `web/package.json` до зависимостей сборки (раздел 3.2) и правит цель `web-check`. 6. Пишет `SNAPSHOT.md` (источник, хеш, дата, что вырезано и какие пакеты/скрипты фронта убраны). @@ -84,9 +80,13 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour `git.aepif.ru`, на месте платформенные файлы (`raisa_config.yml`, `setup/`, `run_server.py`, `requirements.txt`, `.env.test`, `contracts/`). Любая проблема — код возврата 1 и список. -Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`); проверка -копии `arm_statements` (пропуск, отставание, правка) -— в `make test-snapshot` (нужен Python ≥ 3.11 с PyYAML, `make venv`). +Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`). + +Библиотека общего кода `arm_statements` в снимки сервисов не входит: сервисы ставят её из Nexus по строке +`pylib_ai_arm_statements==<версия>` в `api/requirements.txt` (цель `venv` снимка — `pip install -r requirements-dev.txt` +через `setup/pip.conf`). Нужная версия должна быть в Nexus до выкладки сервисов; ядро и оба сервиса с новой версией +библиотеки выкладываются одной поставкой. Репозиторий библиотеки (`RSHB/pylib_ai_arm_statements`) скрипт снимка не +обрабатывает. ### 3.1. Фронт arm-core: `web/` ведётся в arm-core diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 488df4e..1e5a410 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -124,6 +124,8 @@ services: profiles: ["services"] build: context: ../../arm-core + additional_contexts: + statements: ${STATEMENTS_DIR:-../../pylib_ai_arm_statements} image: arm-core:dev restart: unless-stopped depends_on: @@ -156,6 +158,8 @@ services: profiles: ["services"] build: context: ../../tabular-parser + additional_contexts: + statements: ${STATEMENTS_DIR:-../../pylib_ai_arm_statements} image: tabular-parser:dev restart: unless-stopped depends_on: @@ -207,6 +211,8 @@ services: profiles: ["services"] build: context: ../../scan-gateway + additional_contexts: + statements: ${STATEMENTS_DIR:-../../pylib_ai_arm_statements} image: scan-gateway:dev restart: unless-stopped depends_on: diff --git a/openspec/changes/document-statements-library-contract/.openspec.yaml b/openspec/changes/document-statements-library-contract/.openspec.yaml new file mode 100644 index 0000000..f56fe13 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-23 diff --git a/openspec/changes/document-statements-library-contract/design.md b/openspec/changes/document-statements-library-contract/design.md new file mode 100644 index 0000000..7e95f89 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/design.md @@ -0,0 +1,56 @@ +# Design + +## Context + +Код библиотеки, подключение в сервисах и их change — в PR задачи AURORA-1677 (arm-core #178, tabular-parser #70, +scan-gateway #52, `RSHB/pylib_ai_arm_statements`). Этот change — сквозная часть в docs: контракт, спеки, контекст и +инструменты docs, которые обслуживали копии. Первая версия change (23.10.2026) обновляла только описания контракта и +добавляла заметки-баннеры; при ревью 27.10.2026 change расширен до полного перевода docs на библиотеку. + +## Goals / Non-Goals + +**Goals:** docs не описывают копии `api/arm_statements/` и `sync-statements` как действующие; сверки, которые делал +`sync-statements --check`, не теряются; снимок для контура проходит на сервисах с библиотекой. + +**Non-Goals:** алгоритмы §7/§11, структура контракта, DBML, перенос репозитория библиотеки в контур. + +## Decisions + +1. **Что делал `sync-statements --check` и куда это ушло.** Равенство копии источнику и «копия отстала» — больше не + нужны (копий нет, версия закреплена в `requirements.txt`). Зависимости пакета в зависимостях хоста — проверяет + установщик pip. Коды словаря имён ядра в перечне — тест ядра `test_name_dictionary_codes_in_code_orders` (был и + раньше, работает с установленной библиотекой). Перечень кодов docs против копии в библиотеке — новая цель + `statements-orders-check` в `make -C docs check`. +2. **`statements-orders-check` — предупреждение, не ошибка.** Как и код `10` прежнего `--check`: порядок правки + «docs → библиотека → версия в сервисах», и docs законно бывает впереди до выпуска библиотеки. Нет checkout + библиотеки рядом — тоже предупреждение (docs проверяется и без соседей). +3. **Снимок для контура вырезает цель `install`.** Цель ставит библиотеку из соседнего checkout через `uv`; в контуре + библиотека приходит из Nexus по `requirements.txt` через цель `venv` снимка. Без этого проверка снимка «нет uv» + падает на всех трёх сервисах (проверено на ветках PR 27.10.2026). +4. **Требования спек — REMOVED + ADDED, а не MODIFIED.** OpenSpec не даёт убрать сценарий через MODIFIED, а сценарии + про копии (прямая правка копии, тесты копии) больше не верны. Старые требования удалены с Reason/Migration, новые — + под новыми именами. +5. **`STATEMENTS_VERSION_MISMATCH` на заданиях до перехода не исправляется** (решение владельца 26.10.2026): у таких + заданий `statements_version` — хэш коммита копии, у ядра — версия библиотеки; запись `info` ожидаема и на статус не + влияет. Записано в спеку `statement-normalization` и в §9.5 `05-domain-rules`. + +## Decisions made without discussion + +- 27.10.2026 — `statements-orders-check` не валит `make check` при расхождении перечня с библиотекой (решение 2). + Обоснование — прежняя семантика кода `10`. Цена ошибки — расхождение перечня docs и библиотеки может жить до + следующего выпуска незамеченным, если предупреждение не читать. +- 27.10.2026 — шапка `contracts/statement-code-orders.yaml` (упоминала `sync-statements`) поправлена в docs сейчас, + байтовая копия уходит в библиотеку выпуском 0.1.2. До него `statements-orders-check` предупреждает о расхождении с + `v0.1.1` — это ожидаемо (данные перечня не менялись, только комментарий). Цена ошибки — предупреждение в `make check` + docs до выпуска 0.1.2. +- 27.10.2026 — комментарий `recognition_job.statements_version` в DBML v5.23 и `entities-v5.23.md` («коммит копии») не + правится: правка DBML требует новой версии модели. Цена ошибки — устаревшее описание поля до следующей версии DBML. +- 27.10.2026 — описание `service.statements_version` в контракте оставлено как в первой версии change («маркер VERSION, + если есть, иначе версия дистрибутива»), хотя в установленном пакете маркера нет: оно совпадает с кодом + `arm_statements/version.py` библиотеки `v0.1.1`. Цена ошибки — описание сложнее фактического поведения. + +## Risks / Trade-offs + +- [Репозиторий библиотеки не переносится в контур скриптом снимка] → выкладка требует, чтобы нужная версия уже была в + Nexus; процедура переноса — вопрос владельцу, в этом change не решается. +- [Старые change в архиве ссылаются на `sync-statements`] → архив — история, не правится. diff --git a/openspec/changes/document-statements-library-contract/proposal.md b/openspec/changes/document-statements-library-contract/proposal.md new file mode 100644 index 0000000..fb54184 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/proposal.md @@ -0,0 +1,50 @@ +# Proposal + +## Why + +AURORA-1677: общий код нормализации ББ/ОФР `arm_statements` переведён из копий в сервисах (`api/arm_statements/`, +скрипт `sync-statements`) в Python-библиотеку `pylib-ai-arm-statements` (репозиторий `RSHB/pylib_ai_arm_statements`, +тег `v0.1.1`), которую сервисы ставят зависимостью. Сквозная документация, спеки docs и скрипты docs ещё описывают +копии: `sync-statements`, `statements-check`, шаг 0a снимка для контура, маркер версии «коммит копии». + +## What Changes + +- Контракт `recognition-result.schema.json`: два `description` — нормализация библиотекой и источник + `service.statements_version`; структура, обязательность и ограничения не меняются, `schema_version` остаётся 2.0. + Копии схемы в arm-core, tabular-parser и scan-gateway обновляются в их PR той же задачи. +- **BREAKING (инструменты docs):** удалены `scripts/sync-statements.sh`, `scripts/test_sync_statements.sh`, + `scripts/statements_check.py` и цели `sync-statements`, `statements-check`, `test-sync-statements`. Вместо сверки + перечня кодов в копиях — цель `statements-orders-check` (в `make check`): `contracts/statement-code-orders.yaml` + против байтовой копии в библиотеке, расхождение — предупреждение. +- `scripts/prepare-contour-snapshot.sh`: убран шаг 0a (свежесть копии); в снимке вырезается цель `install` (ставит + библиотеку из соседнего checkout через `uv`) — без этого снимок сервисов на библиотеке не проходит проверку «нет uv». +- Спеки docs: `statement-normalization` — библиотека и её поставка вместо копий; `quality-checks` — `make check` и + `make check-full` сервисов без копии и `statements-check`; `arm-core` и `recognition-contract` — формулировки + «копия» → «библиотека». +- Контекст: `03-architecture`, `05-domain-rules` (§7 «Ядро», §9.5, §11), `00-workspace-layout`, `dev/checks.md`, + `dev/contour-transfer-guide.md`. + +## Capabilities + +### New Capabilities + +Нет. + +### Modified Capabilities + +- `statement-normalization`: библиотека вместо пакета с копиями; поставка, версия, порядок правки перечня кодов. +- `quality-checks`: проверки сервисов без копии `arm_statements` и `statements-check`. +- `arm-core`: перечень кодов и версия — библиотеки, сверка словаря имён — тестом ядра. +- `recognition-contract`: `service.statements_version` — версия установленной библиотеки. + +## Impact + +Связанные PR задачи AURORA-1677: arm-core #178, tabular-parser #70, scan-gateway #52, библиотека +`RSHB/pylib_ai_arm_statements` (PR #1, тег `v0.1.1`). Дочерние change: arm-core `use-installed-statements-library`, +tabular-parser `use-arm-statements-library`, scan-gateway `use-statements-library`. Выкладка — одной поставкой ядра и +обоих сервисов. Миграций БД нет. + +## Non-goals + +Изменение алгоритмов §7/§11, контракта (кроме описаний), DBML; перенос репозитория библиотеки в контур (скрипт снимка +его не обрабатывает). diff --git a/openspec/changes/document-statements-library-contract/specs/arm-core/spec.md b/openspec/changes/document-statements-library-contract/specs/arm-core/spec.md new file mode 100644 index 0000000..8d77024 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/specs/arm-core/spec.md @@ -0,0 +1,94 @@ +## MODIFIED Requirements + +### Requirement: Предупреждения и нарушения документа — `document_issue` +Ядро SHALL хранить предупреждения и нарушения документа в `document_issue`: вид (`warning` | `relation` | `soft`), код, +уровень (`error` | `info`), сообщение, список `line_id`, `column_id` (колонка формы) или `column_index` (столбец +предупреждения, которого нет среди колонок формы), поле шапки, источник (`service` | `core`); для нарушений — `codes` +(коды цели и членов), текст соотношения, `expected`, `actual`. Нарушение из `checks.relations_failed` — `kind = +relation`, `code = RELATION_FAILED`, `severity = error`; из `soft_relations_failed` — `kind = soft`, `code = +RELATION_SOFT`, `severity = info`; `line_ids` — все строки `ok` кодов `codes` в документе. При загрузке пишутся +предупреждения и `checks` результата (источник `service`) и проверки ядра уровня `info` (источник `core`): +`CODE_NOT_IN_DICTIONARY` (кода нет в словаре имён редакции документа), `CODE_NOT_IN_EDITION` (код не из перечня +редакции документа и его варианта — по перечню кодов библиотеки `arm_statements`), `NAME_MISMATCH`, `RESULT_STATUS_INCONSISTENT`. +**Редакция документа** для этих проверок — `edition` точки входа пакета для пересчёта (`statement-normalization`): +редакция с лучшей подпоследовательностью кодов строк `ok` документа, при равенстве — по эффективной дате; так бланк 66н, +сданный в 2025 году, проверяется по `66n`, а не по дате. Поле `edition` ответа `GET …/statement` и выбор шаблонных +строк по-прежнему — по дате периода (§7 «Полный состав формы»). `document_issue` ведётся у **любого** документа файла: у +ОСВ — предупреждения `osvDocument.warnings` результата `2.0` (вид `warning`, источник `service`, `OSV_*`, `info`). При перезагрузке файла записи документа +удаляются вместе с ним. При пересчёте после правки записи `relation` и `soft` заменяются целиком независимо от источника; +записи `warning` источника `service` сохраняются, кроме снимаемых правкой: правка `header.report_date` (в том числе +подтверждение тем же значением) снимает `PERIOD_UNKNOWN`, `REPORT_DATE_UNPARSED`, `REPORT_DATE_UNCERTAIN`; правка +`header.inn` или `header.organization_name` — `NO_ORGANIZATION_NO_INN`; правка `columns[].period_date` — +`COLUMN_WITHOUT_DATE` и `COLUMN_DATE_UNCERTAIN` этой колонки; прочие `error` сервиса снимаются только перезагрузкой +файла. При удалении шаблонной строки её `id` SHALL убираться из `line_ids` всех записей документа. `GET …/statement` и +`POST …/confirm` SHALL читать нарушения §7 только из `document_issue`. + +#### Scenario: Дата колонке +- **WHEN** у колонки предупреждение `COLUMN_WITHOUT_DATE`, аналитик ставит ей `period_date` через PATCH +- **THEN** предупреждение удаляется, статус пересчитывается + +#### Scenario: Неснимаемое предупреждение +- **WHEN** у документа `SEGMENT_COLUMNS_MISMATCH`, аналитик правит значения +- **THEN** предупреждение остаётся; документ можно подтвердить вручную (оно подтверждение не блокирует) + +#### Scenario: Бланк 66н в 2025 году без ложного info +- **WHEN** форма Межениновской за 3 кв. 2025 на бланке 66н с кодом 1120 (в перечне `fsbu4` его нет) +- **THEN** редакция документа для проверок ядра — `66n` (подпоследовательность длиннее), `CODE_NOT_IN_EDITION` у 1120 нет + +#### Scenario: Пропущенная колонка +- **WHEN** в результате предупреждение `COLUMN_SKIPPED` с `column_index = 5`, колонки 5 среди `columns[]` нет +- **THEN** запись `document_issue` с `column_index = 5` и `column_id = null`; экран показывает её у документа + +### Requirement: Пересчёт после правки +После каждого PATCH формы ядро SHALL пересчитывать `checks`, `needs_review`, `form_variant`, родителей строк `detail` +и статус точкой входа пакета для пересчёта по сохранённым данным (`statement-normalization`; конвенция знака — по +`source_text`), с правилами: `is_modified` или `is_confirmed` значения ⇒ `needs_review = false`; `line_code_modified` или +`is_confirmed` строки ⇒ `needs_review` строки = `false`; значения строк `detail` — `needs_review = false` (Р-23), у +строки, выведенной из `detail` назначением кода, `needs_review` её значений считается по общему правилу; акцепт ⇒ +жёсткие нарушения не учитываются; правленые ячейки в голосование знака не входят. Результат пишется: `needs_review` +строк и значений, `form_variant`, `parent_row_index` всех строк `detail` (ближайшая строка `ok` выше по `row_index`; +нет такой — `null`, строка остаётся `detail`), записи `document_issue` видов `relation` / `soft`, статус. Отличие `statements_version` задания от версии библиотеки ядра (версия установленного дистрибутива `pylib-ai-arm-statements`) — `document_issue` +`STATEMENTS_VERSION_MISMATCH` (`info`); у задания без версии (`1.x`) запись не пишется. Один PATCH — один пересчёт и одна +запись аудита `RECOGNIZED_DATA_EDIT`. Акцепт SHALL сбрасываться правкой данных — суммы, кода или статуса строки, даты +колонки, поля шапки; подтверждение или снятие подтверждения (`confirmed`) и подтверждение даты отчёта или даты колонки +тем же значением акцепт MUST NOT сбрасывать (Р-25, S-47). + +#### Scenario: Подтверждение ячейки без правки +- **WHEN** аналитик подтверждает ячейку с `needs_review = true` без изменения суммы +- **THEN** `is_confirmed = true`, `confirmed_by/at`, `is_modified` не ставится, `needs_review = false`, аудит; акцепт + проверок, если был, сохраняется + +#### Scenario: Снятие подтверждения +- **WHEN** аналитик снимает подтверждение с ячейки (`confirmed: false`), которая не покрыта соотношением +- **THEN** `is_confirmed = false`, `needs_review = true` после пересчёта, аудит + +#### Scenario: Родитель уходит в расшифровки +- **WHEN** у строки 1150 две расшифровки (`detail`, `parent_row_index` = 1150), аналитик отмечает саму 1150 расшифровкой +- **THEN** после пересчёта родитель всех трёх строк `detail` — ближайшая строка `ok` выше 1150; строк над ними нет — `null` + (на экране — в начале раздела) + +### Requirement: Словари ядра и достройка шаблонных строк +Копия словаря §7 ядра (`api/rules/statement_checks.yaml`) SHALL удаляться: проверки ядро считает библиотекой +`arm_statements`. `api/rules/statement_lines.yaml` SHALL оставаться только словарём имён (код → наименования по +редакциям): у кода может быть несколько имён (синонимы и законные повторы кода в бланке, Р-20), загрузчик повтор кода в +редакции MUST NOT запрещать; сверка наименования — с любым именем кода; шаблонная строка берёт первое имя. Порядок и +состав редакций — из перечня кодов библиотеки; что все коды словаря имён есть в перечне своей редакции, сверяет тест +ядра `test_name_dictionary_codes_in_code_orders` (в `make check`). В PR перехода ядра на v2.0 словарь имён SHALL быть дополнен: в редакции `fsbu4` — вторые имена законных +повторов 1220 «НДС, начисленный по отгрузке», 1520 «Обязательство по аренде», 2410 «Налог при упрощенной системе +налогообложения» (Р-20) и коды печатной формы 1С 1105 «Гудвил», 1130 «Нематериальные поисковые активы», 1140 +«Материальные поисковые активы», 1530 «Доходы будущих периодов», 2460 «Прочее» (имена — по `fixtures/ocr` и шаблонам +банка); без этого законные повторы РБ, Счастливочной и aep_6 и любой неуверенный 1105 дают `NAME_MISMATCH`. Шаблонные строки достраиваются по словарю имён (одна на код); код, у которого есть хотя бы одна строка `ok` из +файла, шаблоном MUST NOT достраиваться; строки `unreadable` / `missing` не заменяются шаблонной строкой молча. + +#### Scenario: Код есть только у строки unreadable +- **WHEN** у формы нет строки `ok` с кодом 1250, но есть строка `unreadable` «ИЧ,250» +- **THEN** шаблонная строка 1250 достраивается пустой, строка `unreadable` показывается отдельно со статусом + +#### Scenario: Два имени у кода +- **WHEN** в редакции `fsbu4` словаря имён код 1520 записан дважды — «Кредиторская задолженность» и «Обязательство по + аренде» +- **THEN** словарь загружается; шаблонная строка 1520 одна, с именем «Кредиторская задолженность» + +#### Scenario: Гудвил с заголовком раздела в имени +- **WHEN** строка 1105 (уверенность 67) с наименованием «АКТИВ I. Внеоборотные активы Гудвил» +- **THEN** справочное «Гудвил» (≥ 5 символов) входит в него — совпало, `NAME_MISMATCH` нет diff --git a/openspec/changes/document-statements-library-contract/specs/quality-checks/spec.md b/openspec/changes/document-statements-library-contract/specs/quality-checks/spec.md new file mode 100644 index 0000000..9f24d94 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/specs/quality-checks/spec.md @@ -0,0 +1,54 @@ +## ADDED Requirements + +### Requirement: `make check` — быстрые проверки сервиса без Docker +Каждый сервисный репозиторий SHALL предоставлять цель `make check`, выполняющую из `api/`: `ruff check .`, +`ruff format --check .`, `mypy src` (в `arm-core` — `mypy src scripts`), `pytest -m "not integration"`, тест +идентичности вендоренного контракта с `../docs/contracts` (схема копии — с `<имя>.v.schema.json` своего `MAJOR`, +если такой файл есть в docs, иначе с основным; примеры — с `examples/v/`, если там есть одноимённый файл, иначе с +`examples/`; правило — спека `recognition-contract`, требование «Копия контракта в сервисе идентична источнику»). Цель SHALL завершаться ненулевым кодом при первой упавшей проверке и MUST NOT требовать +запущенного Docker, PostgreSQL или NATS. Библиотеку `arm_statements` `make check` сервиса MUST NOT проверять линтерами и +её тесты не гоняет: она установлена в окружение и проверяется своим `make check` в репозитории библиотеки. + +#### Scenario: Файл не отформатирован +- **WHEN** в репозитории есть файл, не отформатированный `ruff format` +- **THEN** `make check` завершается с кодом 1 на шаге `ruff format --check`, следующие шаги не выполняются + +#### Scenario: Чистый репозиторий +- **WHEN** все проверки проходят +- **THEN** `make check` завершается с кодом 0 за время, не превышающее нескольких минут, без обращения к сети + +#### Scenario: Библиотека не проверяется сервисом +- **WHEN** в `tabular-parser` установлена библиотека `pylib-ai-arm-statements` +- **THEN** `ruff`, `mypy` и `pytest` сервиса её исходники не обходят; импорты `arm_statements` для `ruff` — сторонние + +### Requirement: `make check-full` — полная проверка сервиса против compose +Каждый сервисный репозиторий SHALL предоставлять цель `make check-full`: `make check` плюс `pytest -m integration` против +инфраструктуры `docs/dev/docker-compose.yml` (PostgreSQL 16, NATS, MinIO, для `scan-gateway` — эмулятор). Для `arm-core` +цель SHALL дополнительно выполнять «`alembic upgrade head` → `alembic downgrade base` → `alembic upgrade head`» на чистой +БД и `python scripts/check_dbml_vs_orm.py` против текущей версии модели (`../docs/model/data-model-v<текущая>.dbml`, путь — переменная `DBML` Makefile). Сборка образа (`docker-build`) SHALL +собирать его с библиотекой из `STATEMENTS_DIR` и отказывать, если её checkout не на теге `STATEMENTS_TAG` (спека +`statement-normalization`, требование «Поставка библиотеки потребителям»). `make check-full` SHALL быть выполнен перед +созданием PR, его итог вписывается в описание PR. + +#### Scenario: Расхождение ORM и DBML +- **WHEN** в ORM добавлена колонка, которой нет в DBML +- **THEN** шаг `check_dbml_vs_orm` падает с перечнем расхождений, `make check-full` завершается с кодом 1 + +#### Scenario: Миграция без отката +- **WHEN** `downgrade` ревизии не удаляет созданный ею объект +- **THEN** шаг «upgrade → downgrade → upgrade» падает + +#### Scenario: Библиотека не на теге +- **WHEN** checkout `../pylib_ai_arm_statements` стоит не на теге `STATEMENTS_TAG` +- **THEN** шаг `docker-build` падает с подсказкой, `make check-full` красный + +## REMOVED Requirements + +### Requirement: `make check` — быстрые проверки без Docker +**Reason**: требование описывало проверку копии `arm_statements` в сервисе (`mypy src arm_statements`, тесты копии); +копий больше нет (AURORA-1677). +**Migration**: «`make check` — быстрые проверки сервиса без Docker». + +### Requirement: `make check-full` — полная проверка против compose +**Reason**: шаг `statements-check` (`sync-statements.sh --check`) удалён вместе с копиями (AURORA-1677). +**Migration**: «`make check-full` — полная проверка сервиса против compose». diff --git a/openspec/changes/document-statements-library-contract/specs/recognition-contract/spec.md b/openspec/changes/document-statements-library-contract/specs/recognition-contract/spec.md new file mode 100644 index 0000000..434d477 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/specs/recognition-contract/spec.md @@ -0,0 +1,69 @@ +## MODIFIED Requirements + +### Requirement: Файл результата +Файл результата SHALL соответствовать `docs/contracts/recognition-result.schema.json` v2.0: один файл (`stored_file`) +→ один `job_id` → массив `documents` из `osvDocument` и/или `statementDocument`; денежные значения — строки +`^-?\d+(\.\d{1,2})?$`; `confidence` — 0–100. Пустой массив `documents` допустим только при `status = error`. +Предупреждение уровня файла пишется там, где документа нет (потерянный json, таблица ИС, исключение разбора таблицы, +лист «прочее»). +`service.statements_version` — версия установленной библиотеки `arm_statements` (дистрибутив +`pylib-ai-arm-statements`), которой посчитан результат (обязателен при наличии `statementDocument`); у результатов, +посчитанных до перехода на библиотеку, — хэш коммита копии общего кода. `metrics.warnings[]` — предупреждения уровня файла (структура `warning` с `locator`). + +Каждый документ (и `osvDocument`, и `statementDocument`) SHALL иметь `status` (`done` | `needs_check`) и `warnings[]` — +структуры `{code, severity, message, line_refs?, column_index?, field?}` (`line_refs` — только `ref` строк из `lines[]`); +у `osvDocument` все предупреждения — `info`, `status = done`, когда контрольные суммы §1.4 листа сошлись, иначе +`needs_check`. Остальное в `osvDocument` — как в v1.2: у строки `rows[]` необязательное поле `extra` — объект «заголовок +колонки → строка или null» для колонок внутри таблицы, не сопоставленных с шестью суммами и ИНН (§1.2 «не терять»); у +шапки — `period_start`, `period_end`, `is_from_year_start`. + +`statementDocument` SHALL иметь `form_variant` (`full` | `simplified` | `null`); `header` — поля `organization_name`, +`inn`, `report_date`, `units`, `okud`, `okpo`, `okopf`, `okfs`, `okei`, `address` и для каждого `<поле>_text` и +`<поле>_confidence`, плюс `needs_review_fields[]`; `columns[]` — `column_index` (номер столбца в сетке кандидата: у OCR +— `column_index` ИС, у Excel — номер столбца листа с 0), `label`, `label_confidence`, +`period_date`, `period_kind` (`absolute` | `relative:current` | `relative:prev_year_end` | +`relative:prev_prev_year_end` | `relative:prev_period` | `none`); `lines[]` — `row_index`, `ref` (у Excel `<лист>!`, +имя листа в кавычках по правилу Excel), `line_status` (`ok` | +`unreadable` | `missing` | `detail`), `line_code` (строка 4–5 цифр или `null`; `null` ⇔ статус не `ok`), +`line_code_text`, `line_code_confidence`, `line_name` (строка или `null`), `line_name_confidence`, `parent_ref` (у +`detail`), `needs_review`, `values[]` — `column_index`, `amount`, `string_value`, `text`, `confidence`, +`is_recognized`, `needs_review`. `checks` SHALL присутствовать у `BALANCE` / `INCOME` со строками и отсутствовать у +документа без строк; `relations_failed` и `soft_relations_failed` — `{column_index, relation, codes[], expected, +actual}`; `duplicate_line_codes` удалён. `document_type` — `BALANCE` | `INCOME` | `DISCLOSURE` (`OTHER` в v2.0 не +порождается: лист «прочее» — предупреждение файла `SHEET_OTHER`, документа нет; значение остаётся только в схеме 1.x и в +`document_type` ядра для документов прежних результатов); `form_variant` обязателен (`null` у `DISCLOSURE`); у документа +без строк `columns` и `lines` — пустые массивы. `ref` уникален в документе. + +#### Scenario: Результат валидируется примерами +- **WHEN** CI сервиса запускает тесты контракта +- **THEN** все файлы `docs/contracts/examples/result-*.json` проходят схему файла результата v2.0, а все + `request-*.json` / `result-message-*.json` — схемы сообщений + +#### Scenario: Лишняя колонка сохранена +- **WHEN** в ОСВ между ИНН и суммами есть колонка «КПП» +- **THEN** у строк контрагентов `extra = {"КПП": "<значение>"}`, файл проходит схему v2.0 + +#### Scenario: Форма из Excel с проверками +- **WHEN** tabular-parser разобрал книгу с балансом на двух листах, ОФР со строкой без кода под 2110 и пояснениями + (`examples/result-statement-excel.json`) +- **THEN** файл проходит схему v2.0: `BALANCE` с `locator` из двух листов, `status`, `checks` и мягким нарушением; + `INCOME` со строкой `detail` (`line_code = null`, `parent_ref`); `DISCLOSURE` с пустыми `columns`/`lines` и без `checks` + +#### Scenario: Форма из OCR с сомнительными значениями +- **WHEN** scan-gateway разобрал ББ, где одна сумма собрана из грязного текста и покрыта соотношением, а одна строка + `unreadable` (`examples/result-statement-minimal.json`) +- **THEN** файл проходит схему v2.0; у покрытого значения `is_recognized = false`, `needs_review = false`, `text` — + сырой; у строки `unreadable` `line_code = null`, `line_code_text` — сырой текст; документ `needs_check` + +#### Scenario: Границы периода ОСВ +- **WHEN** в шапке ОСВ период «Октябрь 2024 г. – Сентябрь 2025 г.» +- **THEN** `period_start = "2024-10-01"`, `period_end = "2025-09-30"`, `is_from_year_start = false`; документ ОСВ с + `status` и `warnings`, файл проходит схему v2.0 + +#### Scenario: Документ OTHER в v2.0 +- **WHEN** в результате v2.0 `statementDocument` с `document_type = OTHER` или без `form_variant` +- **THEN** файл не проходит схему v2.0 + +#### Scenario: Строка без кода с кодом в поле +- **WHEN** у строки `line_status = unreadable` передан непустой `line_code` +- **THEN** файл не проходит схему v2.0 diff --git a/openspec/changes/document-statements-library-contract/specs/statement-normalization/spec.md b/openspec/changes/document-statements-library-contract/specs/statement-normalization/spec.md new file mode 100644 index 0000000..55872c3 --- /dev/null +++ b/openspec/changes/document-statements-library-contract/specs/statement-normalization/spec.md @@ -0,0 +1,91 @@ +## ADDED Requirements + +### Requirement: Библиотека общего кода `arm_statements` +Общий код нормализации ББ/ОФР SHALL жить в репозитории `RSHB/pylib_ai_arm_statements` Python-библиотекой: дистрибутив +`pylib-ai-arm-statements`, импорт — `arm_statements`, свои тесты, конфиг линтеров и `make check`. Библиотека SHALL +содержать: типы шага 1 (`FormCandidate`, `Cell`; сериализуемые в json dataclass-ы), шаги 2–8, общие разборы текста +(двойники, даты, суммы, коды), словари — проверок §7 `statement_checks.yaml` (полный и упрощённый наборы), периодов +`statement_periods.yaml`, заголовков форм и ОКУД, подписей колонок («Код», «Наименование», исключения), букв-двойников, +единиц измерения `statement_units.yaml` (написания и коды ОКЕИ), упорядоченные перечни кодов форм по редакциям (`66n`, +`fsbu4`) и вариантам (`full`, `simplified`) без наименований — байтовая копия `docs/contracts/statement-code-orders.yaml` +как `arm_statements/rules/statement_code_orders.yaml`, диапазоны кодов — и загрузчик словарей. Словари SHALL входить в +дистрибутив данными пакета (wheel и sdist), установленная библиотека читает их без исходного репозитория. Словаря +«код → наименование» в библиотеке MUST NOT быть (Р-3). Библиотека SHALL владеть своими типами входа и выхода; +конвертацию выхода в модель контракта делает потребитель. Golden-тесты — внутри библиотеки на синтетических и +обезличенных данных; тесты на реальных фикстурах остаются в сервисах и соседние репозитории не читают. Зависимости +библиотеки — только те, что есть в зеркале банка (сейчас PyYAML), Python — 3.11 (версия платформы). + +В tabular-parser, scan-gateway и arm-core библиотека SHALL подключаться зависимостью и импортироваться по имени +`arm_statements`; поставку, версию и порядок её подъёма задаёт требование «Поставка библиотеки потребителям». + +#### Scenario: Наименований в библиотеке нет +- **WHEN** ревьюер ищет в библиотеке соответствие «код → наименование» +- **THEN** его нет: в перечне кодов только коды, наименования строк — словарь ядра + +#### Scenario: Словари в установленном пакете +- **WHEN** wheel библиотеки установлен в чистое окружение без исходного репозитория +- **THEN** `import arm_statements` и загрузка стандартных правил работают, все YAML-словари на месте + +### Requirement: Поставка библиотеки потребителям +Версия библиотеки SHALL быть семантической (`MAJOR.MINOR.PATCH`), записываться в файл `VERSION` корня репозитория +библиотеки и помечаться тегом `v<версия>` на коммите выпуска; опубликованная версия MUST NOT пересобираться с другим +содержимым. Публикацию в Nexus выполняет платформа (CI в Gitea нет). + +Потребители (`arm-core`, `tabular-parser`, `scan-gateway`) SHALL держать в `api/requirements.txt` строку +`pylib_ai_arm_statements==<версия>`; копии исходников библиотеки в потребителях MUST NOT быть. На платформе библиотека +ставится из Nexus по этой строке. Вне банка `make venv` SHALL пропускать строки `pylib_ai*` (как `raisa_*`) и ставить +библиотеку из локального checkout `STATEMENTS_DIR` (по умолчанию `../pylib_ai_arm_statements`); `make install` +переустанавливает её без пересоздания окружения. Обе цели SHALL сначала сверять checkout (цель `statements-version`): +`VERSION` checkout не равен версии из `requirements.txt` или checkout нет — ошибка; checkout не на теге `v<версия>` — +предупреждение (правка библиотеки до выпуска). `STATEMENTS_TAG` по умолчанию — `v<версия>` из `requirements.txt`. +`make docker-build` SHALL передавать checkout библиотеки контекстом сборки `statements` и отказывать, если `HEAD` +checkout не совпадает с тегом `STATEMENTS_TAG`. +`ruff` и `mypy` потребителя библиотеку не проверяют (она проверяется своим `make check`), `arm_statements` в конфиге +`ruff` потребителя — сторонний пакет. + +Маркер версии в результате (`service.statements_version`) и версия ядра для `STATEMENTS_VERSION_MISMATCH` SHALL +браться из метаданных установленного дистрибутива (`importlib.metadata`). У заданий, посчитанных до перехода, в +`recognition_job.statements_version` остаётся хэш коммита копии; расхождение с версией ядра даёт +`STATEMENTS_VERSION_MISMATCH` (`info`) — это ожидаемо и не исправляется (решение владельца 26.10.2026). + +Порядок правки перечня кодов: PR в docs (`contracts/statement-code-orders.yaml`) → PR в библиотеку (байтовая копия +yaml) и новая версия → подъём версии в потребителях «сначала ядро». Пока цепочка не дошла до библиотеки, +`statements-orders-check` в docs предупреждает, но не краснеет. Подъём версии в потребителях выкладывается одной +поставкой ядра и обоих сервисов. + +#### Scenario: Версия общего кода в результате +- **WHEN** сервис собирает результат с установленной библиотекой версии `0.1.1` +- **THEN** в результате `service.statements_version = "0.1.1"` + +#### Scenario: Сборка образа не с того коммита +- **WHEN** в `../pylib_ai_arm_statements` checkout стоит на коммите после тега `v0.1.1`, а `STATEMENTS_TAG = v0.1.1` +- **THEN** `make docker-build` завершается ошибкой «HEAD библиотеки не совпадает с v0.1.1», образ не собирается + +#### Scenario: Checkout библиотеки другой версии +- **WHEN** в `api/requirements.txt` сервиса `pylib_ai_arm_statements==0.1.2`, а в `../pylib_ai_arm_statements` `VERSION` = `0.1.1` +- **THEN** `make venv` и `make install` завершаются ошибкой с обеими версиями, библиотека не ставится + +#### Scenario: Задание до перехода +- **WHEN** аналитик правит форму, посчитанную сервисом с копией (`statements_version` — хэш коммита), а у ядра + установлена библиотека `0.1.1` +- **THEN** пересчёт пишет `STATEMENTS_VERSION_MISMATCH` (`info`), на статус и подтверждение это не влияет + +#### Scenario: Перечень кодов в docs новее библиотеки +- **WHEN** в docs в `statement-code-orders.yaml` добавлен код, а библиотека ещё не выпущена с ним +- **THEN** `make -C docs check` печатает предупреждение `statements-orders-check` и называет файл, код возврата 0 + +## REMOVED Requirements + +### Requirement: Пакет общего кода `arm_statements` и его копии +**Reason**: копии пакета в сервисах заменены установленной библиотекой (AURORA-1677), репозиторий `arm-statements` +архивирован, код перенесён в `RSHB/pylib_ai_arm_statements`. +**Migration**: требования «Библиотека общего кода `arm_statements`» и «Поставка библиотеки потребителям». + +### Requirement: `sync-statements` — копии пакета в потребителях +**Reason**: копии `api/arm_statements/` в сервисах заменены установленной библиотекой (AURORA-1677); раскладывать и +сверять нечего, скрипт `docs/scripts/sync-statements.sh` и цели `sync-statements`, `statements-check`, +`test-sync-statements` удалены. +**Migration**: поставка и версия — требование «Поставка библиотеки потребителям»; сверка перечня кодов docs с +библиотекой — цель `statements-orders-check` в `make -C docs check`; сверка словаря имён ядра с перечнем — +тест ядра `test_name_dictionary_codes_in_code_orders`. + diff --git a/openspec/changes/document-statements-library-contract/tasks.md b/openspec/changes/document-statements-library-contract/tasks.md new file mode 100644 index 0000000..cd3ff7b --- /dev/null +++ b/openspec/changes/document-statements-library-contract/tasks.md @@ -0,0 +1,35 @@ +# Tasks + +## 1. Контракт + +- [x] 1.1 docs, `contracts/recognition-result.schema.json`: обновить два `description` (нормализация библиотекой, + источник `service.statements_version`); остальные JSON-значения не меняются, `scripts/validate_contracts.py` проходит. +- [x] 1.2 arm-core, tabular-parser, scan-gateway, `api/contracts/recognition-result.schema.json`: байтовая копия из docs; + тест идентичности в каждом сервисе проходит. + +## 2. Инструменты docs + +- [x] 2.1 Удалить `scripts/sync-statements.sh`, `scripts/test_sync_statements.sh`, `scripts/statements_check.py` и цели + `sync-statements`, `statements-check`, `test-sync-statements` в `Makefile`. +- [x] 2.2 `Makefile`: цель `statements-orders-check` (перечень кодов docs против копии в библиотеке, предупреждение) в + `make check`. +- [x] 2.3 `scripts/prepare-contour-snapshot.sh`: убрать шаг 0a; вырезать цель `install` вместе с `docker-build`; + `scripts/test_prepare_contour_snapshot.sh` — без сценариев копии, с проверкой вырезанной `install`; `make test-snapshot` + проходит; снимки tabular-parser и scan-gateway с веток PR собираются. + +## 3. Спеки + +- [x] 3.1 Дельты `statement-normalization`, `quality-checks`, `arm-core`, `recognition-contract`; + `openspec validate document-statements-library-contract --strict` проходит. + +## 4. Контекст и руководства + +- [x] 4.1 `context/03-architecture.md`, `05-domain-rules.md` (§7 «Ядро», §9.5, §11), `00-workspace-layout.md`: библиотека + вместо копий; баннеры-заметки в 03/07/09 убраны. +- [x] 4.2 `dev/checks.md` (раздел «Библиотека общего кода», таблицы целей), `dev/contour-transfer-guide.md` (шаги + снимка, библиотека в контуре). + +## 5. При архивации + +- [ ] 5.1 В `openspec/specs/statement-normalization/spec.md` поправить Purpose (упоминает копирование скриптом + `sync-statements`). diff --git a/scripts/prepare-contour-snapshot.sh b/scripts/prepare-contour-snapshot.sh index 91ff0a3..e1a27a2 100755 --- a/scripts/prepare-contour-snapshot.sh +++ b/scripts/prepare-contour-snapshot.sh @@ -3,7 +3,7 @@ # # Что делает: берёт дерево коммита (`git archive`, без истории), вырезает то, чего в контуре нет или что ему не нужно # (AI-инструментарий, планы, Dockerfile для локальной сборки), переводит `make venv` с uv на `python -m venv` + pip через -# Nexus (как `setup/setup.sh` шаблона), убирает цель docker-build, заменяет ссылки на внешний Gitea, пишет SNAPSHOT.md с +# Nexus (как `setup/setup.sh` шаблона), убирает цели docker-build, install и statements-version, заменяет ссылки на внешний Gitea, пишет SNAPSHOT.md с # провенансом и проверяет результат. Репозиторий-источник не меняется. # # Запуск из workspace: docs/scripts/prepare-contour-snapshot.sh <каталог репозитория> <каталог вывода> [ref] @@ -12,10 +12,6 @@ # Результат: <каталог вывода>/<имя репозитория>/ — готовое дерево для rsync в клон репозитория контура. # # Флаг --web-as-is остался от синхронизации с arm-web (до 21.10.2026) и игнорируется: фронт ведётся в arm-core/web. -# Для каждого сервиса, где в $REF есть api/arm_statements, проверяется свежесть копии пакета (sync-statements.sh --check -# по дереву $REF; Р-42): отставание — предупреждение и абзац в SNAPSHOT.md, прямая правка копии — стоп (код 1), нет -# arm-statements или Python — предупреждение. Нет api/arm_statements — проверка пропускается. $SYNC_STATEMENTS_ARGS — -# дополнительные аргументы sync-statements.sh (для теста: --source … --docs …). # $SNAPSHOT_SKIP_WEB_BUILD=1 — не проверять сборку web/ (для тестов без сети); по умолчанию проверка включена. # # Встроенный фронт arm-core в контуре только собирается (npm install через Nexus + rsbuild build), а зеркало npm банка не @@ -49,38 +45,7 @@ if [ -e "$DEST" ]; then exit 2 fi -# --- 0a. Копия пакета arm_statements в api/arm_statements не должна отставать от arm-statements --------------------------- -# Проверяется дерево $REF хоста (git archive api → временный каталог), не рабочее. Нет api/arm_statements в $REF — пакет в -# хост ещё не подключён, проверка молча пропускается. Коды sync-statements.sh --check: 0 — тихо; 10 — предупреждение в -# stderr и абзац в SNAPSHOT.md; 20 — прямая правка/чужая версия копии, снимок не делается; 2 (нет arm-statements или -# интерпретатора) — предупреждение, снимок делается. git -C "$REPO" fetch -q origin -STATEMENTS_LAG="" -if git -C "$REPO" rev-parse -q --verify "$REF:api/arm_statements" >/dev/null 2>&1; then - SYNC_STATEMENTS="$(cd "$(dirname "$0")" && pwd)/sync-statements.sh" - STM_TMP=$(mktemp -d) - trap 'rm -rf "$STM_TMP"' EXIT - git -C "$REPO" archive "$REF" api | tar -x -C "$STM_TMP" - rc=0 - # shellcheck disable=SC2086 - stm_out=$("$SYNC_STATEMENTS" --check --host "$NAME" --host-dir "$STM_TMP" ${SYNC_STATEMENTS_ARGS:-} 2>&1) || rc=$? - rm -rf "$STM_TMP" - case "$rc" in - 0) ;; - 10) - echo "предупреждение: копия arm_statements в $NAME отстаёт от arm-statements или требует внимания:" >&2 - echo "$stm_out" >&2 - STATEMENTS_LAG=$(printf '%s\n' "$stm_out" | grep '^statements-check' | tail -n 1) - [ -n "$STATEMENTS_LAG" ] || STATEMENTS_LAG="sync-statements.sh --check вернул 10" ;; - 20) echo "$stm_out" >&2 - echo "ошибка: копия arm_statements в $NAME расходится с arm-statements (прямые правки или чужая версия) — снимок не делается, разберитесь вручную" >&2 - exit 1 ;; - 2) echo "предупреждение: копия arm_statements в $NAME не проверена (sync-statements.sh --check: нет arm-statements рядом или подходящего Python):" >&2 - echo "$stm_out" >&2 ;; - *) echo "$stm_out" >&2 - echo "ошибка: sync-statements.sh --check завершился с кодом $rc" >&2; exit 1 ;; - esac -fi HASH=$(git -C "$REPO" rev-parse "$REF") DATE=$(git -C "$REPO" show -s --format=%cs "$REF") @@ -101,7 +66,7 @@ find "$DEST" \( \( -type d \( -name .claude -o -name .codegraph -o -path '*/docs -o \( -type f \( -name CLAUDE.md -o -path '*/openspec/config.yaml' \) \) \) -prune -exec rm -rf {} + find "$DEST" -type d -name docs -empty -delete -# --- 2. Makefile: venv через python -m venv + pip с Nexus, без uv; без docker-build -------------------------------- +# --- 2. Makefile: venv через python -m venv + pip с Nexus, без uv; без docker-build, install, statements-version python3 -I - "$DEST/Makefile" <<'PY' import re import sys @@ -112,6 +77,9 @@ lines = path.read_text(encoding="utf-8").splitlines() out: list[str] = [] i = 0 venv_patched = False +# цели только для разработки вне банка: образ для compose, установка библиотеки arm_statements из соседнего checkout +# и сверка этого checkout с версией в requirements.txt +DROPPED = ("docker-build", "install", "statements-version") while i < len(lines): line = lines[i] # рецепт venv: все строки с uv заменяются одним рецептом на pip @@ -132,8 +100,8 @@ while i < len(lines): i += 1 venv_patched = True continue - # цель docker-build и комментарий над ней удаляются - if line.startswith("docker-build:"): + # цели DROPPED и комментарий над ними удаляются + if line.split(":", 1)[0] in DROPPED and not line.startswith("\t"): comment_start = len(out) while comment_start > 0 and out[comment_start - 1].startswith("#"): comment_start -= 1 @@ -161,8 +129,9 @@ while i < len(lines): i += 1 continue if line.startswith(".PHONY:"): - line = re.sub(r"\s+docker-build\b", "", line) - if line.startswith('\t@echo "docker-build'): + for target in DROPPED: + line = re.sub(rf"\s+{target}(?=\s|$)", "", line) + if line.startswith(('\t@echo "docker-build', '\t@echo "install', '\t@echo "statements-version')): i += 1 continue line = line.replace(" + docker-build", "") @@ -254,9 +223,6 @@ if [ -n "$WEB_REMOVED" ]; then printf '\nЦель `make web-check` — `npm install` и `npm run build`. `web/package.json` из контура обратно не переносить: он урезан.\n' } >> "$DEST/SNAPSHOT.md" fi -if [ -n "$STATEMENTS_LAG" ]; then - printf '\nКопия пакета arm_statements (`api/arm_statements/`) на момент снимка требует внимания: %s\n' "$STATEMENTS_LAG" >> "$DEST/SNAPSHOT.md" -fi # --- 5. Проверки ---------------------------------------------------------------------------------------------------- fail=0 diff --git a/scripts/statements_check.py b/scripts/statements_check.py deleted file mode 100755 index 5bf3afa..0000000 --- a/scripts/statements_check.py +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env python3 -"""Помощник sync-statements.sh: проверки пакета arm_statements против хоста (arm-core, tabular-parser, scan-gateway). - -Команды: - deps - зависимости пакета (`[project].dependencies`), которых нет в requirements хоста; - lines - коды словаря имён строк, которых нет в перечне кодов копии пакета. - -Коды возврата: 0 — находок нет, 20 — есть находки, 1 — файл не читается или не той формы, 2 — неверные аргументы. -Спека: openspec statement-normalization (единая нормализация ББ/ОФР, issue #116). Только stdlib и PyYAML. -""" -from __future__ import annotations - -import re -import sys -import tomllib - -import yaml - -_NAME_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*") - - -def normalize_name(name: str) -> str: - """Имя пакета по PEP 503.""" - return re.sub(r"[-_.]+", "-", name).lower() - - -def requirement_name(line: str) -> str | None: - """Имя пакета из строки требования; пустая строка, комментарий и опции (`-r`, `--index-url`) дают None.""" - line = line.strip() - if not line or line.startswith("#") or line.startswith("-"): - return None - match = _NAME_RE.match(line) - return match.group(0) if match else None - - -def _pinned_version(line: str) -> str | None: - match = re.search(r"==\s*([^\s;,#]+)", line) - return match.group(1) if match else None - - -def missing_dependencies(pyproject_text: str, requirements_text: str) -> tuple[list[str], list[str]]: - """(зависимости пакета, которых нет среди requirements; примечания о различии версий).""" - project = tomllib.loads(pyproject_text).get("project") - deps = project.get("dependencies") if isinstance(project, dict) else None - if not deps: - return [], [] - host: dict[str, str] = {} - for line in requirements_text.splitlines(): - name = requirement_name(line) - if name: - host[normalize_name(name)] = line.split("#", 1)[0].strip() - missing: list[str] = [] - notes: list[str] = [] - for dep in deps: - name = requirement_name(dep) - if not name: - continue - host_line = host.get(normalize_name(name)) - if host_line is None: - missing.append(dep.strip()) - continue - pkg_version, host_version = _pinned_version(dep), _pinned_version(host_line) - if pkg_version and host_version and pkg_version != host_version: - notes.append(f"версии различаются: пакет {dep.strip()}, хост {host_line}") - return missing, notes - - -def codes_outside_orders(lines_doc: object, orders_doc: object) -> list[str]: - """Коды словаря имён, которых нет в объединении вариантов full и simplified перечня.""" - if not isinstance(lines_doc, dict) or not isinstance(lines_doc.get("editions"), list): - raise ValueError("словарь имён: нет списка editions") - if not isinstance(orders_doc, dict) or not isinstance(orders_doc.get("editions"), dict): - raise ValueError("перечень кодов: нет словаря editions") - problems: list[str] = [] - for edition in lines_doc["editions"]: - if not isinstance(edition, dict) or "id" not in edition or not isinstance(edition.get("forms"), dict): - raise ValueError("словарь имён: запись редакции без id или forms") - ed_id = str(edition["id"]) - ed_orders = orders_doc["editions"].get(ed_id) - for form, entries in edition["forms"].items(): - allowed: set[str] = set() - if isinstance(ed_orders, dict) and isinstance(ed_orders.get(form), dict): - for variant in ("full", "simplified"): - allowed.update(str(c) for c in ed_orders[form].get(variant) or []) - for entry in entries or []: - code = str(entry["code"]) if isinstance(entry, dict) and "code" in entry else None - if code is None: - raise ValueError(f"словарь имён: запись без code в редакции {ed_id}, форма {form}") - if code not in allowed: - problems.append(f"код {code} формы {form} редакции {ed_id} нет в перечне копии пакета") - return problems - - -def _read(path: str) -> str: - with open(path, encoding="utf-8") as fh: - return fh.read() - - -def main(argv: list[str]) -> int: - if len(argv) != 3 or argv[0] not in ("deps", "lines"): - print("использование: statements_check.py deps \n" - " statements_check.py lines ", - file=sys.stderr) - return 2 - try: - if argv[0] == "deps": - missing, notes = missing_dependencies(_read(argv[1]), _read(argv[2])) - findings = [f"зависимость пакета {dep} не найдена в {argv[2]}" for dep in missing] - else: - findings = codes_outside_orders(yaml.safe_load(_read(argv[1])), yaml.safe_load(_read(argv[2]))) - notes = [] - except (OSError, ValueError, yaml.YAMLError, tomllib.TOMLDecodeError) as exc: - print(f"ошибка: {exc}", file=sys.stderr) - return 1 - for finding in findings: - print(finding) - for note in notes: - print(f"примечание: {note}") - return 20 if findings else 0 - - -if __name__ == "__main__": - sys.exit(main(sys.argv[1:])) diff --git a/scripts/sync-statements.sh b/scripts/sync-statements.sh deleted file mode 100755 index 16344da..0000000 --- a/scripts/sync-statements.sh +++ /dev/null @@ -1,553 +0,0 @@ -#!/usr/bin/env bash -# Синхронизация копии пакета arm_statements (репозиторий RSHB/arm-statements) с хостами: arm-core, tabular-parser, -# scan-gateway. Копия лежит в <хост>/api/arm_statements/ и принадлежит источнику: правят только в arm-statements. -# -# Точка синхронизации — файл <копия>/VERSION: полный хэш коммита arm-statements, из которого сделана копия. -# Файлы копии должны совпасть с `git archive arm_statements` (без VERSION и кэшей). -# -# Запуск из workspace: docs/scripts/sync-statements.sh [--check|--in-place|--dry-run] (--host <имя> | --host-dir ) -# [--ref ] [--host-ref ] [--source ] [--docs ] -# [--core-dir ] [--wt-root ] -# --check только проверить рабочее дерево хоста (незакоммиченная правка тоже расхождение): -# 0 — копия синхронна, 10 — предупреждение (копия отстала или перечень кодов в docs отличается), -# 20 — расхождение (прямая правка копии, чужая версия, нет копии, нет зависимости, код вне перечня) -# --in-place разложить копию в рабочее дерево хоста (без git-операций); 1 — защиты (.gitignore хоста, пути для контура), -# 20 — отказ (правка копии, чужая версия, нет зависимости, код вне перечня) -# (без режима) worktree хоста от --host-ref, issue, make check хоста, коммит и PR; 20 — отказ до worktree, 2 — копии нет -# на --host-ref или worktree уже есть, 0 — копия уже на --ref -# --dry-run то же до раскладки включительно, без issue, проверок, коммита и PR (worktree остаётся); с --in-place — код 2 -# --host arm-core | tabular-parser | scan-gateway; по умолчанию — по origin хоста (arm-core, arm-tabular-parser, -# arm-scan-gateway), иначе по имени каталога -# --host-dir каталог хоста (корень репозитория) -# --ref ветка источника, по умолчанию origin/main; --host-ref — база worktree хоста, по умолчанию origin/main -# --source каталог arm-statements, по умолчанию /arm-statements; --docs — каталог docs -# STATEMENTS_PY — интерпретатор помощника (Python >= 3.11 с PyYAML); иначе docs/.venv, хост api/.venv, python3 -# --check важнее --dry-run и --in-place при любом порядке аргументов. -# Итог --check: «statements-check <хост>: <код> — …»; код возврата равен коду в строке. -# Коды выхода: 0 — готово/синхронно (--in-place: итог проверки 0 или 10, он же код); 1 — сбой раскладки, защит, проверок хоста, -# коммита или push; 2 — неверные аргументы, окружение, нет копии на --host-ref, worktree уже есть; 10 — предупреждение -# (только --check и --in-place); 20 — расхождение или отказ (правка копии, чужая версия, нет зависимости, код вне перечня). -# --in-place требует --host-dir (клон/worktree хоста на ветке не main); основной checkout и ветку main он отвергает. -set -euo pipefail - -HERE="$(cd "$(dirname "$0")" && pwd)" -# Корень workspace — где рядом лежит arm-statements. Скрипт запускают и из docs/ (корень = ../..), и из worktree -# .wt/<задача>/docs (корень = ../../../..); внешний кандидат проверяется первым — там основные checkout'ы. -WS="$(cd "$HERE/../.." && pwd)" -for cand in "$HERE/../../../.." "$HERE/../.."; do - if [ -e "$cand/arm-statements/.git" ]; then WS="$(cd "$cand" && pwd)"; break; fi -done -SRC="$WS/arm-statements" -DOCS="$(cd "$HERE/.." && pwd)" -CORE_DIR="$WS/arm-core" -WT_ROOT="$WS/.wt" -REF=origin/main -HOST_REF=origin/main -HOST_NAME="" -HOST_DIR="" -MODE="" -CHECK=0 -IN_PLACE=0 -DRY=0 - -die() { echo "sync-statements: $1" >&2; exit "${2:-1}"; } # die <сообщение> [код] - -while [ $# -gt 0 ]; do - case "$1" in - --check) CHECK=1 ;; # приоритет над --dry-run и --in-place независимо от порядка - --in-place) IN_PLACE=1 ;; - --dry-run) DRY=1 ;; - --host) [ $# -ge 2 ] || die "--host требует значение" 2; HOST_NAME=$2; shift ;; - --host-dir) [ $# -ge 2 ] || die "--host-dir требует значение" 2; HOST_DIR=$2; shift ;; - --ref) [ $# -ge 2 ] || die "--ref требует значение" 2; REF=$2; shift ;; - --host-ref) [ $# -ge 2 ] || die "--host-ref требует значение" 2; HOST_REF=$2; shift ;; - --source) [ $# -ge 2 ] || die "--source требует значение" 2; SRC=$2; shift ;; - --docs) [ $# -ge 2 ] || die "--docs требует значение" 2; DOCS=$2; shift ;; - --core-dir) [ $# -ge 2 ] || die "--core-dir требует значение" 2; CORE_DIR=$2; shift ;; - --wt-root) [ $# -ge 2 ] || die "--wt-root требует значение" 2; WT_ROOT=$2; shift ;; - -h|--help) sed -n "2,30p" "$0"; exit 0 ;; - *) die "неизвестный аргумент $1" 2 ;; - esac - shift -done -if [ "$CHECK" = 1 ]; then MODE=check -elif [ "$IN_PLACE" = 1 ] && [ "$DRY" = 1 ]; then die "--in-place несовместим с --dry-run" 2 -elif [ "$IN_PLACE" = 1 ]; then - MODE=in-place - [ -n "$HOST_DIR" ] || die "--in-place требует --host-dir ; --host один не годится: он указывает на основной checkout" 2 -elif [ "$DRY" = 1 ]; then MODE=dry -else MODE=sync; fi - -[ -n "$HOST_NAME" ] || [ -n "$HOST_DIR" ] || die "укажите --host <имя> или --host-dir " 2 -case "$HOST_NAME" in ""|arm-core|tabular-parser|scan-gateway) ;; *) die "неизвестный хост: $HOST_NAME (arm-core | tabular-parser | scan-gateway)" 2 ;; esac -[ -d "$SRC/.git" ] || [ -f "$SRC/.git" ] || [ -d "$SRC/objects" ] || die "arm-statements не найден: $SRC (--source )" 2 -SRC=$(cd "$SRC" && pwd) -[ -d "$DOCS" ] || die "docs не найден: $DOCS (--docs )" 2 -DOCS=$(cd "$DOCS" && pwd) - -if [ -z "$HOST_DIR" ]; then - case "$HOST_NAME" in - arm-core) HOST_DIR=$CORE_DIR ;; - *) HOST_DIR="$WS/$HOST_NAME" ;; - esac -fi -[ -d "$HOST_DIR" ] || die "каталог хоста не найден: $HOST_DIR" 2 -HOST_DIR=$(cd "$HOST_DIR" && pwd) - -if [ -z "$HOST_NAME" ]; then - ORIGIN=$(git -C "$HOST_DIR" remote get-url origin 2>/dev/null || true) - base=$(basename "${ORIGIN:-$HOST_DIR}"); base=${base%.git} - case "$base" in - arm-core) HOST_NAME=arm-core ;; - arm-tabular-parser|tabular-parser) HOST_NAME=tabular-parser ;; - arm-scan-gateway|scan-gateway) HOST_NAME=scan-gateway ;; - *) base=$(basename "$HOST_DIR") - case "$base" in - arm-core|tabular-parser|scan-gateway) HOST_NAME=$base ;; - *) die "не удалось определить хост по origin и имени каталога: укажите --host" 2 ;; - esac ;; - esac -fi - -# --- интерпретатор помощника --------------------------------------------------------------------------------------- -PY="" -for cand in "${STATEMENTS_PY:-}" "$DOCS/.venv/bin/python" "$HOST_DIR/api/.venv/bin/python" python3; do - [ -n "$cand" ] || continue - if "$cand" -c 'import tomllib, yaml' 2>/dev/null; then PY=$cand; break; fi -done -[ -n "$PY" ] || die "нужен Python >= 3.11 с PyYAML: выполните make -C docs venv (или задайте STATEMENTS_PY)" 2 -CHK="$HERE/statements_check.py" - -# --- источник ------------------------------------------------------------------------------------------------------ -FETCH_OK=1 -if ! git -C "$SRC" fetch -q origin 2>/dev/null; then - FETCH_OK=0 - echo "sync-statements: fetch не удался, сравниваю с локальным $REF" >&2 -fi -REF_SHA=$(git -C "$SRC" rev-parse -q --verify "$REF^{commit}") || die "в arm-statements нет $REF" 2 -REF7=${REF_SHA:0:7} - -TMPD=$(mktemp -d) -trap 'rm -rf "$TMPD"' EXIT - -# helper <аргументы…>: вывод помощника в $HELPER_OUT, код в $HELPER_RC; код 1 — выход скрипта с кодом 1 -HELPER_OUT=""; HELPER_RC=0 -helper() { - HELPER_RC=0 - HELPER_OUT=$("$PY" "$CHK" "$@" 2>&1) || HELPER_RC=$? - case "$HELPER_RC" in - 0|20) ;; - *) echo "$HELPER_OUT" >&2; exit 1 ;; - esac -} - -# --- evaluate ------------------------------------------------------------------------------------------------------ -# evaluate -# Печатает находки, выставляет EVAL_CODE (0/10/20) и EVAL_WARNS (причины предупреждения через «; »). -EVAL_CODE=0 -EVAL_WARNS="" -COPY_BAD=0 # 1 — copy негодна по условиям 1–4 (не из-за зависимостей и словаря) -evaluate() { - local copy=$1 reqs=$2 lines=$3 host=$4 - local version="" v7="" archive="$TMPD/archive" known=1 line - EVAL_CODE=0; EVAL_WARNS=""; COPY_BAD=0 - bad() { echo "$1"; EVAL_CODE=20; } - badcopy() { bad "$1"; COPY_BAD=1; } # условия 1–4: сама копия негодна (правка, чужая версия) - warn() { echo "$1"; [ "$EVAL_CODE" = 20 ] || EVAL_CODE=10; EVAL_WARNS="${EVAL_WARNS:+$EVAL_WARNS; }$2"; } - - if [ ! -d "$copy" ]; then - bad "копии нет: разложите sync-statements.sh --in-place --host-dir ($copy)" - return 0 - fi - if [ -f "$copy/VERSION" ]; then version=$(tr -d ' \t\r\n' < "$copy/VERSION"); fi - v7=${version:0:7} - if ! printf '%s' "$version" | grep -Eq '^[0-9a-f]{40}$' \ - || ! git -C "$SRC" cat-file -e "$version^{commit}" 2>/dev/null; then - known=0 - badcopy "VERSION ${version:-(нет файла)} нет в истории arm-statements$([ "$FETCH_OK" = 1 ] || echo '; fetch не удался — сделайте fetch в arm-statements и повторите')" - fi - - if [ "$known" = 1 ]; then - # 3. версия должна быть из истории ref - if ! git -C "$SRC" merge-base --is-ancestor "$version" "$REF_SHA" 2>/dev/null; then - badcopy "чужая версия: $v7 не из истории $REF" - fi - # 4. файлы копии против git archive - rm -rf "$archive"; mkdir -p "$archive" - local diffs="" have_archive=1 - if ! git -C "$SRC" archive "$version" arm_statements 2>/dev/null | tar -x -C "$archive" 2>/dev/null \ - || [ ! -d "$archive/arm_statements" ]; then - have_archive=0 - badcopy "в коммите VERSION ($v7) нет arm_statements/ — чужая версия" - fi - if [ "$have_archive" = 1 ]; then - diffs=$(diff -rq -x VERSION -x __pycache__ -x '*.pyc' -x .pytest_cache -x .mypy_cache -x .ruff_cache -x .DS_Store \ - "$archive/arm_statements" "$copy" 2>&1 || true) - fi - if [ -n "$diffs" ]; then - badcopy "прямая правка копии (файлы относительно arm_statements/):" - while IFS= read -r line; do - [ -n "$line" ] || continue - case "$line" in - "Files "*" differ") - line=${line#Files }; line=${line%% and *} - echo " изменён: ${line#"$archive/arm_statements"/}" ;; - "Only in "*) - line=${line#Only in } - local d=${line%%: *} n=${line#*: } - if [ "${d#"$copy"}" != "$d" ]; then - d=${d#"$copy"}; echo " только в копии: ${d#/}${d:+/}$n" | sed 's#: /#: #; s#//#/#g' - else - d=${d#"$archive/arm_statements"}; echo " только в источнике: ${d#/}${d:+/}$n" | sed 's#: /#: #; s#//#/#g' - fi ;; - *) echo " $line" ;; - esac - done <<<"$diffs" - fi - # 5. зависимости пакета в requirements хоста - git -C "$SRC" show "$version:pyproject.toml" > "$TMPD/pyproject.toml" 2>/dev/null || : > "$TMPD/pyproject.toml" - if [ ! -f "$reqs" ]; then - bad "нет requirements хоста (api/requirements.txt): зависимости пакета не проверены" - else - helper deps "$TMPD/pyproject.toml" "$reqs" - if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; elif [ -n "$HELPER_OUT" ]; then echo "$HELPER_OUT"; fi - fi - fi - - # 6. коды словаря имён строк (только у хоста со словарём) - if [ -n "$lines" ]; then - if [ ! -f "$copy/rules/statement_code_orders.yaml" ]; then - bad "в копии нет rules/statement_code_orders.yaml: коды словаря имён не проверены" - else - helper lines "$lines" "$copy/rules/statement_code_orders.yaml" - if [ "$HELPER_RC" = 20 ]; then bad "$HELPER_OUT"; fi - fi - fi - # 7. перечень кодов в docs - if [ ! -f "$DOCS/contracts/statement-code-orders.yaml" ]; then - warn "перечня кодов нет в docs: contracts/statement-code-orders.yaml" "перечня кодов нет в docs" - elif ! cmp -s "$DOCS/contracts/statement-code-orders.yaml" "$copy/rules/statement_code_orders.yaml"; then - warn "перечень кодов в docs отличается от копии: contracts/statement-code-orders.yaml" "перечень кодов в docs отличается от копии" - fi - # 8. отставание от ref - if [ "$known" = 1 ] && [ "$version" != "$REF_SHA" ]; then - warn "копия отстала: $v7, $REF — $REF7" "копия отстала: $v7, $REF — $REF7" - git -C "$SRC" log --format=' %h %s' "$version..$REF_SHA" 2>/dev/null || true - fi - VERSION7=$v7 - return 0 -} -VERSION7="" - -# summary: итоговая строка как у --check -summary() { - case "$EVAL_CODE" in - 0) echo "statements-check $HOST_NAME: 0 — копия синхронна с arm-statements@$REF ($VERSION7)" ;; - 10) echo "statements-check $HOST_NAME: 10 — ПРЕДУПРЕЖДЕНИЕ: $EVAL_WARNS" ;; - *) echo "statements-check $HOST_NAME: 20 — расхождение, копию не принимать" ;; - esac -} - -# --- режимы -------------------------------------------------------------------------------------------------------- -if [ "$MODE" = check ]; then - LINES="" - if [ "$HOST_NAME" = arm-core ] && [ -f "$HOST_DIR/api/rules/statement_lines.yaml" ]; then - LINES="$HOST_DIR/api/rules/statement_lines.yaml" - fi - evaluate "$HOST_DIR/api/arm_statements" "$HOST_DIR/api/requirements.txt" "$LINES" "$HOST_NAME" - case "$EVAL_CODE" in - 0) echo "statements-check $HOST_NAME: 0 — копия синхронна с arm-statements@$REF ($VERSION7)" ;; - 10) echo "statements-check $HOST_NAME: 10 — ПРЕДУПРЕЖДЕНИЕ: $EVAL_WARNS" ;; - *) echo "statements-check $HOST_NAME: 20 — расхождение, копию не принимать" ;; - esac - exit "$EVAL_CODE" -fi - -# guard_copy : защиты копии (игнор .gitignore хоста, содержимое, на котором остановится перенос в контур); -# код 1 со списком, если копию нельзя коммитить. Вызывается и после раскладки, и когда раскладывать нечего. -guard_copy() { - local root=$1 dest="$1/api/arm_statements" ign pth bad=0 - ign=$(git -C "$root" ls-files --others --ignored --exclude-standard -- api/arm_statements 2>/dev/null \ - | grep -Ev '(^|/)(__pycache__|\.pytest_cache|\.mypy_cache|\.ruff_cache)(/|$)|\.pyc$|(^|/)\.DS_Store$' || true) - if [ -n "$ign" ]; then - echo "файлы копии игнорируются .gitignore хоста, в коммит не попадут:" - printf '%s\n' "$ign" | sed 's/^/ /' - bad=1 - fi - pth=$(grep -rIn "/Users/\|/home/[a-z]\|git.aepif.ru\|uv venv\|uv pip" "$dest" || true) - if [ -n "$pth" ]; then - echo "копия сломает снимок для контура:" - printf '%s\n' "$pth" | sed 's/^/ /' - bad=1 - fi - [ "$bad" = 0 ] || return 1 - return 0 -} - -# lay_copy : заменить /api/arm_statements копией из источника, записать VERSION, защиты -lay_copy() { - local root=$1 commit=$2 dest="$1/api/arm_statements" - rm -rf "$dest"; mkdir -p "$root/api" - git -C "$SRC" archive "$commit" arm_statements | tar -x -C "$root/api" - printf '%s\n' "$commit" > "$dest/VERSION" - guard_copy "$root" -} - -if [ "$MODE" = in-place ]; then - HB=$(git -C "$HOST_DIR" symbolic-ref -q --short HEAD 2>/dev/null || true) - if [ "$HOST_DIR" = "$WS/$HOST_NAME" ]; then - die "--in-place не пишет в основной checkout хоста ($HOST_DIR): создайте worktree задачи и укажите его в --host-dir" 2 - fi - case "$HB" in main|master) die "--in-place не пишет в ветку $HB хоста: перейдите на ветку задачи (worktree)" 2 ;; esac - COPY="$HOST_DIR/api/arm_statements" - REQS="$HOST_DIR/api/requirements.txt" - LINES="" - if [ "$HOST_NAME" = arm-core ] && [ -f "$HOST_DIR/api/rules/statement_lines.yaml" ]; then - LINES="$HOST_DIR/api/rules/statement_lines.yaml" - fi - # 1. существующая копия не должна быть правленой или чужой - if [ -d "$COPY" ]; then - evaluate "$COPY" "$REQS" "" "$HOST_NAME" > "$TMPD/ev.out" - if [ "$COPY_BAD" = 1 ]; then - cat "$TMPD/ev.out" - echo "если копия «чужая» после squash-слияния ветки arm-statements: удалите api/arm_statements и разложите заново" - echo "statements-check $HOST_NAME: 20 — отказ: копия правлена или чужая, ничего не записано" - exit 20 - fi - fi - # 2. зависимости и словарь имён по коммиту --ref - refuse="" - git -C "$SRC" show "$REF_SHA:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml" - if [ ! -f "$REQS" ]; then refuse="нет requirements хоста (api/requirements.txt)" - else - helper deps "$TMPD/ref-pyproject.toml" "$REQS" - if [ "$HELPER_RC" = 20 ]; then refuse="$HELPER_OUT"; fi - fi - if [ -n "$LINES" ]; then - git -C "$SRC" show "$REF_SHA:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \ - || : > "$TMPD/ref-orders.yaml" - helper lines "$LINES" "$TMPD/ref-orders.yaml" - if [ "$HELPER_RC" = 20 ]; then refuse="${refuse:+$refuse -}$HELPER_OUT"; fi - fi - if [ -n "$refuse" ]; then - echo "$refuse" - echo "statements-check $HOST_NAME: 20 — отказ: $REF несовместим с хостом, ничего не записано" - exit 20 - fi - # 3. уже на ref? - LAID=0 - if [ -d "$COPY" ] && [ -f "$COPY/VERSION" ] && [ "$(tr -d ' \t\r\n' < "$COPY/VERSION")" = "$REF_SHA" ]; then - echo "раскладывать нечего: копия уже на $REF ($REF7)" - guard_copy "$HOST_DIR" || die "копию нельзя коммитить (см. выше)" 1 - else - # 4. раскладка - lay_copy "$HOST_DIR" "$REF_SHA" || die "раскладка выполнена, но копию нельзя коммитить (см. выше)" 1 - LAID=1 - fi - # 5. итог - evaluate "$COPY" "$REQS" "$LINES" "$HOST_NAME" - summary - if [ "$LAID" = 1 ]; then - echo "закоммитьте api/arm_statements отдельным коммитом: git -C $HOST_DIR add api/arm_statements" - fi - exit "$EVAL_CODE" -fi - -# --- обычный режим и --dry-run: worktree хоста, issue, проверки, PR ------------------------------------------------ -TARGET=$REF_SHA -T7=${TARGET:0:7} -case "$HOST_NAME" in - arm-core) GITEA_REPO=RSHB/arm-core ;; - tabular-parser) GITEA_REPO=RSHB/arm-tabular-parser ;; - scan-gateway) GITEA_REPO=RSHB/arm-scan-gateway ;; -esac - -bullets() { if [ -n "$1" ]; then printf '%s\n' "$1" | sed 's/^/- /'; else echo "- (нет коммитов)"; fi; } - -# 1. хост -if ! git -C "$HOST_DIR" fetch -q origin 2>/dev/null; then - echo "sync-statements: fetch хоста не удался, работаю с локальным $HOST_REF" >&2 -fi -git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF^{commit}" >/dev/null || die "в хосте нет $HOST_REF" 2 - -# 2. состояние хоста на --host-ref -git -C "$HOST_DIR" rev-parse -q --verify "$HOST_REF:api/arm_statements" >/dev/null 2>&1 \ - || die "копии нет на $HOST_REF: подключение хоста — вручную, --in-place в worktree задачи подключения (docs/dev/checks.md)" 2 -HT="$TMPD/host-tree"; mkdir -p "$HT" -git -C "$HOST_DIR" archive "$HOST_REF" api | tar -x -C "$HT" -HLINES="" -if [ "$HOST_NAME" = arm-core ] && [ -f "$HT/api/rules/statement_lines.yaml" ]; then HLINES="$HT/api/rules/statement_lines.yaml"; fi -evaluate "$HT/api/arm_statements" "$HT/api/requirements.txt" "$HLINES" "$HOST_NAME" -if [ "$EVAL_CODE" = 20 ]; then - echo "statements-check $HOST_NAME: 20 — отказ: копия на $HOST_REF правлена, чужая или несовместима, worktree не создан" - exit 20 -fi -HOST_WARNS=$EVAL_WARNS -OLD=$(tr -d ' \t\r\n' < "$HT/api/arm_statements/VERSION") -OLD7=${OLD:0:7} - -# 3. зависимости и словарь имён против TARGET -refuse="" -git -C "$SRC" show "$TARGET:pyproject.toml" > "$TMPD/ref-pyproject.toml" 2>/dev/null || : > "$TMPD/ref-pyproject.toml" -if [ ! -f "$HT/api/requirements.txt" ]; then refuse="нет requirements хоста (api/requirements.txt)" -else - helper deps "$TMPD/ref-pyproject.toml" "$HT/api/requirements.txt" - if [ "$HELPER_RC" = 20 ]; then refuse=$HELPER_OUT; fi -fi -if [ -n "$HLINES" ]; then - git -C "$SRC" show "$TARGET:arm_statements/rules/statement_code_orders.yaml" > "$TMPD/ref-orders.yaml" 2>/dev/null \ - || : > "$TMPD/ref-orders.yaml" - helper lines "$HLINES" "$TMPD/ref-orders.yaml" - if [ "$HELPER_RC" = 20 ]; then refuse="${refuse:+$refuse -}$HELPER_OUT"; fi -fi -if [ -n "$refuse" ]; then - echo "$refuse" - echo "statements-check $HOST_NAME: 20 — отказ: $REF несовместим с хостом, worktree не создан" - exit 20 -fi - -# 4. уже на TARGET -if [ "$OLD" = "$TARGET" ]; then - echo "копия уже на $T7, раскладывать нечего" - [ -z "$HOST_WARNS" ] || echo "предупреждение: $HOST_WARNS" - exit 0 -fi - -# 5. «сначала ядро» -CORE_WARN="" -if [ "$HOST_NAME" != arm-core ]; then - CORE_VER="" - if [ -d "$CORE_DIR" ]; then - git -C "$CORE_DIR" fetch -q origin 2>/dev/null || true - CORE_VER=$(git -C "$CORE_DIR" show origin/main:api/arm_statements/VERSION 2>/dev/null | tr -d ' \t\r\n' || true) - fi - if [ -z "$CORE_VER" ] || { [ "$CORE_VER" != "$TARGET" ] && ! git -C "$SRC" merge-base --is-ancestor "$TARGET" "$CORE_VER" 2>/dev/null; }; then - CORE_WARN="ядро (arm-core@origin/main) ещё не на $T7: по порядку выкладки сначала ядро" - echo "предупреждение: $CORE_WARN" - fi -fi - -# 6. worktree хоста -TMP_BRANCH="tmp/statements-sync-$T7" -WT="$WT_ROOT/statements-sync-$T7/$HOST_NAME" -[ ! -e "$WT" ] || die "worktree уже существует: $WT — завершите или удалите предыдущую синхронизацию" 2 -if git -C "$HOST_DIR" rev-parse -q --verify "$TMP_BRANCH" >/dev/null; then - die "ветка $TMP_BRANCH уже существует в $HOST_NAME — завершите или удалите предыдущую синхронизацию" 2 -fi -mkdir -p "$(dirname "$WT")" -if ! git -C "$HOST_DIR" worktree add -q -b "$TMP_BRANCH" "$WT" "$HOST_REF"; then - rmdir "$(dirname "$WT")" 2>/dev/null || true - die "не удалось создать worktree $WT" 1 -fi -touch "$WT/.bounded" -echo "worktree: $WT ($TMP_BRANCH от $HOST_REF)" - -# 7. issue и ветка (не в dry-run) -# tea 0.16+ принимает --login/--repo только после подкоманды (arm-ka-docs#110) -TEA_FLAGS=(--login home --repo "$GITEA_REPO") -COMMITS=$(git -C "$SRC" log --format='%h %s' --no-merges "$OLD..$TARGET" 2>/dev/null || true) -N_COMMITS=$(printf '%s\n' "$COMMITS" | grep -c . || true) -ISSUE=""; ISSUE_URL="" -BRANCH=$TMP_BRANCH -if [ "$MODE" = sync ]; then - TITLE="arm_statements: копия пакета arm-statements@$T7" - BODY="Копия пакета \`api/arm_statements\` в \`$HOST_NAME\` была на \`$OLD7\`, станет \`$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`). -Коммиты arm-statements \`$OLD7..$T7\` ($N_COMMITS): -$(bullets "$COMMITS") - -Bounded-задача, выполнена \`docs/scripts/sync-statements.sh\`: копия раскладывается из \`git archive\`, не правится." - ISSUE_URL=$(tea issues create "${TEA_FLAGS[@]}" --title "$TITLE" --description "$BODY" | grep -o 'https://[^ ]*/issues/[0-9]*' | tail -1) || true - ISSUE=${ISSUE_URL##*/} - [ -n "$ISSUE" ] || die "не удалось создать issue через tea (worktree оставлен: $WT)" - BRANCH="feat/$ISSUE-statements-sync-$T7" - git -C "$WT" branch -m "$TMP_BRANCH" "$BRANCH" - echo "issue: $ISSUE_URL; ветка: $BRANCH" -fi - -# 8. раскладка и проверка результата -lay_copy "$WT" "$TARGET" || die "раскладка выполнена, но копию нельзя коммитить (см. выше); worktree оставлен: $WT" 1 -evaluate "$WT/api/arm_statements" "$WT/api/requirements.txt" "$([ -n "$HLINES" ] && echo "$WT/api/rules/statement_lines.yaml")" "$HOST_NAME" -if [ "$EVAL_CODE" = 20 ]; then - echo "statements-check $HOST_NAME: 20 — после раскладки расхождение; worktree оставлен: $WT" >&2 - exit 1 -fi -SC_LINE=$(summary) -echo "перенесено: api/arm_statements = arm-statements@$T7" -echo "$SC_LINE" - -# 9. dry-run -if [ "$MODE" = dry ]; then - echo "dry-run: issue, проверки, коммит и PR не выполнялись. Посмотреть: git -C $WT status; убрать:" - echo " git -C $HOST_DIR worktree remove --force $WT && git -C $HOST_DIR branch -D $TMP_BRANCH" - exit 0 -fi - -# 10. проверки хоста -run_make() { # run_make <имя> <цель>: хвост вывода при ошибке, worktree остаётся - local log; log=$(mktemp) - if make -C "$WT" "$2" >"$log" 2>&1; then rm -f "$log"; return 0; fi - echo "sync-statements: проверка «$1» не прошла; worktree оставлен для разбора: $WT" >&2 - tail -40 "$log" >&2; rm -f "$log"; exit 1 -} -run_make "make venv" venv -run_make "make check" check -# make check мог переписать копию (форматтеры, кэши): повторная оценка перед коммитом -evaluate "$WT/api/arm_statements" "$WT/api/requirements.txt" "$([ -n "$HLINES" ] && echo "$WT/api/rules/statement_lines.yaml")" "$HOST_NAME" -if [ "$EVAL_CODE" = 20 ]; then - echo "sync-statements: после make check копия расходится с arm-statements@$T7; коммит не сделан, worktree оставлен: $WT" >&2 - exit 1 -fi -SC_LINE=$(summary) -echo "проверки хоста: make check зелёный" - -# 11. коммит, push, PR -git -C "$WT" add -A api/arm_statements -git -C "$WT" -c commit.gpgsign=false commit -q -F - < -EOF -COMMIT=$(git -C "$WT" rev-parse --short=7 HEAD) -git -C "$WT" push -q -u origin "$BRANCH" || die "push не удался; коммит $COMMIT в ветке $BRANCH, worktree оставлен: $WT" 1 -PR_BODY="Закрывает #$ISSUE. - -Копия \`api/arm_statements\` в \`$HOST_NAME\` обновлена скриптом \`docs/scripts/sync-statements.sh\`: \`$OLD7..$T7\` (источник — \`RSHB/arm-statements@${REF#origin/}\`). - -Коммиты arm-statements ($N_COMMITS): -$(bullets "$COMMITS") - -Проверки: \`make check\` в worktree зелёный; $SC_LINE -${CORE_WARN:+ -Предупреждение: $CORE_WARN -} -## Rulings I made -- R-1 — копия не правится: перенос проревьюенного в arm-statements кода, содержимое равно \`git archive\` коммита \`$T7\`. -- R-2 — bounded-задача без OpenSpec change. - -🤖 Generated with [Claude Code](https://claude.com/claude-code)" -PR_URL=$(tea pulls create "${TEA_FLAGS[@]}" --head "$BRANCH" --base "${HOST_REF#origin/}" \ - --title "chore(arm_statements): копия arm-statements@$T7 (#$ISSUE)" --description "$PR_BODY" \ - | grep -o 'https://[^ ]*/pulls/[0-9]*' | tail -1) || true - -# 12. итог -if [ -z "$PR_URL" ]; then - echo - echo "sync-statements: PR не создан (tea не вернул ссылку). Коммит $COMMIT, ветка $BRANCH (запушена), issue: $ISSUE_URL; worktree $WT" >&2 - echo "создать вручную: tea pulls create ${TEA_FLAGS[*]} --head $BRANCH --base ${HOST_REF#origin/}" >&2 - exit 1 -fi -echo -echo "готово: коммит $COMMIT, ветка $BRANCH" -echo "issue: $ISSUE_URL" -echo "PR: $PR_URL" -[ -z "$CORE_WARN" ] || echo "предупреждение: $CORE_WARN" -echo "исполнителю: ревью диапазона не нужно — код проревьюен в arm-statements; merge и «закрывай» по команде человека." -echo "worktree $WT и .bounded остаются до закрытия." diff --git a/scripts/test_prepare_contour_snapshot.sh b/scripts/test_prepare_contour_snapshot.sh index e166dd6..09feab2 100755 --- a/scripts/test_prepare_contour_snapshot.sh +++ b/scripts/test_prepare_contour_snapshot.sh @@ -14,7 +14,7 @@ R="$WORK/src/arm-core" put() { mkdir -p "$(dirname "$R/$1")"; printf '%s\n' "${2:-x}" > "$R/$1"; } # платформенные файлы, без которых проверка снимка падает -printf 'venv:\n\tuv venv api/.venv\n\n# Фронт: biome, типы, сборка\nweb-check:\n\tcd web && npm install\n\tcd web && npx biome check src\n\tcd web && npx tsc --noEmit\n\tcd web && npm run build\n\ncheck-full: check web-check\n' > "$R/Makefile" +printf '.PHONY: venv install statements-version web-check check-full\n\n# Сверка checkout библиотеки с requirements.txt\nstatements-version:\n\t@cat ../pylib_ai_arm_statements/VERSION\n\nvenv: statements-version\n\tuv venv api/.venv\n\n# Библиотека arm_statements из соседнего checkout\ninstall: statements-version\n\tuv pip install --python api/.venv/bin/python ../pylib_ai_arm_statements\n\n# Фронт: biome, типы, сборка\nweb-check:\n\tcd web && npm install\n\tcd web && npx biome check src\n\tcd web && npx tsc --noEmit\n\tcd web && npm run build\n\ncheck-full: check web-check\n' > "$R/Makefile" for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/requirements.txt api/.env.test \ api/contracts/schema.json; do put "$p"; done # корень: артефакты агента и локальная сборка @@ -98,6 +98,9 @@ if grep -Eq "biome|tsc" "$D/Makefile"; then echo "FAIL: в Makefile снимка grep -q "cd web && npm install" "$D/Makefile" && grep -q "cd web && npm run build" "$D/Makefile" \ || { echo "FAIL: web-check потерял npm install/build" >&2; fail=1; } grep -q "^check-full:.*web-check" "$D/Makefile" || { echo "FAIL: check-full не ссылается на web-check" >&2; fail=1; } +if grep -Eq "^install:|^statements-version:|pylib_ai" "$D/Makefile" || ! grep -q "^\.PHONY: venv web-check check-full$" "$D/Makefile"; then + echo "FAIL: цели install/statements-version (библиотека из соседнего checkout) остались в Makefile снимка" >&2; fail=1 +fi # SNAPSHOT.md перечисляет убранное for w in "@biomejs/biome" "eslint" "prettier" "typescript" "vitest" "@types/react" "@biomejs/cli-linux-x64-musl" \ "test" "lint" "format"; do @@ -152,81 +155,3 @@ PATH="$STUB:$PATH" "$SCRIPT" "$T" "$WORK/out-tp" origin/main >/dev/null [ ! -s "$NPM_STUB_LOG" ] || { echo "FAIL: для tabular-parser вызван npm" >&2; fail=1; } [ "$fail" = 0 ] || exit 1 echo "ok: проверка сборки web/ — выдаёт снимок без node_modules/dist, при падении снимка нет, у сервисов без фронта не вызывается" - -# --- копия arm_statements в сервисе: sync-statements.sh --check (Р-42) ------------------------------------------------- -# Синтетический arm-statements и хосты; флаги sync-statements.sh — через SYNC_STATEMENTS_ARGS (реальные репозитории не нужны). -PY=${STATEMENTS_PY:-python3} -if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then - echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2 - exit 1 -fi -export STATEMENTS_PY="$PY" -export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t -SS="$WORK/arm-statements" -git init -q --bare "$WORK/arm-statements.git" -git clone -q "$WORK/arm-statements.git" "$SS" 2>/dev/null -git -C "$SS" checkout -q -b main -printf '[project]\nname = "arm-statements"\ndependencies = ["PyYAML==6.0.3"]\n' > "$SS/pyproject.toml" -mkdir -p "$SS/arm_statements" -echo "# init" > "$SS/arm_statements/__init__.py" -echo "пакет" > "$SS/README.md" -mkdir -p "$SS/arm_statements/rules" -printf 'version: 1\nrepeats: {}\neditions:\n 66n:\n BALANCE: {full: ["1110", "1100"], simplified: ["1100"]}\n' \ - > "$SS/arm_statements/rules/statement_code_orders.yaml" -git -C "$SS" add -A; git -C "$SS" commit -qm S1; git -C "$SS" push -q origin HEAD:main 2>/dev/null; git -C "$SS" fetch -q origin -S1=$(git -C "$SS" rev-parse HEAD) -mkdir -p "$WORK/docs-x/contracts" -cp "$SS/arm_statements/rules/statement_code_orders.yaml" "$WORK/docs-x/contracts/statement-code-orders.yaml" -export SYNC_STATEMENTS_ARGS="--source $SS --docs $WORK/docs-x" - -# mk_service <имя>: сервис с копией пакета на S1 в api/arm_statements (после коммита — origin/main) -mk_service() { - local name=$1 d="$WORK/src/$1" - git init -q --bare "$WORK/$name-o.git" - git clone -q "$WORK/$name-o.git" "$d" 2>/dev/null - printf 'venv:\n\tuv venv api/.venv\n' > "$d/Makefile" - for p in raisa_config.yml setup/setup.sh setup/pip.conf api/run_server.py api/.env.test api/contracts/s.json; do - mkdir -p "$(dirname "$d/$p")"; echo x > "$d/$p" - done - printf 'aiohttp==3.14.3\nPyYAML==6.0.3\n' > "$d/api/requirements.txt" - git -C "$SS" archive "$S1" arm_statements | tar -x -C "$d/api" - echo "$S1" > "$d/api/arm_statements/VERSION" - git -C "$d" add -A; git -C "$d" commit -qm init; git -C "$d" push -q origin HEAD:main 2>/dev/null; git -C "$d" fetch -q origin -} - -# хост без api/arm_statements (tabular-parser выше): проверка пропущена молча -: > "$WORK/tp.err" -"$SCRIPT" "$T" "$WORK/out-tp2" origin/main >/dev/null 2>"$WORK/tp.err" -[ ! -s "$WORK/tp.err" ] && ! grep -q arm_statements "$WORK/out-tp2/tabular-parser/SNAPSHOT.md" \ - || { echo "FAIL: хост без arm_statements: проверка не пропущена молча" >&2; fail=1; } - -# копия на актуальной версии: тихо, абзаца нет -mk_service scan-gateway -G="$WORK/src/scan-gateway" -"$SCRIPT" "$G" "$WORK/out-g1" origin/main >/dev/null 2>"$WORK/g1.err" -[ ! -s "$WORK/g1.err" ] && ! grep -q arm_statements "$WORK/out-g1/scan-gateway/SNAPSHOT.md" \ - || { echo "FAIL: актуальная копия: ожидалась тишина и SNAPSHOT.md без абзаца" >&2; cat "$WORK/g1.err" >&2; fail=1; } - -# нет источника (код 2): предупреждение, снимок делается -rc=0 -SYNC_STATEMENTS_ARGS="--source $WORK/nope --docs $WORK/docs-x" "$SCRIPT" "$G" "$WORK/out-g2" origin/main >/dev/null 2>"$WORK/g2.err" || rc=$? -[ "$rc" = 0 ] && [ -e "$WORK/out-g2/scan-gateway/SNAPSHOT.md" ] && grep -q "arm_statements" "$WORK/g2.err" \ - || { echo "FAIL: нет источника: ожидалось предупреждение и снимок (rc=$rc)" >&2; fail=1; } - -# копия отстала (код 10): предупреждение в stderr и абзац в SNAPSHOT.md, снимок делается -echo "пакет, правка" > "$SS/README.md" -git -C "$SS" add -A; git -C "$SS" commit -qm S2; git -C "$SS" push -q origin HEAD:main 2>/dev/null; git -C "$SS" fetch -q origin -rc=0 -"$SCRIPT" "$G" "$WORK/out-g3" origin/main >/dev/null 2>"$WORK/g3.err" || rc=$? -[ "$rc" = 0 ] && grep -q "arm_statements" "$WORK/g3.err" && grep -q "Копия пакета arm_statements" "$WORK/out-g3/scan-gateway/SNAPSHOT.md" \ - || { echo "FAIL: копия отстала: ожидались предупреждение и абзац в SNAPSHOT.md (rc=$rc)" >&2; cat "$WORK/g3.err" >&2; fail=1; } - -# прямая правка копии (код 20): код 1, снимка нет -echo "# правка" >> "$G/api/arm_statements/__init__.py" -git -C "$G" add -A; git -C "$G" commit -qm "direct edit"; git -C "$G" push -q origin HEAD:main 2>/dev/null; git -C "$G" fetch -q origin -rc=0 -"$SCRIPT" "$G" "$WORK/out-g4" origin/main >/dev/null 2>"$WORK/g4.err" || rc=$? -[ "$rc" = 1 ] && [ ! -e "$WORK/out-g4/scan-gateway" ] && grep -q "arm_statements" "$WORK/g4.err" \ - || { echo "FAIL: прямая правка копии: ожидался код 1 без снимка (rc=$rc)" >&2; cat "$WORK/g4.err" >&2; fail=1; } -[ "$fail" = 0 ] || exit 1 -echo "ok: копия arm_statements — пропуск без пакета, тишина при актуальной, предупреждение при отставании, стоп при правке" diff --git a/scripts/test_sync_statements.sh b/scripts/test_sync_statements.sh deleted file mode 100755 index c513446..0000000 --- a/scripts/test_sync_statements.sh +++ /dev/null @@ -1,376 +0,0 @@ -#!/usr/bin/env bash -# Тест sync-statements.sh на синтетических репозиториях (bare origin + клон), без tea и сети. -# Разделы: помощник statements_check.py; дальше — сценарии самого скрипта (дописываются по задачам плана). -# Запуск: make test-sync-statements (или напрямую из корня docs; интерпретатор — STATEMENTS_PY, по умолчанию python3). -set -euo pipefail - -HERE="$(cd "$(dirname "$0")" && pwd)" -PY=${STATEMENTS_PY:-python3} -WORK=$(mktemp -d) -trap 'rm -rf "$WORK"' EXIT -export GIT_AUTHOR_NAME=t GIT_AUTHOR_EMAIL=t@t GIT_COMMITTER_NAME=t GIT_COMMITTER_EMAIL=t@t - -if ! "$PY" -c 'import tomllib, yaml' 2>/dev/null; then - echo "нужен Python >= 3.11 с PyYAML: выполните make venv (или задайте STATEMENTS_PY)" >&2 - exit 1 -fi - -fail=0 -check() { if [ "$1" = 0 ]; then echo "ok: $2"; else echo "FAIL: $2" >&2; fail=1; fi; } - -put() { mkdir -p "$(dirname "$1")"; printf '%s\n' "${2:-x}" > "$1"; } -# shellcheck disable=SC2317 -commit_push() { git -C "$1" add -A; git -C "$1" commit -qm "$2"; git -C "$1" push -q origin "HEAD:$3" 2>/dev/null; git -C "$1" fetch -q origin; } - -# --- помощник statements_check.py --------------------------------------------------------------------------------- -H="$WORK/h"; mkdir -p "$H" -CHK="$HERE/statements_check.py" -# run <описание> <аргументы…>: вывод — в $OUT -run() { - local want=$1 desc=$2 rc=0; shift 2 - OUT=$("$PY" "$CHK" "$@" 2>&1) || rc=$? - check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)" -} -has() { grep -q -- "$1" <<<"$OUT"; } - -put "$H/pp.toml" $'[project]\ndependencies = ["PyYAML==6.0.3"]' -put "$H/r1.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3' -run 0 "deps: есть" deps "$H/pp.toml" "$H/r1.txt" -check "$([ -z "$OUT" ] && echo 0 || echo 1)" "deps: есть — вывод пустой" -put "$H/r2.txt" $'aiohttp==3.14.3\npyyaml==6.0.3' -run 0 "deps: регистр" deps "$H/pp.toml" "$H/r2.txt" -put "$H/pp_u.toml" $'[project]\ndependencies = ["Py_YAML==6.0.3"]' -put "$H/r2u.txt" $'py-yaml==6.0.3' -run 0 "deps: подчёркивание и дефис равны" deps "$H/pp_u.toml" "$H/r2u.txt" -put "$H/r3.txt" $'aiohttp==3.14.3\n# PyYAML==6.0.3\n-r other.txt' -run 20 "deps: нет" deps "$H/pp.toml" "$H/r3.txt" -check "$(has PyYAML && echo 0 || echo 1)" "deps: нет — в выводе PyYAML" -put "$H/r4.txt" $'PyYAML==6.0.4' -run 0 "deps: другая версия" deps "$H/pp.toml" "$H/r4.txt" -check "$(has 'примечание:' && has 6.0.4 && echo 0 || echo 1)" "deps: другая версия — примечание с 6.0.4" -put "$H/pp_none.toml" $'[tool.ruff]\nline-length = 120' -run 0 "deps: без [project]" deps "$H/pp_none.toml" "$H/r1.txt" -put "$H/pp_jwt.toml" $'[project]\ndependencies = ["PyJWT==2.8.0"]' -put "$H/r5.txt" $'PyJWT[crypto]==2.8.0' -run 0 "deps: экстры хоста" deps "$H/pp_jwt.toml" "$H/r5.txt" - -put "$H/orders.yaml" 'version: 1 -repeats: {} -editions: - 66n: - BALANCE: {full: ["1110", "1100"], simplified: ["1100"]} - INCOME: {full: ["2110"], simplified: ["2110"]} - fsbu4: - BALANCE: {full: ["1110", "1100"], simplified: ["1100"]} - INCOME: {full: ["2110"], simplified: ["2110"]}' -put "$H/lines.yaml" 'editions: - - id: 66n - forms: - BALANCE: - - {code: "1110", name: "Основные средства"} - - {code: "1100", name: "Итого"} - - id: fsbu4 - forms: - BALANCE: - - {code: "1110", name: "Основные средства"}' -run 0 "lines: все коды есть" lines "$H/lines.yaml" "$H/orders.yaml" -put "$H/lines_x.yaml" 'editions: - - id: 66n - forms: - BALANCE: - - {code: "1110", name: "а"} - - id: fsbu4 - forms: - BALANCE: - - {code: "1110", name: "а"} - - {code: "1120", name: "б"}' -run 20 "lines: код вне перечня" lines "$H/lines_x.yaml" "$H/orders.yaml" -check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "lines: код вне перечня — в выводе 1120 и fsbu4" -put "$H/orders_s.yaml" 'version: 1 -editions: - fsbu4: - BALANCE: {full: ["1110"], simplified: ["1100"]}' -put "$H/lines_s.yaml" 'editions: - - id: fsbu4 - forms: - BALANCE: - - {code: "1100", name: "а"}' -run 0 "lines: код только в simplified" lines "$H/lines_s.yaml" "$H/orders_s.yaml" -put "$H/bad.yaml" 'editions: [' -run 1 "битый YAML" lines "$H/bad.yaml" "$H/orders.yaml" -run 2 "аргументы" - -# --- sync-statements.sh --check ----------------------------------------------------------------------------------- -SYNC="$HERE/sync-statements.sh" -export STATEMENTS_PY="$PY" -SRC="$WORK/arm-statements" -git init -q --bare "$WORK/arm-statements.git" -git clone -q "$WORK/arm-statements.git" "$SRC" 2>/dev/null -git -C "$SRC" checkout -q -b main -put "$SRC/pyproject.toml" $'[project]\nname = "arm-statements"\ndependencies = ["PyYAML==6.0.3"]' -put "$SRC/README.md" "пакет" -put "$SRC/arm_statements/__init__.py" "# init" -put "$SRC/arm_statements/steps.py" "def step(): return 1" -mkdir -p "$SRC/arm_statements/rules"; cp "$H/orders.yaml" "$SRC/arm_statements/rules/statement_code_orders.yaml" -put "$SRC/arm_statements/tests/__init__.py" "# t" -put "$SRC/arm_statements/tests/test_steps.py" "def test_step(): pass" -commit_push "$SRC" "S1" main -S1=$(git -C "$SRC" rev-parse HEAD) -mkdir -p "$WORK/docs/contracts" "$WORK/docs-empty" "$WORK/wt" -cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml" - -# make_host <каталог> <имя origin>: bare + клон с копией S1 в api/arm_statements, requirements и (для arm-core) словарём -make_host() { - local dir=$1 name=$2 - git init -q --bare "$WORK/$name.git" - git clone -q "$WORK/$name.git" "$dir" 2>/dev/null - git -C "$dir" checkout -q -b main - put "$dir/api/requirements.txt" $'aiohttp==3.14.3\nPyYAML==6.0.3' - mkdir -p "$dir/api" - git -C "$SRC" archive "$S1" arm_statements | tar -x -C "$dir/api" - printf '%s\n' "$S1" > "$dir/api/arm_statements/VERSION" -} -make_host "$WORK/arm-core" arm-core -mkdir -p "$WORK/arm-core/api/rules"; cp "$H/lines.yaml" "$WORK/arm-core/api/rules/statement_lines.yaml" -commit_push "$WORK/arm-core" "copy S1" main -make_host "$WORK/tabular-parser" arm-tabular-parser -commit_push "$WORK/tabular-parser" "copy S1" main -make_host "$WORK/scan-gateway" arm-scan-gateway -mkdir -p "$WORK/scan-gateway/api/rules" -put "$WORK/scan-gateway/api/rules/statement_lines.yaml" $'editions:\n - id: fsbu4\n forms:\n BALANCE:\n - {code: "9999", name: "х"}' -commit_push "$WORK/scan-gateway" "copy S1" main -COMMON=(--source "$WORK/arm-statements" --docs "$WORK/docs" --wt-root "$WORK/wt" --core-dir "$WORK/arm-core") -AC="$WORK/arm-core/api" -reset_hosts() { local h; for h in arm-core tabular-parser scan-gateway; do git -C "$WORK/$h" checkout -q -- . && git -C "$WORK/$h" clean -fdq; done; } -# sync <описание> <аргументы…>: вывод — в $OUT -sync() { - local want=$1 desc=$2 rc=0; shift 2 - OUT=$("$SYNC" "${COMMON[@]}" "$@" 2>&1) || rc=$? - check "$([ "$rc" = "$want" ] && echo 0 || echo 1)" "$desc (rc=$rc, ожидался $want)" -} - -sync 0 "check: синхронно" --check --host-dir "$WORK/arm-core" -check "$(has 'statements-check arm-core: 0' && echo 0 || echo 1)" "check: синхронно — итоговая строка" -sync 0 "check: имя по origin, словарь не у scan-gateway" --check --host-dir "$WORK/scan-gateway" -check "$(has 'scan-gateway' && ! has 9999 && echo 0 || echo 1)" "check: scan-gateway — в выводе имя, нет 9999" -OUT=$(cd "$WORK/tabular-parser" && "$SYNC" --check --host-dir . "${COMMON[@]}" 2>&1) && rc=0 || rc=$? -check "$([ "$rc" = 0 ] && has 'statements-check tabular-parser: 0' && echo 0 || echo 1)" "check: --host-dir . из каталога хоста (rc=$rc)" -sync 0 "check: --host вместо origin" --check --host scan-gateway --host-dir "$WORK/scan-gateway" -sync 2 "check: неизвестное имя --host" --check --host foo --host-dir "$WORK/arm-core" - -TP="$WORK/tabular-parser/api/arm_statements" -mkdir -p "$TP/__pycache__" "$TP/tests/.pytest_cache" "$TP/.mypy_cache" -: > "$TP/__pycache__/steps.cpython-311.pyc"; : > "$TP/tests/.pytest_cache/x"; : > "$TP/.mypy_cache/x"; : > "$TP/.DS_Store" -sync 0 "check: кэши в копии игнорируются" --check --host-dir "$WORK/tabular-parser" -reset_hosts - -echo "# правка" >> "$AC/arm_statements/steps.py" -sync 20 "check: изменён файл" --check --host-dir "$WORK/arm-core" -check "$(has steps.py && has 'прямая правка' && echo 0 || echo 1)" "check: изменён файл — steps.py и «прямая правка»" -reset_hosts -put "$AC/arm_statements/extra.py" "x = 1" -sync 20 "check: добавлен файл" --check --host-dir "$WORK/arm-core" -check "$(has extra.py && echo 0 || echo 1)" "check: добавлен файл — extra.py" -reset_hosts -rm "$AC/arm_statements/tests/test_steps.py" -sync 20 "check: удалён файл" --check --host-dir "$WORK/arm-core" -check "$(has test_steps.py && echo 0 || echo 1)" "check: удалён файл — test_steps.py" -reset_hosts -rm "$AC/arm_statements/VERSION" -sync 20 "check: нет VERSION" --check --host-dir "$WORK/arm-core" -reset_hosts -printf '%s\n' 0000000000000000000000000000000000000000 > "$AC/arm_statements/VERSION" -sync 20 "check: неизвестный VERSION" --check --host-dir "$WORK/arm-core" -reset_hosts -rm -rf "$AC/arm_statements" -sync 20 "check: нет копии" --check --host-dir "$WORK/arm-core" -check "$(has 'копии нет' && echo 0 || echo 1)" "check: нет копии — сообщение" -reset_hosts - -git -C "$SRC" checkout -q -b feat/x -echo "# F" >> "$SRC/arm_statements/steps.py" -commit_push "$SRC" "F" feat/x -F=$(git -C "$SRC" rev-parse HEAD) -git -C "$SRC" checkout -q main -git -C "$SRC" archive "$F" arm_statements | tar -x -C "$AC" -printf '%s\n' "$F" > "$AC/arm_statements/VERSION" -sync 20 "check: чужая версия" --check --host-dir "$WORK/arm-core" -check "$(has 'чужая версия' && echo 0 || echo 1)" "check: чужая версия — сообщение" -reset_hosts - -put "$AC/requirements.txt" "aiohttp==3.14.3" -sync 20 "check: нет зависимости" --check --host-dir "$WORK/arm-core" -check "$(has PyYAML && echo 0 || echo 1)" "check: нет зависимости — PyYAML" -reset_hosts -cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml" -sync 20 "check: код словаря вне перечня" --check --host-dir "$WORK/arm-core" -check "$(has 1120 && has fsbu4 && echo 0 || echo 1)" "check: код вне перечня — 1120 и fsbu4" -reset_hosts - -rm "$AC/arm_statements/rules/statement_code_orders.yaml" -sync 20 "check: нет statement_code_orders.yaml в копии" --check --host-dir "$WORK/arm-core" -check "$(has 'statements-check arm-core: 20' && has 'rules/statement_code_orders.yaml' && echo 0 || echo 1)" "check: нет перечня в копии — находка и итоговая строка" -reset_hosts -rm "$AC/requirements.txt" -sync 20 "check: нет requirements.txt" --check --host-dir "$WORK/arm-core" -check "$(has 'statements-check arm-core: 20' && has 'requirements' && echo 0 || echo 1)" "check: нет requirements — находка и итоговая строка" -reset_hosts - -echo "# новый код" >> "$WORK/docs/contracts/statement-code-orders.yaml" -sync 10 "check: перечень в docs новее" --check --host-dir "$WORK/arm-core" -check "$(has contracts/statement-code-orders.yaml && has 'statements-check arm-core: 10' && echo 0 || echo 1)" "check: перечень в docs — в выводе файл и итог 10" -cp "$H/orders.yaml" "$WORK/docs/contracts/statement-code-orders.yaml" -sync 10 "check: перечня нет в docs" --check --host-dir "$WORK/arm-core" --docs "$WORK/docs-empty" - -sync 2 "check: нет источника" --check --host-dir "$WORK/arm-core" --source "$WORK/nope" -sync 2 "check: нет хоста" --check --host-dir "$WORK/nope" -sync 2 "check: неизвестный аргумент" --check --bogus -sync 2 "check: нет --host/--host-dir" --check -sync 2 "check: --ref не разрешается" --check --host-dir "$WORK/arm-core" --ref origin/nope -sync 2 "--in-place вместе с --dry-run" --in-place --dry-run --host-dir "$WORK/arm-core" -sync 0 "check важнее --dry-run и --in-place" --in-place --dry-run --check --host-dir "$WORK/arm-core" - -# проверки с S2 — последними -put "$SRC/README.md" "пакет, правка" -commit_push "$SRC" "S2 правка README" main -sync 10 "check: отстала" --check --host-dir "$WORK/arm-core" -check "$(has 'копия отстала' && has 'S2 правка README' && echo 0 || echo 1)" "check: отстала — сообщение и тема S2" -echo "# правка" >> "$AC/arm_statements/steps.py" -sync 20 "check: правка + отставание = 20" --check --host-dir "$WORK/arm-core" -reset_hosts - -# коммит без arm_statements/ (T2.1): код 20 с итоговой строкой, а не 128 -NOARCH=$(git -C "$SRC" commit-tree "$(git -C "$SRC" hash-object -t tree /dev/null)" -m "без пакета") -printf '%s\n' "$NOARCH" > "$AC/arm_statements/VERSION" -sync 20 "check: в коммите VERSION нет arm_statements/" --check --host-dir "$WORK/arm-core" -check "$(has 'нет arm_statements/' && has 'statements-check arm-core: 20' && echo 0 || echo 1)" "check: нет arm_statements/ — сообщение и итог" -reset_hosts - -# --- sync-statements.sh --in-place -------------------------------------------------------------------------------- -S2=$(git -C "$SRC" rev-parse origin/main) -FR="$WORK/fresh/tabular-parser" -mkdir -p "$WORK/fresh" -git init -q --bare "$WORK/fresh/arm-tabular-parser.git" -git clone -q "$WORK/fresh/arm-tabular-parser.git" "$FR" 2>/dev/null -git -C "$FR" checkout -q -b main -put "$FR/api/requirements.txt" $'PyYAML==6.0.3' -commit_push "$FR" "fresh" main - -# хосты — на ветку задачи: --in-place отвергает main/master -for h in arm-core tabular-parser scan-gateway; do git -C "$WORK/$h" checkout -q -b work; done -git -C "$FR" checkout -q -b work -sync 2 "in-place: --host без --host-dir" --in-place --host tabular-parser -check "$(has -- '--host-dir' && echo 0 || echo 1)" "in-place: --host без --host-dir — сообщение" -git -C "$WORK/scan-gateway" checkout -q main -sync 2 "in-place: ветка main хоста" --in-place --host-dir "$WORK/scan-gateway" -check "$(has main && [ "$(tr -d '\n' < "$WORK/scan-gateway/api/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "in-place: main — копия не тронута" -git -C "$WORK/scan-gateway" checkout -q work - -sync 0 "in-place: первичная раскладка" --in-place --host-dir "$FR" --ref origin/main -check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: VERSION = полный хэш S2" -rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" -check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: копия равна git archive S2" -check "$(git -C "$FR" status --porcelain | grep -qv '^?? api/arm_statements' && echo 1 || echo 0)" "in-place: хост менялся только в api/arm_statements" -check "$(has закоммитьте && echo 0 || echo 1)" "in-place: подсказка про коммит" -sync 0 "in-place: повтор" --in-place --host-dir "$FR" --ref origin/main -check "$(has 'раскладывать нечего' && echo 0 || echo 1)" "in-place: повтор — раскладывать нечего" - -sync 0 "in-place: обновление arm-core S1 -> S2" --in-place --host-dir "$WORK/arm-core" -check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: arm-core VERSION = S2" -echo "# правка" >> "$AC/arm_statements/steps.py" -sync 20 "in-place: отказ при правке" --in-place --host-dir "$WORK/arm-core" --ref origin/main -check "$(grep -q '^# правка' "$AC/arm_statements/steps.py" && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "in-place: правка на месте, VERSION прежний" -reset_hosts - -TPR="$WORK/tabular-parser/api" -put "$TPR/requirements.txt" "aiohttp==3.14.3" -sync 20 "in-place: отказ без зависимости (обновление)" --in-place --host-dir "$WORK/tabular-parser" -check "$([ "$(tr -d '\n' < "$TPR/arm_statements/VERSION")" = "$S1" ] && has PyYAML && echo 0 || echo 1)" "in-place: копия не тронута, в выводе PyYAML" -reset_hosts -cp "$FR/api/requirements.txt" "$WORK/req.bak" -put "$FR/api/requirements.txt" "aiohttp==3.14.3" -sync 20 "in-place: отказ без зависимости (fresh)" --in-place --host-dir "$FR" --ref origin/main -check "$([ "$(tr -d '\n' < "$FR/api/arm_statements/VERSION")" = "$S2" ] && diff -r -x VERSION "$WORK/arc/arm_statements" "$FR/api/arm_statements" >/dev/null && echo 0 || echo 1)" "in-place: fresh — api/arm_statements не тронут" -cp "$WORK/req.bak" "$FR/api/requirements.txt" -cp "$H/lines_x.yaml" "$AC/rules/statement_lines.yaml" -sync 20 "in-place: код словаря вне перечня" --in-place --host-dir "$WORK/arm-core" --ref origin/main -check "$([ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && has 1120 && echo 0 || echo 1)" "in-place: словарь — копия не тронута, 1120 в выводе" -reset_hosts - -# защиты: коммиты S3/S4 на ветке test-guards, чтобы origin/main (S2) остался прежним -git -C "$SRC" checkout -q -b test-guards -put "$SRC/arm_statements/tests/golden/case.json" '{}' -commit_push "$SRC" "S3 json" test-guards -put "$FR/.gitignore" '*.json' -sync 1 "in-place: .gitignore прячет файл" --in-place --host-dir "$FR" --ref origin/test-guards -check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: в выводе case.json и .gitignore" -sync 1 "in-place: повтор после защиты .gitignore остаётся 1" --in-place --host-dir "$FR" --ref origin/test-guards -check "$(has case.json && has .gitignore && echo 0 || echo 1)" "in-place: повтор — снова case.json и .gitignore" -rm "$FR/.gitignore" -echo "# /Users/someone" >> "$SRC/arm_statements/steps.py" -commit_push "$SRC" "S4 путь" test-guards -sync 1 "in-place: содержимое для контура" --in-place --host-dir "$FR" --ref origin/test-guards -check "$(has '/Users/' && echo 0 || echo 1)" "in-place: в выводе /Users/" -sync 1 "in-place: повтор после защиты путей остаётся 1" --in-place --host-dir "$FR" --ref origin/test-guards -check "$(has '/Users/' && echo 0 || echo 1)" "in-place: повтор — снова /Users/" -git -C "$SRC" checkout -q main - -# --- sync-statements.sh --dry-run (обычный режим без issue и PR) -------------------------------------------------- -S27=${S2:0:7} -git -C "$SRC" checkout -q main -rm_wt() { # rm_wt <хост-каталог> <имя хоста>: убрать worktree и ветку синхронизации - git -C "$1" worktree remove --force "$WORK/wt/statements-sync-$S27/$2" 2>/dev/null || true - git -C "$1" branch -D "tmp/statements-sync-$S27" >/dev/null 2>&1 || true -} -WTC="$WORK/wt/statements-sync-$S27/arm-core" -sync 0 "dry-run: arm-core" --dry-run --host-dir "$WORK/arm-core" -check "$([ -d "$WTC" ] && git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && [ -f "$WTC/.bounded" ] && echo 0 || echo 1)" "dry-run: worktree, ветка и .bounded" -check "$([ "$(tr -d '\n' < "$WTC/api/arm_statements/VERSION")" = "$S2" ] && echo 0 || echo 1)" "dry-run: VERSION = S2" -rm -rf "$WORK/arc"; mkdir -p "$WORK/arc"; git -C "$SRC" archive "$S2" arm_statements | tar -x -C "$WORK/arc" -check "$(diff -r -x VERSION "$WORK/arc/arm_statements" "$WTC/api/arm_statements" >/dev/null && echo 0 || echo 1)" "dry-run: копия равна git archive S2" -check "$(has 'dry-run: issue, проверки, коммит и PR не выполнялись' && echo 0 || echo 1)" "dry-run: сообщение и команды удаления" -check "$([ -z "$(git -C "$WORK/arm-core" status --porcelain)" ] && [ "$(tr -d '\n' < "$AC/arm_statements/VERSION")" = "$S1" ] && echo 0 || echo 1)" "dry-run: хост не тронут" -sync 2 "dry-run: повтор" --dry-run --host-dir "$WORK/arm-core" -check "$(has 'уже существует' && echo 0 || echo 1)" "dry-run: повтор — уже существует" -rm_wt "$WORK/arm-core" arm-core -check "$([ ! -d "$WTC" ] && ! git -C "$WORK/arm-core" rev-parse -q --verify "tmp/statements-sync-$S27" >/dev/null && echo 0 || echo 1)" "dry-run: worktree и ветка удалены" - -sync 0 "dry-run: сначала ядро" --dry-run --host-dir "$WORK/tabular-parser" -check "$(has 'сначала ядро' && echo 0 || echo 1)" "dry-run: предупреждение «сначала ядро»" -rm_wt "$WORK/tabular-parser" tabular-parser - -# arm-core на S2 закоммичена и запушена -> раскладывать нечего -"$SYNC" "${COMMON[@]}" --in-place --host-dir "$WORK/arm-core" >/dev/null -commit_push "$WORK/arm-core" "copy S2" main -sync 0 "dry-run: уже синхронно" --dry-run --host-dir "$WORK/arm-core" -check "$(has 'раскладывать нечего' && [ ! -e "$WTC" ] && echo 0 || echo 1)" "dry-run: уже синхронно — worktree не создан" - -# прямая правка на origin/main хоста -> отказ 20 -echo "# прямая правка" >> "$WORK/tabular-parser/api/arm_statements/steps.py" -commit_push "$WORK/tabular-parser" "прямая правка" main -sync 20 "dry-run: прямая правка на origin/main" --dry-run --host-dir "$WORK/tabular-parser" -check "$(has steps.py && [ ! -e "$WORK/wt/statements-sync-$S27/tabular-parser" ] && echo 0 || echo 1)" "dry-run: отказ — steps.py, worktree не создан" - -# хост без копии -sync 2 "dry-run: хост без копии" --dry-run --host-dir "$FR" -check "$(has -- '--in-place' && echo 0 || echo 1)" "dry-run: хост без копии — подсказка --in-place" - -# tea: флаги только после подкоманды -check "$(grep -nE '(^|[^[:alnum:]_-])tea[[:space:]]+-' "$SYNC" >/dev/null && echo 1 || echo 0)" "tea: флаги после подкоманды" - -# недостижимые прежде ветки: заглушки tea и make в PATH (обычный режим, scan-gateway: копия S1, ядро на S2) -STUB="$WORK/stub"; mkdir -p "$STUB" -printf '#!/bin/sh\nexit 0\n' > "$STUB/make" -printf '#!/bin/sh\nexit 1\n' > "$STUB/tea" -chmod +x "$STUB/make" "$STUB/tea" -OUT=$(PATH="$STUB:$PATH" "$SYNC" "${COMMON[@]}" --host-dir "$WORK/scan-gateway" 2>&1) && rc=0 || rc=$? -check "$([ "$rc" = 1 ] && has 'не удалось создать issue' && echo 0 || echo 1)" "tea упал на issue: сообщение и код 1 (rc=$rc)" -rm_wt "$WORK/scan-gateway" scan-gateway -printf '#!/bin/sh\nif [ "$1" = issues ]; then echo https://git.example/RSHB/arm-scan-gateway/issues/77; exit 0; fi\nexit 1\n' > "$STUB/tea" -OUT=$(PATH="$STUB:$PATH" "$SYNC" "${COMMON[@]}" --host-dir "$WORK/scan-gateway" 2>&1) && rc=0 || rc=$? -check "$([ "$rc" = 1 ] && has 'PR не создан' && has 'tea pulls create' && has 'feat/77-statements-sync' && echo 0 || echo 1)" "tea упал на PR: итог и ручная команда, код 1 (rc=$rc)" -rm_wt "$WORK/scan-gateway" scan-gateway -git -C "$WORK/scan-gateway" branch -D "feat/77-statements-sync-$S27" >/dev/null 2>&1 || true - -[ "$fail" = 0 ] || exit 1 -echo "ok: sync-statements.sh — все сценарии пройдены"