Compare commits

..

26 Commits

Author SHA1 Message Date
Andrey Epifancev
5f2be96e11 fix(dev): healthcheck PostgreSQL по TCP, порты на 127.0.0.1, пароли MinIO из .env в README
- pg_isready с -h 127.0.0.1: временный сервер initdb слушает только сокет (M-3)
- README: mc и psql берут учётные данные из переменных окружения контейнера, а не зашитые значения (M-4)
- порты postgres/nats/minio публикуются на ${BIND_ADDR:-127.0.0.1}; BIND_ADDR добавлен в .env.example и README,
  примечание про безопасность обновлено (M-6, R-9)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:59:56 +04:00
Andrey Epifancev
5bebdfe9fc docs(architecture): актуализировать описание контракта распознавания и версионирование
- убрана устаревшая пометка «черновик» у recognition-result.schema.json (M-1)
- в строку ack добавлен schema_version, metrics и raw_s3_key помечены как опциональные,
  правило версионирования дополнено принятием любого MINOR и порядком раскатки (M-2)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:59:03 +04:00
Andrey Epifancev
991a152454 fix(contracts): schema_version принимает любой MINOR, границы confidence, описание Nats-Msg-Id
- schema_version: const "1.0" заменён на pattern ^1\.\d+$ во всех трёх схемах (I-1, R-8);
  в $comment добавлено правило раскатки нового MINOR сначала на потребителей
- границы 0–100 для organization_name_confidence, inn_confidence, line_code_confidence,
  line_name_confidence (I-2)
