Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
45 KiB
План 0 — тест идентичности копий контракта на окне перехода v1 → v2. План реализации
Для исполнителей-агентов: обязательный навык — superpowers:subagent-driven-development или superpowers:executing-plans (в плане 5 задач: по
~/.claude/CLAUDE.md— executing-plans, Native). Шаги размечены чекбоксами (- [ ]).
Goal: тест идентичности api/contracts/ в arm-core, tabular-parser и scan-gateway сверяет каждый файл копии с
файлом docs своего MAJOR (правило S-53), чтобы make check всех трёх оставался зелёным и до, и после слияния docs PR #117
(контракт v2.0).
Architecture: правило сравнения — один модуль api/tests/contract_identity.py (чистые функции над двумя каталогами,
только stdlib) и один файл сценарных тестов на синтетических каталогах; оба файла побайтно одинаковы в трёх
репозиториях (пакета для общего кода тестов нет, arm-statements ещё не существует). Существующий
test_vendored_contracts_are_identical_to_docs в каждом репозитории сохраняет поиск docs и пропуск, а вместо сравнения
rglob-наборов вызывает identity_problems. Шаг 0 выкладки (proposal.md, «Порядок выкладки»): три PR сливаются
до docs PR #117.
Tech Stack: Python 3.11, pytest, ruff, mypy (make check), git worktree, tea, OpenSpec 1.13.
Spec: openspec/changes/unified-statement-normalization/ (репозиторий arm-ka-docs, ветка
feat/116-unified-normalization): specs/recognition-contract/spec.md — требование «Копия контракта в сервисе
идентична источнику» и его сценарии; specs/quality-checks/spec.md — make check; proposal.md — «Порядок выкладки»,
шаг 0; design.md — решение S-53. Справка по проверкам — dev/checks.md («Внешние ресурсы», «Проверки этого
репозитория», «Известные расхождения реализаций»).
Global Constraints
- Пути — абсолютные. Workspace:
/Users/andrey.epifancev/projects/awp-ca(не git). Git-команды — толькоgit -C <абсолютный путь worktree>; cwd оболочки никогда не стоит в основном checkout наmain(хук guard-main). - Зеркальный workspace плана:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/— worktreearm-core/,tabular-parser/,scan-gateway/(веткиfeat/<issue>-contract-identity-window),docs/— отсоединённый worktree arm-ka-docs только для проверок (в нём ничего не коммитится), симлинкиfixtures,reference-old-arm,CLAUDE.md,.claudeна основной workspace (docs/context/00-workspace-layout.md, «Worktree вместо веток»). - Одна фича = одна issue = одна ветка = один PR на репозиторий; ветки
feat/<issue>-contract-identity-window; Gitea —tea … --login home --repo RSHB/<repo>(RSHB/arm-core,RSHB/arm-tabular-parser,RSHB/arm-scan-gateway). - Правило сравнения — дословно из спеки
recognition-contract: схема копии<имя>.schema.jsonс$id…/v<M>— сdocs/contracts/<имя>.v<M>.schema.json, если такой файл есть, иначе с основным<имя>.schema.json; примерexamples/<имя>.json— вид по префиксу (request-— запрос,result-message-— конверт,result-— файл результата),M— MAJOR копии схемы этого вида, пара —examples/v<M>/<имя>.json, если он есть, иначеexamples/<имя>.json; прочие файлы копии (<имя>.v<N>.schema.json,examples/v<N>/*.json) — с одноимённым файлом docs, нет его — расхождение; набор: каждой схеме верхнего уровня docs (contracts/*.schema.jsonбез.v<N>) и каждому примеруcontracts/examples/*.jsonсоответствует файл копии; файлы docs*.v<N>.schema.jsonиexamples/v<N>/требуются в копии, только если копия держит этот MAJOR; файлы другого MAJOR не сверяются. Уrecognition-requestфайла.v1нет. $idсхем в docs имеют видarm-ka/<имя>/v<M>(arm-ka/recognition-result/v1,…/v2).- Сверяются только
*.json(как сейчас);contracts/statement-code-orders.yamlв копии сервисов не лежит. - Поиск docs: каталог из
ARM_KA_DOCS, иначе соседнийdocsрядом с репозиторием; нет каталогаcontracts—pytest.skipс текстомdocs недоступен: тест идентичности контракта пропущен (задайте ARM_KA_DOCS или положите docs рядом). - Файлы
api/tests/contract_identity.pyи сценарного теста (api/tests/test_contract_identity.pyв arm-core,api/tests/unit/test_contract_identity.pyв tabular-parser и scan-gateway) побайтно одинаковы в трёх репозиториях; импорт —from tests.contract_identity import …(работает во всех трёх,rootdir—api/). - Линтер одинаков в трёх репозиториях: ruff
line-length = 120,select = ["E", "F", "I", "UP", "B", "PL"];PLR2004игнорируется в тестах только у scan-gateway — сравнений с числовыми литералами в новых файлах нет. - Копии
api/contracts/**в этом плане не меняются (они побайтно равны docs main и.v1-файлам веткиfeat/116-unified-normalization— сверено 18.10.2026). make checkне требует Docker, сети и запущенной инфраструктуры (quality-checks).- Язык сообщений теста, комментариев, коммитов, issue и PR — русский. Коммиты заканчиваются строками
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>иClaude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W; PR — строкой🤖 Generated with [Claude Code](https://claude.com/claude-code). - Слияние — только по команде человека; три PR сливаются до docs PR #117. Описание PR #117 не трогать.
- Находки и решения несут маркеры (
T<n>.<k>,C-n/I-n/M-n,R-n,O-n); решения этого плана —P0-n(ниже).
Проверка на двух состояниях docs
Состояние A — docs main (контракт 1.x, файлов .v1 нет); состояние B — ветка feat/116-unified-normalization
(основные схемы v2.0, *.v1.schema.json, examples/v1/). Переключается отсоединённый worktree
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs, так что все три репозитория видят его как ../docs
без переменных окружения, а правки других агентов в .wt/normalization/docs проверку не задевают:
git -C /Users/andrey.epifancev/projects/awp-ca/docs fetch origin
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs checkout --detach origin/main # A
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/docs checkout --detach origin/feat/116-unified-normalization # B
После проверки B worktree всегда возвращается в A. Запасной путь для B без переключения — ARM_KA_DOCS=<каталог> make check (arm-core и tabular-parser — сейчас, scan-gateway — после Task 3). Признак настоящей проверки, а не пропуска:
cd api && .venv/bin/pytest -rs -k vendored_contracts <файл test_contract.py> печатает PASSED, а не SKIPPED.
Решения плана, принятые без обсуждения (переносятся в design.md задачей 4)
- P0-1. Правило живёт копией в трёх репозиториях (
api/tests/contract_identity.py+ сценарный тест), а не общим пакетом и не скриптом docs. Эталон — копия arm-core (пишется первой); расхождение копий ловит шаг сверкиshasumзадачи 5 и ревью; соседние репозитории вmake checkне читаются. Цена ошибки: правка правила — три PR; разошедшиеся копии до сверки не видны. - P0-2. «Копия держит MAJOR N» — в копии есть хотя бы один файл
*.v<N>.schema.jsonилиexamples/v<N>/*.json. Тогда все файлы docs*.v<N>.schema.jsonиexamples/v<N>/*.jsonобязательны в копии. Цена: ядро, положившее один файл.v1, обязано положить все. - P0-3. Неопределимая пара — ошибка теста с именем файла, а не пропуск: пример с неизвестным префиксом, схема без
$idвида…/v<M>, пример, вида которого нет среди схем копии. Цена: новый вид примера в docs требует правки правила. - P0-4. scan-gateway получает поиск docs через
ARM_KA_DOCSв тесте идентичности (спека требования называетARM_KA_DOCS); поискfixturesв scan-gateway не меняется. Цена: расхождение вdev/checks.mdсужается до fixtures. - P0-5. tabular-parser и scan-gateway получают OpenSpec change
contract-identity-windowс MODIFIED требования «Копия контракта идентична источнику» (их текущий текст — «байт в байт равны../docs/contracts/» и «модели валидируют примеры docs» — на окне неверен). arm-core change не получает: его спеки тест идентичности не описывают, правило — в спеке docsrecognition-contract. Цена: у arm-core при закрытии нетopenspec archive.
Review Focus
- Новый пример верхнего уровня в docs v2 без пары в
examples/v1/(например, отдельный пример OCR в PR #117): копия сервиса v1 его не имеет — по правилу набора тест сервиса краснеет на окне, вопреки сценарию «посторонний PR не краснеет» (O-1). Ожидание — сообщение называет файл; тестtest_new_top_level_docs_example_is_reported(задача 1); задача 5 повторяет проверку B на последнем коммите ветки перед слиянием #117. - docs удалил
.v1раньше, чем ядро — свои копии (нарушение порядка шага 6): тест ядра падает и называет файл.v1; тестtest_window_copy_without_docs_pair_fails(задача 1). - Схема копии без
$idили с$idбез/v<M>: падение с именем файла, а не исключение без контекста или молчаливое сравнение с основным файлом; тестtest_schema_without_major_fails(задача 1). - Префикс
result-message-начинается сresult-: конверт и файл результата разных MAJOR не должны спутаться; тестtest_example_kind_by_longest_prefix(задача 1). - docs не найден или
ARM_KA_DOCSуказывает не туда: пропуск с причиной, зелёныйmake checkбез ложной «идентичности»; в scan-gateway до задачи 3ARM_KA_DOCSигнорировался — тестыtest_docs_contracts_dir_uses_arm_ka_docsиtest_docs_contracts_dir_defaults_to_sibling_docs(задача 3).
Task 1: arm-core — правило S-53 и сценарии (эталон копий)
Модель исполнителя: sonnet (новый код).
Files:
- Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/contract_identity.py - Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract_identity.py - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract.py:100-124(раздел «идентичность вендоренных копий»;_docs_contracts_dir()черезdefault_dbml_path()остаётся) - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/CLAUDE.md:155-157(строка о копиях контракта)
Interfaces:
-
Consumes: —
-
Produces (
tests/contract_identity.py, используют задачи 2, 3 и план 3 ядра):KIND_BY_PREFIX: tuple[tuple[str, str], ...] = (("request-", "recognition-request"), ("result-message-", "recognition-result-message"), ("result-", "recognition-result"))— порядок значим, первый подходящий префикс;schema_major(schema_path: Path) -> int— MAJOR из$id(re/v(\d+)$);ValueErrorс текстом"<имя файла>: в $id нет MAJOR (…/v<M>)", если$idнет или он не того вида;example_kind(file_name: str) -> str— имя схемы вида;ValueError"<file_name>: вид примера не определён по префиксу (request-, result-message-, result-)";docs_pair(copy_rel: str, copy_dir: Path, docs_dir: Path) -> str— относительный POSIX-путь файла docs, с которым сверяется файл копииcopy_rel;ValueErrorс именем файла, если пара неопределима (в т. ч. «нет схемы<вид>.schema.jsonв копии»);held_window_majors(copy_rels: set[str]) -> set[int]— MAJOR, которые копия держит файлами.v<N>(P0-2);identity_problems(copy_dir: Path, docs_dir: Path) -> list[str]— все расхождения, каждое начинается с относительного пути файла; пустой список — копия идентична.- Тексты расхождений (на них опираются тесты):
"<copy_rel>: отличается от docs/contracts/<pair> — скопируйте заново (cp), не правьте руками";"<copy_rel>: нет пары в docs/contracts (<pair>)";"<docs_rel>: есть в docs/contracts, нет в api/contracts"; приValueErrorотdocs_pair—"<copy_rel>: <текст исключения>".
-
Step 1: Зеркальный workspace, issue, ветка, индекс. cwd —
/Users/andrey.epifancev/projects/awp-ca.
WT=/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window
mkdir -p $WT && cd $WT
ln -s ../../fixtures fixtures; ln -s ../../reference-old-arm reference-old-arm; ln -s ../../CLAUDE.md CLAUDE.md; ln -s ../../.claude .claude
git -C /Users/andrey.epifancev/projects/awp-ca/docs fetch origin
git -C /Users/andrey.epifancev/projects/awp-ca/docs worktree add --detach $WT/docs origin/main
git -C /Users/andrey.epifancev/projects/awp-ca/arm-core fetch origin
git -C /Users/andrey.epifancev/projects/awp-ca/arm-core worktree add -b tmp/contract-window $WT/arm-core origin/main
tea issues create --login home --repo RSHB/arm-core \
--title "Тест идентичности копии контракта по MAJOR (шаг 0 docs#116)" \
--description "Правило S-53 спеки recognition-contract (docs change unified-statement-normalization): копия сверяется с файлом docs своего MAJOR, если он есть, иначе с основным. Шаг 0 выкладки: слить до docs PR #117. План: openspec/changes/unified-statement-normalization/plans/2026-10-18-plan-0-contract-identity.md"
git -C $WT/arm-core branch -m feat/<N>-contract-identity-window
cd $WT/arm-core && codegraph init && make venv
Expected: git -C $WT/arm-core branch --show-current → feat/<N>-contract-identity-window; ls $WT/docs/contracts
без *.v1.schema.json. <N> — номер созданной issue.
- Step 2: Красный базовый прогон текущего теста на состоянии B. Переключить docs в B («Проверка на двух состояниях docs»), затем:
Run: cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract.py -k vendored_contracts -rs
Expected: FAIL набор файлов контракта разошёлся: … только в docs/contracts: ['examples/v1/result-error.json', …, 'recognition-result.v1.schema.json']. Вернуть docs в A.
- Step 3: Сценарные тесты в
api/tests/test_contract_identity.py(синтетические каталоги вtmp_path, реальный docs не нужен). Построители и наборы:
"""Правило теста идентичности копии контракта (S-53, спека recognition-contract). Файл одинаков в arm-core,
tabular-parser и scan-gateway; правка — сразу во всех трёх."""
import json
from pathlib import Path
import pytest
from tests.contract_identity import docs_pair, example_kind, identity_problems, schema_major
REQ, MSG, RES = "recognition-request", "recognition-result-message", "recognition-result"
def _schema(name: str, major: int) -> str:
return json.dumps({"$id": f"arm-ka/{name}/v{major}"})
def _example(major: int, mark: str) -> str:
return json.dumps({"schema_version": f"{major}.0", "mark": mark})
def _v1_copy() -> dict[str, str]: # копия сервиса сегодня = docs main до #117
return {
f"{REQ}.schema.json": _schema(REQ, 1),
f"{MSG}.schema.json": _schema(MSG, 1),
f"{RES}.schema.json": _schema(RES, 1),
"examples/request-image.json": _example(1, "request"),
"examples/result-message-done.json": _example(1, "message"),
"examples/result-statement-excel.json": _example(1, "result"),
}
def _v2_main() -> dict[str, str]: # копия сервиса после шага 5 = docs после #117 без файлов окна; запрос остаётся v1
return {
f"{REQ}.schema.json": _schema(REQ, 1),
f"{MSG}.schema.json": _schema(MSG, 2),
f"{RES}.schema.json": _schema(RES, 2),
"examples/request-image.json": _example(1, "request"),
"examples/result-message-done.json": _example(2, "message"),
"examples/result-statement-excel.json": _example(2, "result"),
}
def _docs_window() -> dict[str, str]: # docs после #117 = копия ядра после шага 3
v1 = _v1_copy()
return {
**_v2_main(),
f"{MSG}.v1.schema.json": v1[f"{MSG}.schema.json"],
f"{RES}.v1.schema.json": v1[f"{RES}.schema.json"],
"examples/v1/result-message-done.json": v1["examples/result-message-done.json"],
"examples/v1/result-statement-excel.json": v1["examples/result-statement-excel.json"],
}
def _dirs(tmp_path: Path, copy: dict[str, str], docs: dict[str, str]) -> tuple[Path, Path]:
"""Пишет файлы в tmp_path/copy и tmp_path/docs (подкаталоги создаёт), возвращает (copy_dir, docs_dir)."""
Тесты и их утверждения (c, d = _dirs(tmp_path, <копия>, <docs>)):
| Тест | Копия / docs | Утверждения |
|---|---|---|
test_step0_before_docs_merge_passes |
_v1_copy() / _v1_copy() |
identity_problems(c, d) == []; docs_pair(f"{RES}.schema.json", c, d) == f"{RES}.schema.json" |
test_stale_copy_fails_naming_file |
_v1_copy() / _v1_copy() с RES-схемой json.dumps({"$id": "arm-ka/recognition-result/v1", "new": 1}) |
одно расхождение; начинается с f"{RES}.schema.json", содержит "отличается" |
test_service_on_v1_after_v2_merge_passes |
_v1_copy() / _docs_window() |
[]; пара RES-схемы — f"{RES}.v1.schema.json", MSG-схемы — f"{MSG}.v1.schema.json" |
test_request_v1_pairs_with_main_after_v2_merge |
_v1_copy() / _docs_window() |
пара f"{REQ}.schema.json" — она же; пара "examples/request-image.json" — она же |
test_previous_major_examples_pair_with_v1_dir |
_v1_copy() / _docs_window() |
пары examples/result-statement-excel.json и examples/result-message-done.json — examples/v1/<то же имя> |
test_service_on_v2_passes_and_ignores_v1_files |
_v2_main() / _docs_window() |
[]; пара RES-схемы — f"{RES}.schema.json"; пара examples/result-statement-excel.json — она же |
test_core_window_copy_passes |
_docs_window() / _docs_window() |
[]; пары f"{RES}.v1.schema.json" и examples/v1/result-statement-excel.json — они же |
test_core_window_copy_requires_every_v1_docs_file |
_docs_window() без examples/v1/result-message-done.json / _docs_window() |
расхождение "examples/v1/result-message-done.json: есть в docs/contracts, нет в api/contracts" |
test_window_copy_without_docs_pair_fails |
_docs_window() / _v2_main() (docs удалил .v1 раньше ядра) |
расхождения начинаются с f"{RES}.v1.schema.json" и "examples/v1/result-statement-excel.json", содержат "нет пары" |
test_top_level_docs_file_missing_in_copy_fails |
_v1_copy() без examples/request-image.json / _v1_copy() |
"examples/request-image.json: есть в docs/contracts, нет в api/contracts" в списке |
test_extra_copy_file_fails |
_v1_copy() + examples/result-new.json / _v1_copy() |
расхождение начинается с "examples/result-new.json", содержит "нет пары" |
test_new_top_level_docs_example_is_reported |
_v1_copy() / _docs_window() + examples/result-statement-ocr.json (v2, без пары в v1/) |
ровно одно расхождение, начинается с "examples/result-statement-ocr.json" (O-1) |
test_unknown_example_prefix_fails |
_v1_copy() + examples/foo.json / то же |
расхождение начинается с "examples/foo.json" и содержит "вид примера"; pytest.raises(ValueError, match="вид примера") для example_kind("foo.json") |
test_schema_without_major_fails |
_v1_copy() с RES-схемой json.dumps({"$id": "arm-ka/recognition-result"}) / _v1_copy() |
есть расхождение, начинающееся с f"{RES}.schema.json" и содержащее "$id"; pytest.raises(ValueError) у schema_major |
test_example_kind_by_longest_prefix |
копия: _v1_copy() с RES-схемой v2 и examples/result-statement-excel.json = _example(2, "result"); docs: та же копия + examples/v1/result-message-done.json = _example(1, "message") |
example_kind("result-message-done.json") == MSG; пара examples/result-message-done.json — examples/v1/result-message-done.json; пара examples/result-statement-excel.json — она же |
- Step 4: Убедиться, что сценарии красные.
Run: cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract_identity.py -q
Expected: ошибка сбора ModuleNotFoundError: No module named 'tests.contract_identity'.
-
Step 5: Реализовать
api/tests/contract_identity.py(интерфейс — блок Produces). Шапка-докстринг: правило S-53, ссылка на спекуrecognition-contract, «файл побайтно одинаков в arm-core, tabular-parser и scan-gateway (api/tests/contract_identity.py); правка — во всех трёх, сверка —shasum -a 256». Только stdlib (json,re,pathlib). Алгоритм:docs_pair: путь вида<имя>.v<N>.schema.jsonилиexamples/v<N>/<файл>.json→ он же;<имя>.schema.jsonв корне →M = schema_major(copy_dir / copy_rel), кандидат<имя>.v<M>.schema.json, если(docs_dir / кандидат).is_file(), иначеcopy_rel;examples/<файл>.json(один уровень) → видexample_kind(<файл>), схема видаcopy_dir / f"{вид}.schema.json"(нет —ValueError),M = schema_major(…), кандидатexamples/v<M>/<файл>, если он есть в docs, иначеcopy_rel; любой другой путь → он же.identity_problems: наборыrglob("*.json")обоих каталогов (относительные POSIX-пути); для каждого файла копии по порядку — пара (ValueError→"<copy_rel>: <текст>"в список, файл дальше не сверяется), пары нет в docs → «нет пары», байты различаются → «отличается»; затем обязательный набор docs: схемы корня без.v<N>,examples/*.jsonодного уровня, для каждогоNизheld_window_majors(<набор копии>)—*.v<N>.schema.jsonиexamples/v<N>/*.json; каждого нет в копии — «есть в docs/contracts, нет в api/contracts». Порядок списка — по пути (детерминированный вывод).
-
Step 6: Сценарии зелёные.
Run: cd $WT/arm-core/api && .venv/bin/pytest tests/test_contract_identity.py -q
Expected: 15 passed.
-
Step 7: Перевести реальный тест на правило. В
tests/test_contract.pyтелоtest_vendored_contracts_are_identical_to_docsпосле проверкиdocs_dir.is_dir()(пропуск и его текст — без изменений) —problems = identity_problems(CONTRACTS_DIR, docs_dir);assert problems == [], "\n".join(problems). Докстринг модуля: «вендоренные схемыapi/contracts/идентичны../docs/contracts/по правилу MAJOR (S-53)».CLAUDE.md:155-157: дописать, что копия сверяется с файлом docs своего MAJOR (*.v<M>.schema.json,examples/v<M>/, если есть), правило —tests/contract_identity.py, одинаковый файл в трёх репозиториях. -
Step 8:
make checkна A и на B.
Run (A): cd $WT/arm-core && make check, затем cd api && .venv/bin/pytest tests/test_contract.py -k vendored_contracts -rs
Expected: make check — код 0; тест — PASSED.
Run (B): переключить docs в B, те же две команды, вернуть docs в A.
Expected: код 0; PASSED (копия v1 сверилась с .v1 и examples/v1/).
- Step 9: Commit
git -C $WT/arm-core add api/tests/contract_identity.py api/tests/test_contract_identity.py api/tests/test_contract.py CLAUDE.md
git -C $WT/arm-core commit -m "test(#<N>): тест идентичности копии контракта по MAJOR (S-53, шаг 0 docs#116)" \
-m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W"
Task 2: tabular-parser — копия правила, реальный тест, OpenSpec change
Модель исполнителя: haiku (код готов в задаче 1, текст спеки — в плане).
Files:
- Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/contract_identity.py(побайтная копия файла arm-core из задачи 1) - Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/unit/test_contract_identity.py(побайтная копияarm-core/api/tests/test_contract_identity.py) - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/api/tests/unit/test_contract.py:104-128 - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/CLAUDE.md:45-46,92 - Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/tabular-parser/openspec/changes/contract-identity-window/{proposal.md,tasks.md,specs/tabular-recognition-service/spec.md}
Interfaces:
-
Consumes:
identity_problems(copy_dir: Path, docs_dir: Path) -> list[str]и остальное из задачи 1 (файл копируется, не переписывается). -
Produces: —
-
Step 1: Worktree, issue, ветка, индекс — как задача 1, шаг 1, для
tabular-parser(--repo RSHB/arm-tabular-parser, тот же заголовок и описание; симлинки и docs уже есть). -
Step 2: Красный базовый прогон на B:
cd $WT/tabular-parser/api && .venv/bin/pytest tests/unit/test_contract.py -k vendored_contracts -rs→ FAIL «набор файлов контракта разошёлся … только в docs/contracts: ['examples/v1/…']». Вернуть A. -
Step 3: Скопировать оба файла
cpиз$WT/arm-core/api/tests/(сценарный тест — вtests/unit/); проверитьshasum -a 256— суммы равны суммам arm-core. -
Step 4: Сценарии зелёные:
.venv/bin/pytest tests/unit/test_contract_identity.py -q→15 passed. -
Step 5: Реальный тест — как задача 1, шаг 7 (
_docs_contracts_dir()сARM_KA_DOCSостаётся).CLAUDE.md: в строках о копиях (45-46,92) — «идентичность по правилу MAJOR (S-53),tests/contract_identity.py». -
Step 6: OpenSpec change
contract-identity-window.proposal.md— Why (окно перехода v1 → v2, шаг 0proposal.mddocs changeunified-statement-normalization), What Changes, Non-goals (копииapi/contracts/и модели не меняются; переход на v2.0 — шаг 5, отдельный change), Impact (api/tests/).tasks.md— один пункт со ссылкой на этот план. Дельтаspecs/tabular-recognition-service/spec.md:
## MODIFIED Requirements
### Requirement: Копия контракта идентична источнику
Каждый файл `api/contracts/**/*.json` SHALL быть байт в байт равен своей паре в `../docs/contracts/` (или в `contracts`
каталога из `ARM_KA_DOCS`) по правилу спеки docs `recognition-contract`, требование «Копия контракта в сервисе
идентична источнику»: схема с `$id` `…/v<M>` — с `<имя>.v<M>.schema.json`, если такой файл есть в docs, иначе с
основным; пример — с `examples/v<M>/<имя>.json`, если он есть, иначе с `examples/<имя>.json`; файлы docs другого
`MAJOR` не сверяются. pydantic-модели SHALL валидировать все примеры копии `api/contracts/examples/*.json` и порождать
JSON, проходящий схему копии. Проверка входит в `make check`.
#### Scenario: `make check`
- **WHEN** выполняются тесты контракта при доступном `../docs` (или `ARM_KA_DOCS`)
- **THEN** идентичность подтверждена, примеры валидны; при недоступном docs тест идентичности пропущен с предупреждением
#### Scenario: Окно перехода на контракт 2.0
- **WHEN** в docs основной `recognition-result.schema.json` уже v2.0, рядом `recognition-result.v1.schema.json` и
`examples/v1/`, а копия сервиса — v1.2
- **THEN** копия сверяется с файлами `.v1` и `examples/v1/` (запрос — с основным файлом), тест проходит
#### Scenario: Копия устарела
- **WHEN** в docs изменён файл, с которым сверяется копия, а копия — нет
- **THEN** тест идентичности падает, сообщение называет файл
Run: cd $WT/tabular-parser && openspec validate contract-identity-window --strict
Expected: Change 'contract-identity-window' is valid.
- Step 7:
make checkна A и на B — как задача 1, шаг 8 (файлtests/unit/test_contract.py). Expected: код 0 иPASSEDв обоих состояниях. - Step 8: Commit — как задача 1, шаг 9 (файлы задачи; сообщение
test(#<N>): …).
Task 3: scan-gateway — ARM_KA_DOCS, копия правила, реальный тест, OpenSpec change
Модель исполнителя: haiku (новый код — три строки поиска docs, его тесты даны).
Files:
- Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/contract_identity.py(побайтная копия) - Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/unit/test_contract_identity.py(побайтная копия) - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/api/tests/unit/test_contract.py:113-134 - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/CLAUDE.md:87-88 - Create:
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/scan-gateway/openspec/changes/contract-identity-window/{proposal.md,tasks.md,specs/image-recognition-gateway/spec.md}
Interfaces:
-
Consumes: файлы задачи 1.
-
Produces:
_docs_contracts_dir() -> Pathвtests/unit/test_contract.py—Path(os.environ["ARM_KA_DOCS"]) / "contracts", если переменная задана и непуста, иначеAPI_DIR.parent.parent / "docs" / "contracts"(как в tabular-parser). -
Step 1: Worktree, issue, ветка, индекс — как задача 1, шаг 1 (
--repo RSHB/arm-scan-gateway). -
Step 2: Красный базовый прогон на B:
cd $WT/scan-gateway/api && .venv/bin/pytest tests/unit/test_contract.py -k vendored_contracts -rs→ FAIL «набор файлов контракта разошёлся …». Вернуть A. -
Step 3: Тесты поиска docs в
tests/unit/test_contract.py:
def test_docs_contracts_dir_uses_arm_ka_docs(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
monkeypatch.setenv("ARM_KA_DOCS", str(tmp_path))
assert _docs_contracts_dir() == tmp_path / "contracts"
def test_docs_contracts_dir_defaults_to_sibling_docs(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("ARM_KA_DOCS", raising=False)
assert _docs_contracts_dir() == API_DIR.parent.parent / "docs" / "contracts"
Run: .venv/bin/pytest tests/unit/test_contract.py -k docs_contracts_dir -q
Expected: test_docs_contracts_dir_uses_arm_ka_docs FAIL (возвращается соседний docs).
- Step 4: Поиск docs через
ARM_KA_DOCS(import os; интерфейс — Produces); текст пропуска — общий из Global Constraints (с «задайте ARM_KA_DOCS»). Run тот же →2 passed. - Step 5: Скопировать оба файла правила, сверить
shasum, сценарии зелёные — как задача 2, шаги 3–4. - Step 6: Реальный тест — как задача 1, шаг 7.
CLAUDE.md:87-88: «идентичность по правилу MAJOR (S-53,tests/contract_identity.py); docs —../docsилиARM_KA_DOCS». - Step 7: OpenSpec change
contract-identity-window— как задача 2, шаг 6, дельтаspecs/image-recognition-gateway/spec.mdс тем же текстом требования и сценариями; в сценарииmake check— «при доступном../docs(илиARM_KA_DOCS)» и «при недоступном docs тест идентичности пропущен с предупреждением». Run:cd $WT/scan-gateway && openspec validate contract-identity-window --strict→ valid. - Step 8:
make checkна A и на B — как задача 1, шаг 8; дополнительно B через переменную без переключения:cd $WT/scan-gateway && ARM_KA_DOCS=/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs make check→ код 0, тест идентичностиPASSED. - Step 9: Commit — как задача 1, шаг 9.
Task 4: docs — dev/checks.md и решения P0-n в design.md
Модель исполнителя: haiku. Репозиторий — worktree /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs,
ветка feat/116-unified-normalization (входит в PR #117; описание PR не трогать).
Files:
- Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/dev/checks.md— «Известные расхождения реализаций», пунктARM_KA_DOCS/ARM_KA_FIXTURES; абзац о правиле теста идентичности в «Проверки этого репозитория» - Modify:
/Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs/openspec/changes/unified-statement-normalization/design.md— конец раздела## Decisions made without discussion
Interfaces:
-
Consumes: решения P0-1…P0-5 (раздел «Решения плана»), результат задачи 3 (scan-gateway читает
ARM_KA_DOCSв тесте идентичности). -
Produces: —
-
Step 1:
dev/checks.md. В пунктеARM_KA_DOCS/ARM_KA_FIXTURES: scan-gateway учитываетARM_KA_DOCSв тесте идентичности контракта (с шага 0 #116),ARM_KA_FIXTURES— нет; фраза «docs/fixtures нужно класть рядом» — только про fixtures. В абзаце о правиле: «правило реализовано файломapi/tests/contract_identity.py, побайтно одинаковым в трёх репозиториях; правка — сразу во всех трёх». -
Step 2:
design.md. Подраздел### Решения при планировании (18.10.2026)в конце## Decisions made without discussion; P0-1…P0-5 — следующими свободными номерами списка (на 18.10.2026 последний — 97, S-74; перед записью перечитать — параллельные планы дописывают тот же раздел), маркерP0-nв тексте сохраняется, формат как у S-n: дата, решение, обоснование, цена ошибки. -
Step 3: Проверка:
cd /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs && make check→ код 0. -
Step 4: Commit и push (
git -C … pull --rebaseперед push; при запрете хука —git pullслиянием):
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs add dev/checks.md openspec/changes/unified-statement-normalization/design.md
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs commit -m "docs(#116): тест идентичности копии контракта — ARM_KA_DOCS в scan-gateway, решения плана 0 (P0-1…P0-5)" -m "Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W"
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs pull --rebase
git -C /Users/andrey.epifancev/projects/awp-ca/.wt/normalization/docs push
Task 5: сверка трёх копий и итоговая проверка на A и B
Модель исполнителя: haiku. Кода не пишет; результат — таблица для описаний трёх PR.
Files: — (только чтение и прогоны)
Interfaces:
-
Consumes: ветки задач 1–3.
-
Produces: таблица «репозиторий × состояние docs → код
make check, число тестов, пропуски» для PR. -
Step 1: Копии правила совпадают.
shasum -a 256 /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/{arm-core,tabular-parser,scan-gateway}/api/tests/contract_identity.py
shasum -a 256 /Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/arm-core/api/tests/test_contract_identity.py \
/Users/andrey.epifancev/projects/awp-ca/.wt/contract-window/{tabular-parser,scan-gateway}/api/tests/unit/test_contract_identity.py
Expected: в каждой из двух групп три одинаковые суммы.
- Step 2: A — docs main.
make checkв трёх worktree; тест идентичности в каждом —PASSED, неSKIPPED. Expected: три кода 0. - Step 3: B — последний коммит
origin/feat/116-unified-normalization(послеfetch). То же. Expected: три кода 0. Красный тест с расхождением «есть в docs/contracts, нет в api/contracts» по новому примеру верхнего уровня — это O-1: остановиться и поднять владельцу, правило не ослаблять. Вернуть docs в A. - Step 4: Записать таблицу в отчёт исполнителя; дальше — superpowers:finishing-a-development-branch: три PR
(
tea pulls create --login home --repo RSHB/<repo>), в каждом — «Closes #», ссылка на docs#116 и этот план, «слить до docs PR #117 (шаг 0 выкладки)», таблица шага 2–3, список рулингов. Перед слиянием #117 шаг 3 повторяется на его последнем коммите.
Самопроверка плана
- Покрытие спеки: правило схем, примеров, прочих файлов и набора — задача 1 (шаг 5) и сценарии шага 3; сценарии
требования «Копия устарела», «Сервис ещё пишет 1.x», «Сервис перешёл на 2.0», «Запрос v1.0 после слияния v2.0»,
«Примеры прежнего MAJOR», «Шаг 0 до слияния docs», «Ядро на окне перехода» — тесты таблицы задачи 1;
../docsилиARM_KA_DOCS, пропуск с предупреждением — задачи 1–3 (scan-gateway — задача 3); «всех трёх потребителей до слияния v2.0» — задачи 1–3, порядок — задача 5, шаг 4; удаление MAJOR 1 «сначала ядро» —test_window_copy_without_docs_pair_fails;quality-checks—make checkзадач 1–3, 5;dev/checks.md— задача 4. - Интерфейсы: имена
identity_problems,docs_pair,example_kind,schema_major,held_window_majors,KIND_BY_PREFIXодинаковы в задачах 1–3 и в Review Focus; сценарный тест импортирует толькоdocs_pair,example_kind,identity_problems,schema_major. - Счёт тестов сценариев — 15 (таблица задачи 1), он же в ожиданиях шагов 6 (задача 1) и 4 (задача 2).