Compare commits
26 Commits
a0be5ab5a1
...
5f2be96e11
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f2be96e11 | ||
|
|
5bebdfe9fc | ||
|
|
991a152454 | ||
|
|
92d642d64a | ||
|
|
0ee1d41da7 | ||
|
|
45d45e17f7 | ||
|
|
de116ed0c1 | ||
|
|
6b97d87d8e | ||
|
|
34c43be881 | ||
|
|
64c103da5f | ||
|
|
944296250e | ||
|
|
4de51361a3 | ||
|
|
a22a9c3100 | ||
|
|
3074632fb1 | ||
|
|
7f0de551c1 | ||
|
|
af0f25fbd8 | ||
|
|
38c50c2101 | ||
|
|
faae87c9c7 | ||
|
|
a57cf2db49 | ||
|
|
4be67392ab | ||
|
|
8efaba47bf | ||
|
|
1166abe42b | ||
|
|
65c3809efe | ||
|
|
8e1a1ce4f5 | ||
|
|
22f55e5f7d | ||
| 05a906ef90 |
4
.gitignore
vendored
4
.gitignore
vendored
@ -1 +1,5 @@
|
||||
.DS_Store
|
||||
.codegraph/
|
||||
.superpowers/
|
||||
.bounded
|
||||
dev/.env
|
||||
|
||||
@ -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 отвергают
|
||||
незнакомые значения.
|
||||
- Сервисы не знают про БД и про бизнес-смысл: они возвращают структуру файла; нормализация счетов, резолв
|
||||
контрагентов, проверки — в ядре.
|
||||
|
||||
|
||||
14
contracts/examples/request-image.json
Normal file
14
contracts/examples/request-image.json
Normal 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
|
||||
}
|
||||
14
contracts/examples/request-tabular.json
Normal file
14
contracts/examples/request-tabular.json
Normal 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
|
||||
}
|
||||
12
contracts/examples/result-error.json
Normal file
12
contracts/examples/result-error.json
Normal 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": []
|
||||
}
|
||||
21
contracts/examples/result-message-done.json
Normal file
21
contracts/examples/result-message-done.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
11
contracts/examples/result-message-error.json
Normal file
11
contracts/examples/result-message-error.json
Normal 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"
|
||||
}
|
||||
}
|
||||
7
contracts/examples/result-message-processing.json
Normal file
7
contracts/examples/result-message-processing.json
Normal 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" }
|
||||
}
|
||||
79
contracts/examples/result-osv-minimal.json
Normal file
79
contracts/examples/result-osv-minimal.json
Normal 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": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
50
contracts/examples/result-statement-minimal.json
Normal file
50
contracts/examples/result-statement-minimal.json
Normal 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 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
28
contracts/recognition-request.schema.json
Normal file
28
contracts/recognition-request.schema.json
Normal 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" }
|
||||
}
|
||||
}
|
||||
70
contracts/recognition-result-message.schema.json
Normal file
70
contracts/recognition-result-message.schema.json
Normal 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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -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
51
dev/.env.example
Normal 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
176
dev/README.md
Normal 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
84
dev/docker-compose.yml
Normal 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:
|
||||
45
docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md
Normal file
45
docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase1.md
Normal 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
|
||||
|
||||
45
docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md
Normal file
45
docs/superpowers/plans/2026-10-03-slice-0-skeleton-phase2.md
Normal 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` по именам файлов)
|
||||
|
||||
@ -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` порты хоста не нужны |
|
||||
|
||||
@ -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`)
|
||||
|
||||
116
scripts/validate_contracts.py
Normal file
116
scripts/validate_contracts.py
Normal 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())
|
||||
Loading…
x
Reference in New Issue
Block a user