- description recognition-result-message: две формы Nats-Msg-Id на recognition.result (M-5)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:58:48 +04:00
Andrey Epifancev
92d642d64a docs: design.md — решения 19–20 (schema_version по MAJOR, bind-адрес портов compose)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:57:15 +04:00
Andrey Epifancev
0ee1d41da7 chore: отметить задачу 7 (2.2) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:47:31 +04:00
Andrey Epifancev
45d45e17f7 Исправить dev/.env.example и README по ревью: SMB-заглушки, NATS_ENABLED, статус целей check
Спека и R-7 (design №18): dev/.env.example остаётся единственным источником для запуска из venv,
поэтому в него добавлены OPENBAO__SMB__* и NATS_ENABLED=true; README отделяет текущие цели
Makefile arm-core от планируемых по spec quality-checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:42:17 +04:00
Andrey Epifancev
de116ed0c1 docs: design.md — решение 18 (SMB-заглушки и NATS_ENABLED в dev/.env.example)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:41:31 +04:00
Andrey Epifancev
6b97d87d8e Добавить dev/README.md: запуск инфраструктуры, переменные, сброс, запуск из venv
Черновик: профиль services, запуск из venv и интеграционные тесты помечены
как планируемые и дописываются в задачах 5.x.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:36:17 +04:00
Andrey Epifancev
34c43be881 chore: отметить задачу 6 (2.1) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:34:49 +04:00
Andrey Epifancev
64c103da5f docs: design.md — решения 16–17 (образы MinIO в dev-compose)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:30:59 +04:00
Andrey Epifancev
944296250e Игнорировать dev/.env с локальными настройками compose
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:29:57 +04:00
Andrey Epifancev
4de51361a3 Добавить docker-compose инфраструктуры разработки (dev/)
PostgreSQL 16, NATS 2.10 без JetStream, MinIO и minio-init с бакетом arm-ka;
имя проекта arm-ka, порты и учётные данные из .env, версии образов зафиксированы.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:29:57 +04:00
Andrey Epifancev
a22a9c3100 chore: отметить задачу 5 (1.5) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:18:09 +04:00
Andrey Epifancev
3074632fb1 Архитектура: таблица контракта NATS со ссылками на схемы, ack и правила Nats-Msg-Id
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:07:32 +04:00
Andrey Epifancev
7f0de551c1 chore: отметить задачу 4 (1.4) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:06:41 +04:00
Andrey Epifancev
af0f25fbd8 Контракт: скрипт validate_contracts.py валидирует примеры схемами
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:04:49 +04:00
Andrey Epifancev
38c50c2101 Контракт: примеры сообщений и результатов распознавания v1.0
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:04:49 +04:00
Andrey Epifancev
faae87c9c7 chore: отметить задачу 3 (1.3) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:03:16 +04:00
Andrey Epifancev
a57cf2db49 Контракт: схема сообщения результата recognition-result-message v1.0
Конверт на subject recognition.result: status processing | done | needs_check | error,
условные обязательные поля через if/then (done/needs_check → result_s3 и
result_schema_version; error → error). group_schema в конверт не входит.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:02:18 +04:00
Andrey Epifancev
4be67392ab chore: отметить задачу 2 (1.2) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:01:18 +04:00
Andrey Epifancev
8efaba47bf Контракт: схема запроса распознавания recognition-request v1.0
JSON Schema 2020-12 по spec recognition-contract: schema_version, job_id,
file_id (uuid), pipeline, file_type, original_name, s3 {bucket, key},
reply_deadline (date-time), attempt (>= 1).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 18:00:35 +04:00
Andrey Epifancev
1166abe42b chore: отметить задачу 1 (1.1) выполненной
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 17:59:57 +04:00
Andrey Epifancev
65c3809efe Контракт v1.0: схема результата распознавания без пометки «черновик»
- title без «ЧЕРНОВИК v1, уточнить в Срезе 2»
- $comment о правилах версионирования (MAJOR.MINOR)
- NOT_IMPLEMENTED в перечне error.code
- пустой documents допустим только при status = error (if/then, minItems: 1 для done и needs_check)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 17:58:47 +04:00
Andrey Epifancev
8e1a1ce4f5 docs: design.md — решения 13–15 (SDD, фаза 1 среза 0)
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 17:55:37 +04:00
Andrey Epifancev
22f55e5f7d chore: план исполнения SDD из tasks.md change'а, служебные каталоги в .gitignore
docs/superpowers/plans/2026-10-03-slice-0-skeleton*.md сгенерирован 1:1 из openspec/changes/slice-0-skeleton/tasks.md
(заголовки «Task N» для скриптов subagent-driven-development, Global Constraints из спеки и CLAUDE.md).
.gitignore: .codegraph/, .superpowers/, .bounded, .DS_Store.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 17:53:56 +04:00
aep
05a906ef90 Merge pull request 'Срез 0 (proposal): соглашения шаблона, сверка макета, change slice-0-skeleton (контракт v1.0, compose, проверки)' (#2) from feat/1-slice-0-skeleton into main
Reviewed-on: #2
2026-10-03 16:43:10 +03:00
21 changed files with 876 additions and 22 deletions

4
.gitignore vendored
View File

@ -1 +1,5 @@
.DS_Store
.codegraph/
.superpowers/
.bounded
dev/.env

View File

@ -28,7 +28,7 @@
(вычисления), согласование, ЕХ, аудит, права. Один Alembic, одна схема.
- **tabular-parser** — читает файл из S3 `uploads/`, находит на листах ОСВ (§1) и формы ББ/ОФР (§12) **по содержимому**,
пишет нормализованный JSON в S3 `results/`, публикует `recognition.result`. Контракт результата —
`docs/contracts/recognition-result.schema.json` (черновик; финализировать в срезе 2). Правила распознавания
`docs/contracts/recognition-result.schema.json` (контракт v1.0). Правила распознавания
заголовков/группировок — `rules/*.yaml`.
- **scan-gateway** — адаптер ИС Сканирование: переименовать файл `UUID_имя.pdf`, положить в «Входящие», опросить
«Исходящие/UUID/UUID.json», сохранить сырой ответ в S3 `raw/{job_id}.json` (в БД не регистрируется), нормализовать
@ -38,16 +38,19 @@
## Контракт обмена через NATS (DECISION-4)
Схемы — `docs/contracts/` (v1.0): `recognition-request.schema.json` и `recognition-result-message.schema.json` — тела
сообщений, `recognition-result.schema.json` — файл результата в S3; примеры — `docs/contracts/examples/`.
Схемы — `docs/contracts/` (v1.0), JSON Schema 2020-12: [`recognition-request.schema.json`](../contracts/recognition-request.schema.json)
и [`recognition-result-message.schema.json`](../contracts/recognition-result-message.schema.json) — тела сообщений,
[`recognition-result.schema.json`](../contracts/recognition-result.schema.json) — файл результата в S3; примеры —
[`docs/contracts/examples/`](../contracts/examples/). Примеры проверяются схемами: `python scripts/validate_contracts.py`
(из корня репозитория docs).
| Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` |
|---|---|---|---|---|
| запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `<job_id>:<attempt>` |
| результат | `recognition.result` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `error {code, message}`, `service {name, version}`, `result_s3 {bucket, key}`, `result_schema_version`, `raw_s3_key` (только scan-gateway), `metrics` | `result:<job_id>:<attempt>` |
| Поток | Subject | Кто пишет | Полезная нагрузка | `Nats-Msg-Id` | Схема |
|---|---|---|---|---|---|
| запрос | `recognition.tabular.request` / `recognition.image.request` | arm-core (outbox из `recognition_job`) | `schema_version`, `job_id`, `file_id`, `pipeline`, `file_type`, `original_name`, `s3 {bucket, key}` исходника, `reply_deadline`, `attempt` | `<job_id>:<attempt>` | [request](../contracts/recognition-request.schema.json) |
| ack | `recognition.result` | tabular-parser / scan-gateway | конверт со `schema_version`, `status = processing`, `job_id`, `file_id`, `service {name, version}` | `ack:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json) |
| результат | `recognition.result` | tabular-parser / scan-gateway | `schema_version`, `job_id`, `file_id`, `status` (done / needs_check / error), `service {name, version}`, `metrics` (опц.), `raw_s3_key` (опц., только scan-gateway); при `done` / `needs_check` обязательны `result_s3 {bucket, key}` и `result_schema_version`, при `error` — `error {code, message}` | `result:<job_id>:<attempt>` | [message](../contracts/recognition-result-message.schema.json); файл в S3 — [result](../contracts/recognition-result.schema.json) |
`group_schema` в конверт не входит — ядро берёт его из `documents[].group_schema` файла результата при маппинге.
Конверт со `status = processing` — подтверждение взятия в работу (ack), `Nats-Msg-Id = ack:<job_id>:<attempt>`.
**На платформе NATS без JetStream** (ответ 03.10.2026): нет durable consumer'ов, серверного дедупа и redelivery.
Поэтому гарантии доставки — на уровне приложения: сервисы подписаны queue group'ами (`tabular-parser`, `scan-gateway`),
@ -63,6 +66,14 @@
- Результат принимается подпиской queue group; `result_msg_id UNIQUE` = дедуп. Нет ack после повторов → `dlq`;
потерянный результат → `reply_deadline` → `timeout`; replay = сброс в `queued` с `attempt + 1`.
- Результат одного файла содержит **N документов**; ядро на каждый создаёт `document` и маппит данные.
- Три формы `Nats-Msg-Id`: запрос `<job_id>:<attempt>`, ack `ack:<job_id>:<attempt>`, результат `result:<job_id>:<attempt>`.
Ack (`processing`) сервис публикует **до чтения файла из S3**; повторная публикация запроса ядром идёт с тем же
`Nats-Msg-Id` и тем же `attempt`. Дубликат результата ядро определяет по `recognition_job.result_msg_id`.
- `schema_version` — `MAJOR.MINOR`; ядро и сервисы принимают тот же MAJOR и любой MINOR. Сервис отвергает запрос с
неизвестным MAJOR результатом `error` с кодом `SCHEMA_MISMATCH` и подтверждает его ack'ом, чтобы не зациклить
доставку. Новый MINOR раскатывается сначала на потребителей
(ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают
незнакомые значения.
- Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв
контрагентов, проверки — в ядре.

View File

@ -0,0 +1,14 @@
{
"schema_version": "1.0",
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
"pipeline": "image",
"file_type": "PDF",
"original_name": "bb_2025_q3.pdf",
"s3": {
"bucket": "arm-ka",
"key": "uploads/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/bb_2025_q3.pdf"
},
"reply_deadline": "2026-10-03T12:45:00Z",
"attempt": 1
}

View File

@ -0,0 +1,14 @@
{
"schema_version": "1.0",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"pipeline": "tabular",
"file_type": "XLSX",
"original_name": "osv_60_2025_q1.xlsx",
"s3": {
"bucket": "arm-ka",
"key": "uploads/a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02/3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01/osv_60_2025_q1.xlsx"
},
"reply_deadline": "2026-10-03T12:30:00Z",
"attempt": 1
}

View File

@ -0,0 +1,12 @@
{
"schema_version": "1.0",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"service": { "name": "tabular-parser", "version": "0.1.0" },
"status": "error",
"error": {
"code": "NOT_IMPLEMENTED",
"message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet"
},
"documents": []
}

View File

@ -0,0 +1,21 @@
{
"schema_version": "1.0",
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
"status": "done",
"service": { "name": "scan-gateway", "version": "0.1.0" },
"result_s3": {
"bucket": "arm-ka",
"key": "results/c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03/result.json"
},
"result_schema_version": "1.0",
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
"metrics": {
"duration_ms": 18400,
"pages": 2,
"fields_total": 4,
"fields_low_confidence": 0,
"fields_unrecognized": 0,
"warnings": []
}
}

View File

@ -0,0 +1,11 @@
{
"schema_version": "1.0",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"status": "error",
"service": { "name": "tabular-parser", "version": "0.1.0" },
"error": {
"code": "NOT_IMPLEMENTED",
"message": "Разбор ОСВ пока не реализован / OSV parsing is not implemented yet"
}
}

View File

@ -0,0 +1,7 @@
{
"schema_version": "1.0",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"status": "processing",
"service": { "name": "tabular-parser", "version": "0.1.0" }
}

View File

@ -0,0 +1,79 @@
{
"schema_version": "1.0",
"job_id": "3f2a9c1e-7b4d-4e0a-9d51-2c8e6a1b0f01",
"file_id": "a1c4e7d2-5b36-4f89-8e12-9d0c3b7a4e02",
"service": { "name": "tabular-parser", "version": "0.1.0" },
"status": "done",
"documents": [
{
"document_type": "OSV",
"locator": "sheet:Лист_1",
"header": {
"organization_name": "ООО Ромашка",
"organization_inn": "7700000000",
"account_code_raw": "60",
"period_text": "1 квартал 2025 г.",
"period_year": 2025,
"period_quarter": "Q1",
"is_cumulative": false,
"data_type": "BU",
"currency": "RUB",
"header_variant": 1
},
"group_schema": [
{ "type": "COUNTERPARTY", "name": "Контрагенты", "has_inn": true }
],
"columns": {
"opening_debit": "C",
"opening_credit": "D",
"turnover_debit": "E",
"turnover_credit": "F",
"closing_debit": "G",
"closing_credit": "H"
},
"rows": [
{
"row_number": 1,
"level": 0,
"parent_row_number": null,
"row_value": "60",
"group_type": "ACCOUNT",
"group_name": null,
"account_code_raw": "60",
"counterparty_name": null,
"counterparty_inn": null,
"data_type": "BU",
"opening_debit": "0.00",
"opening_credit": "1000.00",
"turnover_debit": "500.00",
"turnover_credit": "200.50",
"closing_debit": "0.00",
"closing_credit": "700.50"
},
{
"row_number": 2,
"level": 1,
"parent_row_number": 1,
"row_value": "ООО Лютик, ИНН 7711111111",
"group_type": "COUNTERPARTY",
"group_name": "Контрагенты",
"account_code_raw": "60",
"counterparty_name": "ООО Лютик",
"counterparty_inn": "7711111111",
"data_type": "BU",
"opening_debit": "0.00",
"opening_credit": "1000.00",
"turnover_debit": "500.00",
"turnover_credit": "200.50",
"closing_debit": "0.00",
"closing_credit": "700.50"
}
],
"checks": {
"total_row_matches_root": true,
"balance_identity_violations": [],
"parent_sum_violations": []
}
}
]
}

View File

@ -0,0 +1,50 @@
{
"schema_version": "1.0",
"job_id": "8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03",
"file_id": "c7e2a9b4-1d58-4630-a2f7-6b4d8e0c1f04",
"service": { "name": "scan-gateway", "version": "0.1.0" },
"status": "done",
"raw_s3_key": "raw/8d6b1f30-2e94-4a7c-b5d8-0f1e9c3a6b03.json",
"documents": [
{
"document_type": "BALANCE",
"locator": "pages:1-2",
"header": {
"form_name": "Бухгалтерский баланс",
"organization_name": "ООО Ромашка",
"organization_name_confidence": 97.5,
"inn": "7700000000",
"inn_confidence": 99,
"report_date_text": "30 сентября 2025 г.",
"report_date": "2025-09-30",
"units": "тыс. руб."
},
"columns": [
{ "column_index": 0, "label": "На 30 сентября 2025 г.", "period_date": "2025-09-30" },
{ "column_index": 1, "label": "На 31 декабря 2024 г.", "period_date": "2024-12-31" }
],
"lines": [
{
"line_code": "1100",
"line_code_confidence": 99,
"line_name": "Итого по разделу I",
"line_name_confidence": 96,
"values": [
{ "column_index": 0, "amount": "1234.50", "confidence": 95, "is_recognized": true },
{ "column_index": 1, "amount": "1100", "confidence": 93, "is_recognized": true }
]
},
{
"line_code": "1600",
"line_code_confidence": 99,
"line_name": "БАЛАНС (актив)",
"line_name_confidence": 98,
"values": [
{ "column_index": 0, "amount": "5678.00", "confidence": 94.5, "is_recognized": true },
{ "column_index": 1, "amount": "-12.30", "confidence": 90, "is_recognized": true }
]
}
]
}
]
}

View File

@ -0,0 +1,28 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "arm-ka/recognition-request/v1",
"title": "Запрос распознавания файла (arm-core → tabular-parser / scan-gateway), контракт v1.0",
"description": "Тело сообщения на subject recognition.tabular.request или recognition.image.request; subject должен соответствовать полю pipeline (tabular → recognition.tabular.request, image → recognition.image.request). Заголовок Nats-Msg-Id = <job_id>:<attempt> (не поле тела). Один файл (stored_file) → один job.",
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
"type": "object",
"required": ["schema_version", "job_id", "file_id", "pipeline", "file_type", "original_name", "s3", "reply_deadline", "attempt"],
"properties": {
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" },
"job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" },
"file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" },
"pipeline": { "enum": ["tabular", "image"], "description": "конвейер распознавания; определяет subject запроса" },
"file_type": { "enum": ["XLS", "XLSX", "XLSB", "PDF", "PNG", "JPG"] },
"original_name": { "type": "string", "minLength": 1, "description": "имя файла, как загрузил пользователь" },
"s3": {
"type": "object",
"description": "исходный файл в роли uploads",
"required": ["bucket", "key"],
"properties": {
"bucket": { "type": "string", "minLength": 1 },
"key": { "type": "string", "minLength": 1 }
}
},
"reply_deadline": { "type": "string", "format": "date-time", "description": "срок ответа, UTC" },
"attempt": { "type": "integer", "minimum": 1, "description": "номер попытки, с 1" }
}
}

