feat(scripts): помощник statements_check.py — зависимости пакета и коды словаря имён против перечня (#120)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d4f5453980
commit
ddf6927332
12
Makefile
12
Makefile
@ -1,18 +1,18 @@
|
|||||||
# Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория.
|
# Команды проверки репозитория arm-ka-docs. Запуск из корня репозитория.
|
||||||
# Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md.
|
# Сервисных целей (lint/test/up...) здесь нет: описание общих целей сервисов — dev/checks.md.
|
||||||
VENV := .venv
|
VENV := .venv
|
||||||
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должен быть jsonschema)
|
# Если окружение создано (`make venv`) — берём его интерпретатор, иначе системный python3 (в нём должны быть jsonschema и PyYAML)
|
||||||
PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
|
PY := $(if $(wildcard $(VENV)/bin/python),$(CURDIR)/$(VENV)/bin/python,python3)
|
||||||
CHANGE ?= slice-0-skeleton
|
CHANGE ?= slice-0-skeleton
|
||||||
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
|
# Текущая версия модели данных — по ней рисуются диаграммы (make diagrams)
|
||||||
DBML ?= model/data-model-v5.18.dbml
|
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 check diagrams
|
||||||
|
|
||||||
# Единственная зависимость скриптов проверки контракта — jsonschema
|
# Зависимости скриптов проверки контракта — jsonschema и PyYAML (перечни кодов форм contracts/statement-code-orders.yaml)
|
||||||
venv:
|
venv:
|
||||||
uv venv --python 3.11 --allow-existing $(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
|
# Примеры contracts/examples/ против трёх схем JSON Schema 2020-12
|
||||||
validate-contracts:
|
validate-contracts:
|
||||||
@ -30,6 +30,10 @@ test-snapshot:
|
|||||||
test-sync-web:
|
test-sync-web:
|
||||||
scripts/test_sync_web.sh
|
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"
|
# Синхронизация arm-core/web с arm-web@test одной командой (worktree, issue, PR); SYNC_ARGS="--check" или "--dry-run"
|
||||||
sync-web:
|
sync-web:
|
||||||
scripts/sync-web.sh $(SYNC_ARGS)
|
scripts/sync-web.sh $(SYNC_ARGS)
|
||||||
|
|||||||
125
scripts/statements_check.py
Executable file
125
scripts/statements_check.py
Executable file
@ -0,0 +1,125 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Помощник sync-statements.sh: проверки пакета arm_statements против хоста (arm-core, tabular-parser, scan-gateway).
|
||||||
|
|
||||||
|
Команды:
|
||||||
|
deps <pyproject.toml> <requirements.txt>
|
||||||
|
зависимости пакета (`[project].dependencies`), которых нет в requirements хоста;
|
||||||
|
lines <statement_lines.yaml> <statement_code_orders.yaml>
|
||||||
|
коды словаря имён строк, которых нет в перечне кодов копии пакета.
|
||||||
|
|
||||||
|
Коды возврата: 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 <pyproject.toml> <requirements.txt>\n"
|
||||||
|
" statements_check.py lines <statement_lines.yaml> <statement_code_orders.yaml>",
|
||||||
|
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:]))
|
||||||
104
scripts/test_sync_statements.sh
Executable file
104
scripts/test_sync_statements.sh
Executable file
@ -0,0 +1,104 @@
|
|||||||
|
#!/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 <rc-ожидаемый> <описание> <аргументы…>: вывод — в $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 "аргументы"
|
||||||
|
|
||||||
|
[ "$fail" = 0 ] || exit 1
|
||||||
|
echo "ok: sync-statements.sh — все сценарии пройдены"
|
||||||
Loading…
x
Reference in New Issue
Block a user