openspec(#116): план 0 — тест идентичности копий контракта на окне перехода v1 → v2 (arm-core, tabular-parser, scan-gateway)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
This commit is contained in:
parent
cecb5dea2a
commit
0bb33e88c2
@ -0,0 +1,506 @@
|
||||
# План 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/` — worktree `arm-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` проверку не задевают:
|
||||
|
||||
```bash
|
||||
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 не получает: его спеки тест идентичности не описывают, правило —
|
||||
в спеке docs `recognition-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 до задачи 3 `ARM_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`.
|
||||
|
||||
```bash
|
||||
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 не нужен). Построители и наборы:
|
||||
|
||||
```python
|
||||
"""Правило теста идентичности копии контракта (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**
|
||||
|
||||
```bash
|
||||
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, шаг 0
|
||||
`proposal.md` docs change `unified-statement-normalization`), What Changes, Non-goals (копии `api/contracts/` и модели
|
||||
не меняются; переход на v2.0 — шаг 5, отдельный change), Impact (`api/tests/`). `tasks.md` — один пункт со ссылкой на
|
||||
этот план. Дельта `specs/tabular-recognition-service/spec.md`:
|
||||
|
||||
```markdown
|
||||
## 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`:
|
||||
|
||||
```python
|
||||
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` слиянием):
|
||||
|
||||
```bash
|
||||
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: Копии правила совпадают.**
|
||||
|
||||
```bash
|
||||
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 #<N>», ссылка на 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).
|
||||
Loading…
x
Reference in New Issue
Block a user