View File

@ -0,0 +1,70 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "arm-ka/recognition-result-message/v1",
"title": "Сообщение результата распознавания (tabular-parser / scan-gateway → arm-core), контракт v1.0",
"description": "Тело сообщения на subject recognition.result. Заголовок Nats-Msg-Id (не поле тела) на этом subject имеет две формы: ack:<job_id>:<attempt> — для status = processing; result:<job_id>:<attempt> — для done, needs_check и error. Форма запроса <job_id>:<attempt> описана в схеме recognition-request. status = processing — ack «взял в работу», публикуется до чтения файла из S3. group_schema в конверт не входит: источник — файл результата (recognition-result).",
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
"type": "object",
"required": ["schema_version", "job_id", "file_id", "status", "service"],
"properties": {
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" },
"job_id": { "type": "string", "format": "uuid", "description": "recognition_job.id" },
"file_id": { "type": "string", "format": "uuid", "description": "stored_file.id" },
"status": { "enum": ["processing", "done", "needs_check", "error"] },
"service": {
"type": "object",
"required": ["name", "version"],
"properties": {
"name": { "enum": ["tabular-parser", "scan-gateway"] },
"version": { "type": "string" }
}
},
"result_s3": {
"type": "object",
"description": "файл результата (recognition-result): results/{file_id}/{job_id}/result.json",
"required": ["bucket", "key"],
"properties": {
"bucket": { "type": "string", "minLength": 1 },
"key": { "type": "string", "minLength": 1 }
}
},
"result_schema_version": { "type": "string", "pattern": "^\\d+\\.\\d+$", "description": "schema_version файла результата, MAJOR.MINOR" },
"error": {
"type": "object",
"required": ["code", "message"],
"properties": {
"code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] },
"message": { "type": "string" }
}
},
"raw_s3_key": { "type": ["string", "null"], "description": "scan-gateway: raw/{job_id}.json — сырой ответ ИС Сканирование; в БД не регистрируется" },
"metrics": {
"type": "object",
"properties": {
"duration_ms": { "type": "integer" },
"pages": { "type": "integer" },
"rows": { "type": "integer" },
"fields_total": { "type": "integer" },
"fields_low_confidence": { "type": "integer" },
"fields_unrecognized": { "type": "integer" },
"warnings": { "type": "array", "items": { "type": "string" } }
}
}
},
"allOf": [
{
"if": {
"properties": { "status": { "enum": ["done", "needs_check"] } },
"required": ["status"]
},
"then": { "required": ["result_s3", "result_schema_version"] }
},
{
"if": {
"properties": { "status": { "const": "error" } },
"required": ["status"]
},
"then": { "required": ["error"] }
}
]
}

View File

