diff --git a/Makefile b/Makefile index fe25938..c494bc7 100644 --- a/Makefile +++ b/Makefile @@ -1,18 +1,18 @@ # Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория. # Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md. VENV := .venv -# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema) +# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML) PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3) CHANGE ?= slice-0-skeleton # Текущая версия модели данных — по ней рисуются диаграммы (make diagrams) DBML ?= model/data-model-v5.18.dbml -.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web sync-web check diagrams +.PHONY: venv validate-contracts validate-openspec test-snapshot test-sync-web test-sync-statements sync-web sync-statements statements-check check diagrams -# Единственная зависимость скриптов проверки контракта — jsonschema +# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml) venv: uv venv --python 3.11 --allow-existing $(VENV) - uv pip install --python $(VENV)/bin/python jsonschema + uv pip install --python $(VENV)/bin/python jsonschema pyyaml # Примеры contracts/examples/ против трёх схем JSON Schema 2020-12 validate-contracts: @@ -24,17 +24,32 @@ validate-openspec: # Снимок для контура вырезает артефакты агента на любой глубине (dev/contour-transfer-guide.md §3); нужны git и bash test-snapshot: - scripts/test_prepare_contour_snapshot.sh + STATEMENTS_PY=$(PY) scripts/test_prepare_contour_snapshot.sh # sync-web.sh на синтетических репозиториях: точка синхронизации, перенос набора, отказ при расхождении, пайплайн переноса test-sync-web: scripts/test_sync_web.sh +# sync-statements.sh на синтетических репозиториях: помощник проверок, копирование пакета, отказы +test-sync-statements: + STATEMENTS_PY=$(PY) scripts/test_sync_statements.sh + # Синхронизация arm-core/web с arm-web@test одной командой (worktree, issue, PR); SYNC_ARGS="--check" или "--dry-run" sync-web: scripts/sync-web.sh $(SYNC_ARGS) -check: validate-contracts validate-openspec test-snapshot test-sync-web +# Копия пакета 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-web test-sync-statements # Диаграммы model/diagrams/part01…09.{svg,png} из $(DBML); нужен npx (Node.js), зависимостей Python нет diagrams: diff --git a/dev/checks.md b/dev/checks.md index caf5c65..afd55eb 100644 --- a/dev/checks.md +++ b/dev/checks.md @@ -133,7 +133,10 @@ PR убедитесь, что `docs` и `fixtures` доступны (пропу | `make venv` | `.venv` (Python 3.11, `uv`) с `jsonschema` — единственной зависимостью скриптов | | `make validate-contracts` | `python scripts/validate_contracts.py`: примеры `contracts/examples/` против трёх схем JSON Schema 2020-12 (3 схемы, 8 примеров); код 1 при любой ошибке | | `make validate-openspec` | `openspec validate slice-0-skeleton --strict` (другое изменение: `make validate-openspec CHANGE=`) | -| `make check` | обе проверки выше | +| `make check` | все проверки выше и `test-snapshot`, `test-sync-web`, `test-sync-statements` | +| `make test-sync-statements` | сценарии `sync-statements.sh` на синтетических репозиториях: коды `--check`, `--in-place`, `--dry-run`, отказ при прямой правке | +| `make sync-statements` | копия пакета `arm_statements` в хост одной командой (worktree, issue, PR); `SYNC_ARGS="--host arm-core [--check\|--dry-run]"` | +| `make statements-check` | обзор свежести копий по трём хостам рядом (`arm-core`, `tabular-parser`, `scan-gateway`): строка итога на хост; в `check` не входит | Без `make`: `python scripts/validate_contracts.py` (нужен `jsonschema`) и `openspec validate --strict`. Менять `contracts/**` нужно осознанно: байтовые копии схем лежат в `api/contracts/` каждого сервиса, после правки их diff --git a/dev/contour-transfer-guide.md b/dev/contour-transfer-guide.md index b1d633d..e1d9a64 100644 --- a/dev/contour-transfer-guide.md +++ b/dev/contour-transfer-guide.md @@ -71,6 +71,10 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour задаётся: нужен флаг `--web-as-is` первым аргументом, иначе код 3. Если `web/` правили напрямую в arm-core мимо arm-web (точки синхронизации нет) — снимок не делается, разбирать вручную. Если `arm-web` рядом не найден — предупреждение, снимок делается. +0a. Для каждого сервиса, где в `` есть `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` — в корне. @@ -88,7 +92,8 @@ docs/scripts/prepare-contour-snapshot.sh scan-gateway /tmp/contour `.env.test`, `contracts/`). Любая проблема — код возврата 1 и список. Тест скрипта — `make test-snapshot` (синтетический репозиторий с артефактами агента в корне и в `web/`); проверка -отставания `web/` и остановка переноса — в `make test-sync-web`. +отставания `web/` и остановка переноса — в `make test-sync-web`, а проверка копии `arm_statements` (пропуск, отставание, правка) +— в `make test-snapshot` (нужен Python ≥ 3.11 с PyYAML, `make venv`). ### 3.1. Синхронизация `arm-core/web` с `arm-web`: `scripts/sync-web.sh` diff --git a/scripts/prepare-contour-snapshot.sh b/scripts/prepare-contour-snapshot.sh index edc37fc..5dc47e3 100755 --- a/scripts/prepare-contour-snapshot.sh +++ b/scripts/prepare-contour-snapshot.sh @@ -17,6 +17,10 @@ # issue. Без терминала вопрос не задаётся: нужен флаг --web-as-is, иначе код 3. Расхождение (прямые правки web/ в arm-core) — # всегда стоп. arm-web ищется рядом с репозиторием (../arm-web) или в $ARM_WEB; если его нет — предупреждение, снимок делается. # $SYNC_WEB_ARGS — дополнительные аргументы sync-web.sh (для теста: --dry-run --wt-root …). +# Для каждого сервиса, где в $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 банка не @@ -85,7 +89,39 @@ if [ "$NAME" = arm-core ]; then fi 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") SUBJECT=$(git -C "$REPO" show -s --format=%s "$REF") @@ -258,6 +294,9 @@ 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 if [ -n "$WEB_LAG" ]; then printf '\nВстроенный фронт \`web/\` на момент снимка %s.\n' "$WEB_LAG" >> "$DEST/SNAPSHOT.md" fi diff --git a/scripts/statements_check.py b/scripts/statements_check.py new file mode 100755 index 0000000..5bf3afa --- /dev/null +++ b/scripts/statements_check.py @@ -0,0 +1,125 @@ +#!/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 new file mode 100755 index 0000000..16344da --- /dev/null +++ b/scripts/sync-statements.sh @@ -0,0 +1,553 @@ +#!/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 1385ed2..299f8b9 100755 --- a/scripts/test_prepare_contour_snapshot.sh +++ b/scripts/test_prepare_contour_snapshot.sh @@ -152,3 +152,81 @@ 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 new file mode 100755 index 0000000..c513446 --- /dev/null +++ b/scripts/test_sync_statements.sh @@ -0,0 +1,376 @@ +#!/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 — все сценарии пройдены"