@ -1,12 +1,13 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "arm-ka/recognition-result/v1",
"title": "Результат распознавания файла (tabular-parser / scan-gateway) — ЧЕРНОВИК v1, уточнить в Срезе 2",
"title": "Результат распознавания файла (tabular-parser / scan-gateway), контракт v1.0",
"description": "Один файл (stored_file) → один job → N документов. Сервисы не знают БД: только структура файла. Нормализация счетов, резолв контрагентов, проверки — в arm-core.",
"$comment": "Версионирование: schema_version = MAJOR.MINOR. MINOR (1.1) — необязательные поля и новые значения enum; MAJOR (2.0) — удаление/переименование полей, смена типов или семантики. Потребитель принимает тот же MAJOR и любой MINOR. Новый MINOR раскатывается сначала на потребителей (ядро и сервисы обновляют копии схем раньше, чем производитель начнёт слать 1.1), потому что закрытые enum отвергают незнакомые значения.",
"type": "object",
"required": ["schema_version", "job_id", "file_id", "service", "status", "documents"],
"properties": {
"schema_version": { "const": "1.0" },
"schema_version": { "type": "string", "pattern": "^1\\.\\d+$", "description": "MAJOR.MINOR; производитель контракта v1.0 пишет \"1.0\"" },
"job_id": { "type": "string", "format": "uuid" },
"file_id": { "type": "string", "format": "uuid" },
"service": {
@ -21,7 +22,7 @@
"error": {
"type": ["object", "null"],
"properties": {
"code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] },
"code": { "enum": ["UNSUPPORTED_FORMAT", "UNREADABLE", "TIMEOUT", "SCHEMA_MISMATCH", "NOT_IMPLEMENTED", "FILE_READ_ERROR", "INVALID_FORMAT", "INVALID_DOCUMENT", "RECOGNITION_FAILED", "UNKNOWN_ERROR"] },
"message": { "type": "string" }
}
},
@ -43,6 +44,13 @@
"items": { "oneOf": [ { "$ref": "#/$defs/osvDocument" }, { "$ref": "#/$defs/statementDocument" } ] }
}
},
"if": {
"properties": { "status": { "enum": ["done", "needs_check"] } },
"required": ["status"]
},
"then": {
"properties": { "documents": { "minItems": 1 } }
},
"$defs": {
"money": { "type": ["string", "null"], "pattern": "^-?\\d+(\\.\\d{1,2})?$", "description": "Decimal как строка, чтобы не терять копейки в JSON" },
"osvDocument": {
@ -131,9 +139,9 @@
"properties": {
"form_name": { "type": "string" },
"organization_name": { "type": ["string", "null"] },
"organization_name_confidence": { "type": ["number", "null"] },
"organization_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
"inn": { "type": ["string", "null"] },
"inn_confidence": { "type": ["number", "null"] },
"inn_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
"report_date_text": { "type": ["string", "null"] },
"report_date": { "type": ["string", "null"], "format": "date" },
"units": { "type": ["string", "null"], "examples": ["тыс. руб."] },
@ -163,9 +171,9 @@
"required": ["line_code", "line_name", "values"],
"properties": {
"line_code": { "type": "string" },
"line_code_confidence": { "type": ["number", "null"] },
"line_code_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
"line_name": { "type": "string" },
"line_name_confidence": { "type": ["number", "null"] },
"line_name_confidence": { "type": ["number", "null"], "minimum": 0, "maximum": 100 },
"values": {
"type": "array",
"items": {

51
dev/.env.example Normal file
View File

@ -0,0 +1,51 @@
# Локальное окружение разработки. Скопируйте в .env: cp .env.example .env (.env в git не попадает)
# Файл читают docker compose (из этого каталога) и, блоком ниже, сервисы при запуске из venv.
# --- Переменные docker-compose: учётные данные, порты хоста, бакет ---
# Адрес привязки портов хоста: 127.0.0.1 — только эта машина; 0.0.0.0 — доступ с других машин (пароли по умолчанию dev)
BIND_ADDR=127.0.0.1
# PostgreSQL 16 (схема dkb создаётся миграциями arm-core, не compose)
POSTGRES_DB=armka
POSTGRES_USER=armka
POSTGRES_PASSWORD=armka
POSTGRES_PORT=5432
# NATS 2.10 без JetStream: клиентский порт и порт мониторинга (/varz, /healthz)
NATS_PORT=4222
NATS_MONITOR_PORT=8222
# MinIO: учётная запись администратора, порты API и консоли, единственный бакет
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin
MINIO_API_PORT=9000
MINIO_CONSOLE_PORT=9001
MINIO_BUCKET=arm-ka
# --- Настройки сервисов при запуске из venv против compose по localhost (формат шаблона команды) ---
# Значения согласованы с блоком выше: при смене порта/пароля/бакета меняйте обе части.
# Если порт хоста изменён, поправьте и порты ниже.
DEBUG=true
# PostgreSQL (драйвер asyncpg)
OPENBAO__DATABASE__DATABASE_URL=postgresql+asyncpg://armka:armka@localhost:5432/armka
# MinIO: адрес без схемы (host:port) — так ждёт miniopy-async
OPENBAO__MINIO__MINIO_URL=localhost:9000
OPENBAO__MINIO__MINIO_ADMIN_LOGIN=minioadmin
OPENBAO__MINIO__MINIO_ADMIN_PASSWORD=minioadmin
OPENBAO__MINIO__MINIO_BUCKET=arm-ka
# NATS без JetStream; пустые NATS_SEED/NATS_JWT — подключение без nkeys
OPENBAO__NATS__NATS_URL=nats://localhost:4222
OPENBAO__NATS__NATS_SEED=
OPENBAO__NATS__NATS_JWT=
# SMB — заглушки (в Срезе 0 не используется); блок обязателен для Settings arm-core
OPENBAO__SMB__SMB_HOST=localhost
OPENBAO__SMB__SMB_PATH=share
OPENBAO__SMB__SMB_LOGIN=user
OPENBAO__SMB__SMB_PASSWORD=password
# Включает подключение arm-core к NATS (без него /healthcheck показывает nats=off)
NATS_ENABLED=true

176
dev/README.md Normal file
View File

@ -0,0 +1,176 @@
# Окружение разработки АРМ КА (`docs/dev/`)
Локальная инфраструктура workspace одной командой: PostgreSQL 16, NATS 2.10 без JetStream и MinIO с бакетом `arm-ka`.
**Статус (фаза 1):** в compose сейчас только инфраструктура (`postgres`, `nats`, `minio`, `minio-init`). Профиль
`services` (контейнеры `arm-core`, `tabular-parser`, `scan-gateway`, `scan-emulator`) **планируется** — добавляется позже
(задача 5.3), здесь не описан. До этого сервисы запускаются из venv против этой инфраструктуры (см. ниже).
## Предварительные требования
- Docker и compose v2 (проверено на docker 28 / compose 2.34).
- `uv` и Python 3.11 — для запуска сервисов из venv (версия Python — как на платформе).
- Свободные порты хоста (по умолчанию доступны только с `127.0.0.1`, см. `BIND_ADDR`): 5432 (PostgreSQL), 4222 и 8222 (NATS и его мониторинг), 9000 и 9001 (MinIO API и консоль).
Занятые порты меняются в `.env` (см. «Типичные проблемы»).
## Запуск инфраструктуры
Команды — из каталога `docs/dev/`:
```bash
cp .env.example .env # .env в git не попадает
docker compose up -d # вернётся сразу; healthy — примерно через 6 с
```
Ожидание готовности (для скриптов и `Makefile`):
```bash
docker compose up -d && docker compose up -d --wait postgres nats minio
```
Голый `docker compose up -d --wait` **возвращает код 1**: одноразовый `minio-init` (создаёт бакет и завершается с
кодом 0) compose 2.34 считает сбоем (`container arm-ka-minio-init-1 exited (0)`). Поэтому `--wait` нужно давать со
списком долгоживущих сервисов — как выше.
Проверка:
```bash
docker compose ps # postgres, nats, minio — Up (healthy); minio-init — Exited (0)
docker compose exec postgres sh -c 'psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "select version()"'
curl -s localhost:8222/healthz # {"status":"ok"}
curl -s localhost:8222/varz | grep -A1 jetstream # "jetstream": {} — JetStream не включён
docker compose run --rm --entrypoint sh minio-init -c 'mc alias set local http://minio:9000 "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" >/dev/null && mc ls local'
# [...] 0B arm-ka/
```
Консоль MinIO — <http://localhost:9001> (по умолчанию `minioadmin` / `minioadmin`); S3 API — `localhost:9000`.
Схема БД (`dkb`) создаётся миграциями `arm-core`, не compose.
## Переменные `.env`
Файл читают `docker compose` (из этого каталога) и, вторым блоком, сервисы при запуске из venv. Значения по умолчанию —
только для dev.
Блок compose:
| Переменная | По умолчанию | Для чего |
|---|---|---|
| `BIND_ADDR` | `127.0.0.1` | адрес привязки всех портов хоста; `0.0.0.0` — доступ с других машин (небезопасно, см. «Примечания») |
| `POSTGRES_DB` | `armka` | имя БД |
| `POSTGRES_USER` | `armka` | пользователь БД |
| `POSTGRES_PASSWORD` | `armka` | пароль БД |
| `POSTGRES_PORT` | `5432` | порт PostgreSQL на хосте |
| `NATS_PORT` | `4222` | клиентский порт NATS на хосте |
| `NATS_MONITOR_PORT` | `8222` | порт мониторинга NATS (`/varz`, `/healthz`) |
| `MINIO_ROOT_USER` | `minioadmin` | администратор MinIO |
| `MINIO_ROOT_PASSWORD` | `minioadmin` | пароль администратора MinIO |
| `MINIO_API_PORT` | `9000` | порт S3 API на хосте |
| `MINIO_CONSOLE_PORT` | `9001` | порт консоли MinIO на хосте |
| `MINIO_BUCKET` | `arm-ka` | единственный бакет, создаётся `minio-init` |
Блок сервисов (запуск из venv против compose по `localhost`, формат шаблона команды):
| Переменная | По умолчанию | Для чего |
|---|---|---|
| `DEBUG` | `true` | dev-режим: пакеты `raisa_*` импортируются условно |
| `OPENBAO__DATABASE__DATABASE_URL` | `postgresql+asyncpg://armka:armka@localhost:5432/armka` | подключение к PostgreSQL (asyncpg) |
| `OPENBAO__MINIO__MINIO_URL` | `localhost:9000` | адрес MinIO без схемы (`host:port`, так ждёт `miniopy-async`) |
| `OPENBAO__MINIO__MINIO_ADMIN_LOGIN` | `minioadmin` | логин MinIO |
| `OPENBAO__MINIO__MINIO_ADMIN_PASSWORD` | `minioadmin` | пароль MinIO |
| `OPENBAO__MINIO__MINIO_BUCKET` | `arm-ka` | бакет |
| `OPENBAO__NATS__NATS_URL` | `nats://localhost:4222` | адрес NATS |
| `OPENBAO__NATS__NATS_SEED` | пусто | nkey seed; пусто — подключение без nkeys |
| `OPENBAO__NATS__NATS_JWT` | пусто | JWT NATS; пусто — подключение без nkeys |
| `OPENBAO__SMB__SMB_HOST` | `localhost` | заглушка SMB (в Срезе 0 не используется); блок обязателен для `Settings` arm-core |
| `OPENBAO__SMB__SMB_PATH` | `share` | заглушка SMB |
| `OPENBAO__SMB__SMB_LOGIN` | `user` | заглушка SMB |
| `OPENBAO__SMB__SMB_PASSWORD` | `password` | заглушка SMB |
| `NATS_ENABLED` | `true` | включает подключение arm-core к NATS; без него `/healthcheck` показывает `nats=off` |
Два блока описывают одни и те же ресурсы: **при смене порта, пароля, пользователя или бакета меняйте обе части.**
## Сброс окружения
```bash
docker compose down # останавливает контейнеры; тома postgres-data и minio-data сохраняются
docker compose down -v # полный сброс: удаляет тома postgres-data и minio-data (БД и бакет — с нуля)
```
Имя проекта compose фиксировано (`name: arm-ka`), поэтому тома общие для любого worktree (`.wt/<срез>/docs/dev`):
сброс из одного worktree затрагивает все.
## Запуск сервисов из venv без Nexus (черновик)
> Черновик: будет дописан и проверен вручную в задаче 5.4 (сквозная проверка). `make venv` и `make test-integration`
> в `tabular-parser` и `scan-gateway` будут доступны после задач этих сервисов; сейчас они есть в `arm-core`.
Пакеты `raisa_*` из Nexus банка вне банка недоступны (D-10). Поэтому `make venv` ставит `requirements.txt` без строк
`raisa_*` (фильтр `grep -v '^raisa_'`), код импортирует `raisa_*` условно (при `DEBUG=true`), а npm-пакеты `raisa-*`
в `web/package.json` вынесены в `optionalDependencies`.
Шаги для сервиса (пути — относительно каталога его репозитория; в workspace `docs/` лежит рядом, `../docs/dev/`):
```bash
# 1. поднять инфраструктуру (из docs/dev/, см. выше)
# 2. из корня репозитория сервиса:
cp ../docs/dev/.env.example api/.env
make venv # api/.venv: Python 3.11 + зависимости без raisa_*
cd api && .venv/bin/python run_server.py # сервис подключается к PostgreSQL/NATS/MinIO по localhost
# 3. в другом терминале:
curl -s localhost:<порт сервиса>/healthcheck
```
Порт сервиса задаётся его настройками; в профиле `services` по spec это 8000 (`arm-core`), 8001 (`tabular-parser`),
8002 (`scan-gateway`). Для `arm-core` в ответе `/healthcheck` ожидаются `version` и `nats = ok` — при `NATS_ENABLED=true`
(задан в `.env.example`; без него `nats = off`). Для `tabular-parser` и `scan-gateway` ожидаемые поля и переменные
зависят от их настроек — будет уточнено в задачах 5.x.
## Интеграционные тесты сервисов против compose (черновик)
> Черновик: уточняется после задач 3.x и 5.x.
**Сейчас.** Makefile есть только у `arm-core`: `make test-integration` (`pytest -m integration`),
`make check` = `lint` + `test` (ruff, mypy, `pytest -m "not integration"`), `make check-full` = `check` +
`test-integration`. У `tabular-parser` и `scan-gateway` Makefile пока нет.
**Планируется** (spec `quality-checks`, реализуется в задачах 3.x/4.x/5.x): одинаковый набор целей во всех сервисных
репозиториях со следующей семантикой:
- `make test-integration` — `pytest -m integration` против инфраструктуры из этого compose;
- `make check` — быстрые проверки без Docker (ruff, mypy, `pytest -m "not integration"`, идентичность контракта);
- `make check-full` — `make check` + интеграционные тесты; для `arm-core` — ещё «`alembic upgrade head` →
`downgrade base` → `upgrade head`» и `scripts/check_dbml_vs_orm.py` (этих шагов и скрипта пока нет). Выполняется
перед созданием PR.
Интеграционные тесты **требуют поднятой инфраструктуры**: сначала `docker compose up -d && docker compose up -d --wait
postgres nats minio`, затем `make test-integration` (или `make check-full`) в репозитории сервиса. Для `scan-gateway`
дополнительно нужен эмулятор ИС Сканирование (появится вместе с профилем `services`). Общее описание проверок —
`docs/dev/checks.md` (будет добавлено).
## Проверка контракта
Из корня репозитория `docs`: `python scripts/validate_contracts.py` — валидирует примеры в
[`contracts/examples/`](../contracts/examples/) тремя схемами из [`contracts/`](../contracts/) (3 схемы, 8 примеров;
нужен `jsonschema`).
## Примечания
- **Образы MinIO неофициальные.** Официальные `minio/minio` и `minio/mc` недоступны (Docker Hub — denied, на quay.io нет
нужных тегов), поэтому в compose стоят `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и
`bitnamilegacy/minio-client:2025.7.21`, а сервис `minio` запущен с `user: "0:0"` (иначе образ не пишет в свежий том) —
только для dev. Решения R-5/R-6: [`design.md`](../openspec/changes/slice-0-skeleton/design.md), раздел
«Decisions made without discussion», решения №16–17. Замена на официальные образы — две строки `image:` в
`docker-compose.yml` (и удаление `user: "0:0"`).
- **Безопасность.** По умолчанию порты публикуются только на `127.0.0.1` (`BIND_ADDR`), снаружи машины они недоступны.
Чтобы открыть их для других машин, задайте в `.env` `BIND_ADDR=0.0.0.0` — но пароли по умолчанию (`armka`,
`minioadmin`) годятся только для dev, поэтому сначала смените их (в обоих блоках `.env`) и не делайте этого в
недоверенной сети.
## Типичные проблемы
- **Порт занят** (`address already in use`): измените `*_PORT` в `.env` и одновременно соответствующий порт в
`OPENBAO__*` (`DATABASE_URL`, `MINIO_URL`, `NATS_URL`); перезапустите `docker compose up -d`.
- **`minio-init` завис или завершился с ошибкой:** `docker compose logs minio-init`; чаще всего `minio` не стал healthy —
`docker compose logs minio`. Повторный `docker compose up -d` идемпотентен.
- **Остаточные тома от прошлых запусков** (старый пароль БД, бакет не тот): `docker compose down -v` и запуск заново.
- **`up -d --wait` возвращает код 1:** см. раздел «Запуск инфраструктуры» — указывайте сервисы явно.

84
dev/docker-compose.yml Normal file
View File

@ -0,0 +1,84 @@
# Инфраструктура разработки АРМ КА: PostgreSQL 16, NATS 2.10 (без JetStream, как на платформе), MinIO + бакет.
# Запуск: cp .env.example .env && docker compose up -d (из этого каталога)
# Ожидание готовности: docker compose up -d --wait postgres nats minio. Голый `up -d --wait` возвращает код 1, потому что
# одноразовый minio-init завершается (хотя и с кодом 0) — для скриптов указывайте сервисы явно.
# Все учётные данные, порты хоста и имя бакета берутся из .env (значения по умолчанию — dev).
# Порты публикуются только на 127.0.0.1; адрес привязки меняется через BIND_ADDR (см. .env.example).
# Профиль `services` (arm-core, tabular-parser, scan-gateway, scan-emulator) добавляется в фазе 2.
# Фиксированное имя проекта: одинаковые тома и сети из любого worktree (.wt/<срез>/docs/dev).
name: arm-ka
services:
postgres:
image: postgres:16.10-alpine
environment:
POSTGRES_DB: ${POSTGRES_DB:-armka}
POSTGRES_USER: ${POSTGRES_USER:-armka}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-armka}
ports:
- "${BIND_ADDR:-127.0.0.1}:${POSTGRES_PORT:-5432}:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 3s
timeout: 3s
retries: 20
start_period: 5s
# Core NATS без JetStream: флаг --jetstream не передаётся. Мониторинг — порт 8222 (/varz, /healthz).
nats:
image: nats:2.10.29-alpine
command: ["-m", "8222"]
ports:
- "${BIND_ADDR:-127.0.0.1}:${NATS_PORT:-4222}:4222"
- "${BIND_ADDR:-127.0.0.1}:${NATS_MONITOR_PORT:-8222}:8222"
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:8222/healthz || exit 1"]
interval: 3s
timeout: 3s
retries: 20
start_period: 2s
# Образы minio/minio и minio/mc сняты с Docker Hub и quay.io; используется сборка alpine/minio с RELEASE-тегом.
minio:
image: alpine/minio:RELEASE.2025-10-15T17-29-55Z
# Образ по умолчанию работает от пользователя minio и не может писать в свежий именованный том — в dev запускаем от root.
user: "0:0"
command: ["server", "/data", "--console-address", ":9001"]
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
ports:
- "${BIND_ADDR:-127.0.0.1}:${MINIO_API_PORT:-9000}:9000"
- "${BIND_ADDR:-127.0.0.1}:${MINIO_CONSOLE_PORT:-9001}:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://127.0.0.1:9000/minio/health/live || exit 1"]
interval: 3s
timeout: 3s
retries: 20
start_period: 3s
# Одноразовое создание бакета; идемпотентно (--ignore-existing), при повторном up снова завершается с кодом 0.
minio-init:
image: bitnamilegacy/minio-client:2025.7.21
depends_on:
minio:
condition: service_healthy
restart: "no"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
MINIO_BUCKET: ${MINIO_BUCKET:-arm-ka}
entrypoint: ["/bin/sh", "-c"]
command:
- >-
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD" &&
mc mb --ignore-existing "local/$$MINIO_BUCKET"
volumes:
postgres-data:
minio-data:

View File

@ -0,0 +1,45 @@
# Срез 0 — сквозной скелет (docs) — фаза 1: контракт и инфраструктура — план исполнения (Срез 0)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Branch:** `feat/1-slice-0-skeleton` (worktree `.wt/slice-0/docs`). **Issue:** #1.
**Spec:** `openspec/changes/slice-0-skeleton/` — proposal.md, design.md, specs/*/spec.md этого change'а. Спека — единственный источник истины при рулингах; этот план сгенерирован из её `tasks.md` 1:1 (номера в скобках — исходные номера задач tasks.md; по завершении задачи отметить её `- [x]` и в `tasks.md`).
## Global Constraints
- Репозиторий `arm-ka-docs`: документы, контракты, `docs/dev/`. `docs/source/**` не редактировать.
- Контракт v1.0: три схемы JSON Schema 2020-12 (`recognition-request`, `recognition-result-message`, `recognition-result`), `schema_version` = `"1.0"`; статусы конверта `processing | done | needs_check | error`; коды ошибок — перечень из spec `recognition-contract` (включая `NOT_IMPLEMENTED`); деньги — строки `^-?\d+(\.\d{1,2})?$`; `Nats-Msg-Id`: `<job_id>:<attempt>`, `ack:<job_id>:<attempt>`, `result:<job_id>:<attempt>`.
- Платформа: Python 3.11, PostgreSQL 16, NATS 2.10 **без JetStream**, один бакет S3 `arm-ka`; Nexus недоступен; CI нет — `make check` / `make check-full`.
- Compose собирает сервисы из `../../<repo>` относительно `docs/dev/`; версии образов фиксированы.
- Язык документов и коммитов — русский; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`.
## Задачи
### Task 1: [1.1] [docs] `docs/contracts/recognition-result.schema.json`
- [x] [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы (метасхема 2020-12 валидна)
### Task 2: [1.2] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recogniti
- [x] [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum, форматы uuid/date-time); проверить метасхемой
### Task 3: [1.3] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт
- [x] [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done | needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой
### Task 4: [1.4] [docs] Примеры `docs/contracts/examples/`
- [x] [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`, `result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`, `result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все примеры схемами и возвращает 0
### Task 5: [1.5] [docs] `03-architecture.md`
- [x] [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила `Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы
### Task 6: [2.1] [docs] `docs/dev/docker-compose.yml`
- [x] [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`, `minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка: `docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает
### Task 7: [2.2] [docs] `docs/dev/README.md`
- [x] [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus (`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4

View File

@ -0,0 +1,45 @@
# Срез 0 — сквозной скелет (docs) — фаза 2: проверки, профиль services, документы — план исполнения (Срез 0)
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Branch:** `feat/1-slice-0-skeleton` (worktree `.wt/slice-0/docs`). **Issue:** #1.
**Spec:** `openspec/changes/slice-0-skeleton/` — proposal.md, design.md, specs/*/spec.md этого change'а. Спека — единственный источник истины при рулингах; этот план сгенерирован из её `tasks.md` 1:1 (номера в скобках — исходные номера задач tasks.md; по завершении задачи отметить её `- [x]` и в `tasks.md`).
## Global Constraints
- Репозиторий `arm-ka-docs`: документы, контракты, `docs/dev/`. `docs/source/**` не редактировать.
- Контракт v1.0: три схемы JSON Schema 2020-12 (`recognition-request`, `recognition-result-message`, `recognition-result`), `schema_version` = `"1.0"`; статусы конверта `processing | done | needs_check | error`; коды ошибок — перечень из spec `recognition-contract` (включая `NOT_IMPLEMENTED`); деньги — строки `^-?\d+(\.\d{1,2})?$`; `Nats-Msg-Id`: `<job_id>:<attempt>`, `ack:<job_id>:<attempt>`, `result:<job_id>:<attempt>`.
- Платформа: Python 3.11, PostgreSQL 16, NATS 2.10 **без JetStream**, один бакет S3 `arm-ka`; Nexus недоступен; CI нет — `make check` / `make check-full`.
- Compose собирает сервисы из `../../<repo>` относительно `docs/dev/`; версии образов фиксированы.
- Язык документов и коммитов — русский; коммит заканчивается строкой `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>`.
## Задачи
### Task 1: [4.1] [docs] `docs/dev/checks.md`
- [ ] [docs] `docs/dev/checks.md`: общие цели `Makefile` (`up`, `test`, `test-integration`, `lint`, `check`, `check-full`), что делает каждая, порядок перед PR, как пропускаются тесты без фикстур/docs; проверка: цели `arm-core` соответствуют описанию
### Task 2: [4.2] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` се
- [ ] [docs] Эталонный фрагмент `docs/dev/Makefile.common` (включается в `Makefile` сервисов через `include` или копируется); проверка: `make check` в трёх репозиториях вызывает одинаковые команды (`diff` общих целей)
### Task 3: [5.3] [docs] `docker-compose.yml`
- [ ] [docs] `docker-compose.yml`: профиль `services` — `arm-core` (8000), `tabular-parser` (8001), `scan-gateway` (8002), `scan-emulator` (том `hotfolder`, `../../fixtures/ocr:ro`); проверка: `--profile services up --build`, все `/healthcheck` 200 с `nats = ok`
### Task 4: [5.4] [docs] Сквозная проверка руками по README
- [ ] [docs] Сквозная проверка руками по README: вставить `recognition_job` в БД через `psql` (или тестовый скрипт arm-core) → паблишер публикует → `tabular-parser` отвечает ack и `NOT_IMPLEMENTED` → `recognition_job.status` проходит `published → processing → error`, `error_code = NOT_IMPLEMENTED`; то же для `image` через эмулятор (`raw/{job_id}.json` появляется в MinIO); остановить `tabular-parser`, вставить задание — через 30 с повторная публикация, после трёх — `dlq`; зафиксировать в README как «проверка окружения»
### Task 5: [6.1] [docs] `06-test-data.md` §6
- [ ] [docs] `06-test-data.md` §6: `fixtures/directories/` — только тестовые ЮЛ/ГК (Срез 1); план счетов, правила элиминации, права — `arm-core/api/seed/`; `09-delivery-plan.md` Срез 0: решение D-1 по пакету контрактов, ссылка на `docs/dev/`, проверки вместо CI
### Task 6: [6.2] [docs] `07-open-questions.md`
- [ ] [docs] `07-open-questions.md`: OQ по итогам Среза 0 (контракт `data_type = NU` vs enum `osv_data_type`; обязательность ОСВ расширенного подхода; источник полного плана счетов 1С) и расхождения макета из задачи B; статусы допущений
### Task 7: [6.3] [docs] Итоговая проверка
- [ ] [docs] Итоговая проверка: `openspec validate slice-0-skeleton --strict` зелёный в четырёх репозиториях; ссылки между документами ведут на существующие файлы (`grep` по именам файлов)

View File

@ -144,3 +144,11 @@ PostgreSQL 16, схемы создавать можно. Остаются (не
| 10 | 2026-10-03 | D-9: `python:3.11-slim`, PostgreSQL 16, NATS 2.10 без JS, `node:22` до ответа | ответы пользователя; Node неизвестен | смена тега в compose/Dockerfile |
| 11 | 2026-10-03 | Worktree среза — зеркальный workspace `.wt/<срез>/` с симлинками | относительные пути `../docs`, `../fixtures` и хуки должны работать в ветке | переместить worktree |
| 12 | 2026-10-03 | D-10: `raisa_*` фильтруются при dev-установке, npm-пакеты `raisa-*` → `optionalDependencies` | Nexus недоступен до прода | вернуть две строки в `dependencies` |
| 13 | 2026-10-03 | Файл результата: пустой `documents` допустим только при `status = error` — выражено в схеме условием `if/then` (`minItems: 1`) | требование spec `recognition-contract` «Файл результата»; текст задачи 1.1 его не перечислял | ослабить/убрать условие в схеме, пересобрать копии контракта в сервисах |
| 14 | 2026-10-03 | Compose: фиксированное имя проекта `name: arm-ka`, порты хоста переопределяются через `.env` со значениями по умолчанию из spec | одинаковые тома/сети из любого worktree; порты 5432/4222 на машине разработчика могут быть заняты | смена имени проекта — потеря dev-томов (`down -v`), порты — правка `.env` |
| 15 | 2026-10-03 | Чекбоксы плана и `tasks.md` отмечает контроллер SDD отдельным коммитом после чистого ревью задачи | исполнитель не должен отмечать выполненным то, что ещё не прошло ревью | отметки правятся вручную |
| 16 | 2026-10-03 | Compose: MinIO берётся из неофициальных образов `alpine/minio:RELEASE.2025-10-15T17-29-55Z` и `bitnamilegacy/minio-client:2025.7.21` | `minio/minio` и `minio/mc` недоступны на Docker Hub (denied), на quay.io нужных тегов нет; для dev достаточно любого S3-совместимого MinIO нужной версии API | образы сторонние: при появлении доступа к банковскому реестру/официальным образам — заменить две строки `image:` в `dev/docker-compose.yml`; в прод эти образы не попадают |
| 17 | 2026-10-03 | Compose: сервис `minio` запускается от `user: "0:0"` | образ `alpine/minio` по умолчанию работает от пользователя `minio` и падает на свежем именованном томе | только dev; удалить строку, когда образ/том позволят писать без root |
| 18 | 2026-10-03 | `dev/.env.example` дополнен заглушками `OPENBAO__SMB__*` и `NATS_ENABLED=true` | `Settings` шаблона (arm-core) требует блок `OPENBAO.SMB`, без него сервис из venv не стартует; `NATS_ENABLED` по умолчанию `false`, и `/healthcheck` показывает `nats = off`; spec велит копировать именно `docs/dev/.env.example` | убрать две группы строк; сервисы с другим набором обязательных настроек получат лишние (игнорируемые) переменные |
| 19 | 2026-10-03 | `schema_version` во всех трёх схемах: `const "1.0"` заменён на `type: string, pattern: ^1\.\d+$` (производитель v1.0 пишет `"1.0"`); в `$comment` и в 03-architecture.md добавлено правило «новый MINOR раскатывается сначала на потребителей» | spec «Версионирование контракта»: потребитель принимает тот же MAJOR и любой MINOR, сервис «поддерживающий 1.x» принимает 1.1 и отвергает только 2.0; с `const` это неисполнимо, а копии схем уйдут в сервисы побайтно (финальное ревью I-1) | вернуть `const "1.0"` и пересобрать копии схем в сервисах; побочный риск — копии в `arm-core`/`tabular-parser`/`scan-gateway` нужно обновить (тест идентичности контракта упадёт, пока копии не обновлены) |
| 20 | 2026-10-03 | Compose: порты хоста публикуются на `${BIND_ADDR:-127.0.0.1}`, а не на 0.0.0.0 | пароли по умолчанию (`armka`, `minioadmin`); все сценарии идут через localhost; spec bind-адрес не оговаривает (финальное ревью M-6) | если нужен доступ с другой машины — `BIND_ADDR=0.0.0.0` в `.env`; для запуска профиля `services` порты хоста не нужны |

View File

@ -5,26 +5,26 @@
## 1. Контракт v1.0 (первым — от него зависят все сервисы)
- [ ] 1.1 [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о
- [x] 1.1 [docs] `docs/contracts/recognition-result.schema.json`: снять «ЧЕРНОВИК» из title, добавить `$comment` о
версионировании, добавить `NOT_IMPLEMENTED` в `error.code`; проверить `python -m jsonschema` на файле схемы
(метасхема 2020-12 валидна)
- [ ] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum,
- [x] 1.2 [docs] Новый `docs/contracts/recognition-request.schema.json` по spec `recognition-contract` (поля, enum,
форматы uuid/date-time); проверить метасхемой
- [ ] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done |
- [x] 1.3 [docs] Новый `docs/contracts/recognition-result-message.schema.json` (конверт: `status` ∈ processing | done |
needs_check | error; условные обязательные поля через `if/then`); проверить метасхемой
- [ ] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`,
- [x] 1.4 [docs] Примеры `docs/contracts/examples/`: `request-tabular.json`, `request-image.json`,
`result-message-processing.json`, `result-message-done.json`, `result-message-error.json`, `result-osv-minimal.json`,
`result-statement-minimal.json`, `result-error.json`; скрипт `docs/scripts/validate_contracts.py` валидирует все
примеры схемами и возвращает 0
- [ ] 1.5 [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила
- [x] 1.5 [docs] `03-architecture.md`: таблица «Контракт обмена через NATS» — ссылки на три файла схем, правила
`Nats-Msg-Id`, ack через `processing`, повторная публикация; проверить, что текст ссылается на существующие файлы
## 2. Инфраструктура разработки
- [ ] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`,
- [x] 2.1 [docs] `docs/dev/docker-compose.yml`: сервисы `postgres` (16), `nats` (2.10, без `--jetstream`), `minio`,
`minio-init` (создаёт бакет `arm-ka`), healthcheck'и, именованные тома; `docs/dev/.env.example`; проверка:
`docker compose up -d` → все healthy, `mc ls` видит бакет, `curl :8222/varz` отвечает
- [ ] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus
- [x] 2.2 [docs] `docs/dev/README.md`: запуск инфраструктуры, переменные, сброс томов, запуск сервисов из venv без Nexus
(`make venv` фильтрует `raisa_*`), черновик — дописывается в 5.4
## 3. Ядро (детали — `arm-core/openspec/changes/slice-0-skeleton/tasks.md`)

View File

@ -0,0 +1,116 @@
#!/usr/bin/env python3
"""Валидация примеров контракта распознавания схемами JSON Schema 2020-12.
Запуск из любого cwd: python scripts/validate_contracts.py
Зависимости: стандартная библиотека + jsonschema.
Код возврата: 0 — всё валидно, 1 — любая ошибка.
"""
import datetime
import json
import re
import sys
from pathlib import Path
from jsonschema import Draft202012Validator, FormatChecker
ROOT = Path(__file__).resolve().parent.parent
CONTRACTS = ROOT / "contracts"
EXAMPLES = CONTRACTS / "examples"
SCHEMAS = {
"request": CONTRACTS / "recognition-request.schema.json",
"message": CONTRACTS / "recognition-result-message.schema.json",
"result": CONTRACTS / "recognition-result.schema.json",
}
# Порядок важен: result-message-* проверяется раньше result-*.
PATTERNS = [
("request-", "request"),
("result-message-", "message"),
("result-", "result"),
]
_DATE_TIME_RE = re.compile(r"^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2}:\d{2})$")
def _check_date_time(value):
# Собственная проверка вместо rfc3339-validator: нужна и дата, и время, и часовой пояс.
if not isinstance(value, str):
return True
if not _DATE_TIME_RE.match(value):
return False
datetime.datetime.fromisoformat(value.replace("z", "Z")) # ValueError -> формат не прошёл
return True
def make_format_checker():
checker = FormatChecker()
checker.checks("date-time", raises=ValueError)(_check_date_time)
return checker
def short_path(error):
path = "/".join(str(p) for p in error.absolute_path)
return f"{path or '<root>'}: {error.message[:120]}"
def main():
failed = False
validators = {}
format_checker = make_format_checker()
for kind, path in SCHEMAS.items():
if not path.is_file():
print(f"FAIL схема {path.name}: файл не найден")
failed = True
continue
try:
schema = json.loads(path.read_text(encoding="utf-8"))
Draft202012Validator.check_schema(schema)
except Exception as exc: # битый JSON или нарушение метасхемы
print(f"FAIL схема {path.name}: {str(exc).splitlines()[0][:150]}")
failed = True
continue
validators[kind] = Draft202012Validator(schema, format_checker=format_checker)
print(f"OK схема {path.name}")
counts = {kind: 0 for kind in SCHEMAS}
for example in sorted(EXAMPLES.glob("*.json")):
kind = next((k for prefix, k in PATTERNS if example.name.startswith(prefix)), None)
if kind is None:
print(f"FAIL {example.name}: имя не подходит ни под один шаблон (request-*, result-message-*, result-*)")
failed = True
continue
counts[kind] += 1
validator = validators.get(kind)
if validator is None:
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: схема недоступна")
failed = True
continue
try:
instance = json.loads(example.read_text(encoding="utf-8"))
except ValueError as exc:
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]: не JSON: {exc}")
failed = True
continue
errors = sorted(validator.iter_errors(instance), key=lambda e: list(e.absolute_path))
if errors:
failed = True
print(f"FAIL {example.name} [{SCHEMAS[kind].name}]")
for error in errors:
print(f" {short_path(error)}")
else:
print(f"OK {example.name} [{SCHEMAS[kind].name}]")
for kind, count in counts.items():
if count == 0:
print(f"FAIL нет ни одного примера для схемы {SCHEMAS[kind].name}")
failed = True
total = sum(counts.values())
print(f"Итого: примеров {total}, {'есть ошибки' if failed else 'всё валидно'}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())