diff --git a/openspec/changes/slice-0-skeleton/open-questions-proposal.md b/openspec/changes/slice-0-skeleton/open-questions-proposal.md new file mode 100644 index 0000000..9650979 --- /dev/null +++ b/openspec/changes/slice-0-skeleton/open-questions-proposal.md @@ -0,0 +1,64 @@ +> Назначение: предложения для `docs/context/07-open-questions.md` и DBML v5.14; docs-PR открывает владелец `../docs`. +> Дата: 2026-10-04 (срез 0, ветка `feat/1-slice-0-skeleton`; файл архивируется вместе с change `slice-0-skeleton`, рулинг R-26). +> Номера `OQ-??` присваиваются там, в реестре `07-open-questions.md` (подряд с OQ-48), а не здесь. + +# Предложения для `../docs/context/07-open-questions.md` (Срез 0, arm-core) — для PR в docs (R-8) + +Формат строк — как в таблице `07-open-questions.md`: `| ID | Вопрос | Откуда | Влияет на | Статус / допущение |`. +Занятые номера: OQ-01…OQ-47 (последний — OQ-47). Новые строки ниже помечены `OQ-?? (предл. N)`; при переносе — +нумеровать подряд с OQ-48 в порядке предложений. Ничего в `../docs` не правилось. + +## A. Правки существующих строк + +| ID | Вопрос | Откуда | Влияет на | Статус / допущение | +|---|---|---|---|---| +| OQ-46 (правка статуса) | Обязательны ли все ОСВ расширенного подхода (02, 03, 08, 44, 26, 91, 51, 52, 59, 80, 83, 86, 97, 001, 008, 009, 011) при `approach = EXTENDED`, или часть — «при наличии»? | ТЗ 5.1; `required_document_rule.is_mandatory` | срез 0 (seed), срез 3 (чек-лист) | **допущение принято и применено**: все 27 строк EXTENDED в seed (`api/seed/core/required_document_rules.yaml`, маркер `ASSUMPTION(OQ-46)`) — `is_mandatory = true`; смена — правка одного yaml; BASIC — 10 строк, все обязательные | +| OQ-47 (дополнение) | Источник полного плана счетов 1С для seed `chart_of_account`: в фикстурах файла нет, «план счетов из бандла макета» не передан. **Дополнение**: названия нестандартных субсчетов (66/67 `.06/.22/.24/.26`, 76.07.x, 76.08/.28/.38) в seed выведены по их роли в §4 правил, а не взяты из справочника 1С | `06-test-data.md` §6, `fixtures/README.md`; `api/seed/core/chart_of_accounts.csv` | срез 0 (seed), срез 1 (справочник счетов) | допущение: План счетов 94н (01–99, 001–011) + типовые субсчета 1С БП 3.0, упомянутые в §4/§6 (122 строки: 62 синтетических 94н, 11 забалансовых, 49 субсчетов/подсубсчетов); названия-заглушки заменить по справочнику 1С клиента; остальное добавляет администратор (ARCH-004) | + +## B. Новые вопросы + +| ID | Вопрос | Откуда | Влияет на | Статус / допущение | +|---|---|---|---|---| +| OQ-?? (предл. 1) | Правило элиминации № 1 (§4), сторона Б «76.07.1 − 76.07.5 − 76.07.9»: 76.07.5 и 76.07.9 не потомки 76.07.1, поэтому «вычитание» чисто информативно. Так и задумано? | `05-domain-rules.md` §4 (правило 1); seed `elimination_rules_v1.yaml` (T14.1) | срез 5 (пары ВГО) | допущение: исключения 76.07.5/76.07.9 на стороне Б хранятся как `is_excluded` и не влияют на сумму (76.07.1 их не содержит); алгоритм на них не опирается; подтвердить с методологами | +| OQ-?? (предл. 2) | Правило № 1, сторона А: резервный счёт 01 лежит в одном списке с 03 и 76.07.9 — приоритета нет. Как выбирать «03, а если у А нет ОСВ 03 — 01»? Что значит «результат в строку ППА» (см. OQ-11)? | §4 (правило 1: «01 — резервный»); seed (T14.2) | срез 5–6 | допущение: в seed `accounts: ["03", "76.07.9", "01"]` без порядка; fallback «берём 01, если нет ОСВ 03» — в `comment` правила; логику приоритета реализует срез 5, до решения — не реализуется | +| OQ-?? (предл. 3) | Составные коды статей КФО `1450/1550` (правило 1), `1310/1350` (правило 7, гудвилл) хранятся в `elimination_rule.article_2_line` одной строкой с «/». Как их потреблять: делить по «/» и подбирать ту строку, что есть в ББ Б, или хранить две строки? | §4 (правила 1, 7); seed (T14.3) | срез 5–6; DBML (note колонки) | допущение: хранится как в §4, потребитель делит по «/»; предложение — зафиксировать это в note `article_2_line` в DBML v5.14 | +| OQ-?? (предл. 4) | `required_document_rule`: уникальный индекс `(approach, document_type, account_id)` при nullable `account_id` допускает дубли строк для не-ОСВ документов (NULL в PostgreSQL различны). Добавить `NULLS NOT DISTINCT`? | DBML v5.13 (строка `Indexes { (approach, document_type, account_id) [unique] }`); ORM следует DBML буквально (T8.3) | DBML v5.14, срез 3 (чек-лист) | допущение: ORM/миграция 1:1 с DBML (без `NULLS NOT DISTINCT`); предложение v5.14: `[unique, note: 'NULLS NOT DISTINCT']`, как у `document` | +| OQ-?? (предл. 5) | Контракт v1.0, `recognition-result-message`: `schema_version` принимает только `1.x` (`^1\.\d+$`), а `result_schema_version` — любой MAJOR (`^\d+\.\d+$`). Намеренно (файл результата версионируется отдельно) или небрежность? | `docs/contracts/recognition-result-message.schema.json` (T20.7) | контракт v1.x, срез 2 (inbox/маппинг) | допущение: ядро повторяет схему буквально (любой MAJOR проходит валидацию); проверку совместимости MAJOR результата делает маппинг Среза 2; уточнить у владельца контракта | +| OQ-?? (предл. 6) | Контракт v1.0: схемы нигде не задают `additionalProperties: false` (кроме `columns` ОСВ). Считать ли неизвестные поля допустимыми расширениями? | `docs/contracts/*.schema.json` (T20) | контракт v1.x, все три сервиса | допущение: модели ядра `extra = "ignore"` (строже схемы быть нельзя — иначе ядро отвергнет совместимого пира); изменение — через MINOR контракта | +| OQ-?? (предл. 7) | Параметры JWT РАИСА неизвестны: JWKS URL / публичный ключ, `iss`, `aud`, имя claim с email. Что в продуктивном контуре? Сделать проверку `aud`/`iss` обязательной в проде? | ТЗ 8.1, `03-architecture.md` (SSO РАИСА); `core/auth.py` (T16.2) | срез 1+ (прод-конфигурация) | допущение: настраиваются `AUTH_JWKS_URL`/`AUTH_PUBLIC_KEY` (взаимоисключающие), `AUTH_ISSUER`, `AUTH_AUDIENCE`, `AUTH_EMAIL_CLAIM` (по умолчанию `email`); при пустых `AUTH_AUDIENCE`/`AUTH_ISSUER` `aud`/`iss` не проверяются; предложение: в проде сделать `aud` обязательным и предупреждать при старте, если пусто | +| OQ-?? (предл. 8) | В enum `audit_action` нет действия для смены статуса задания распознавания (`recognition_job`). Добавлять ли (DBML v5.14) и нужен ли аудит обмена вообще? | DBML v5.13 `audit_action`; решение R-7 (design #20) | DBML v5.14, срез 2 (обмен) | допущение: Срез 0 не пишет `audit_event` из обмена `recognition_job`; сервис аудита (`services/audit.py`) готов и покрыт тестами; если нужно — действие в v5.14 и вызовы в `recognition_exchange.py` | +| OQ-?? (предл. 9) | Append-only аудит обеспечен только на уровне кода (единственная точка записи, статический тест на UPDATE/DELETE по `AuditEvent`). Нужна ли защита на уровне БД (триггер или `REVOKE UPDATE, DELETE` для роли приложения)? | инвариант «Append-only аудит» (`../CLAUDE.md`); `services/audit.py` (задача 21) | DBML/миграции, платформа (права БД) | допущение: только код + тесты; решение по триггеру/правам — с командой платформы до прода | +| OQ-?? (предл. 10) | Счётчик повторных публикаций запроса (`ACK_TIMEOUT_SECONDS` × `OUTBOX_MAX_REPUBLISH`) хранится в `recognition_job.metrics.republish_count` (jsonb), колонки в DBML нет. Нужна ли колонка (v5.14)? | `services/recognition_exchange.py` (design #6, T23) | DBML v5.14, срез replay | допущение: в `metrics.republish_count`; `attempt` не растёт при повторе; при будущем replay счётчик и `reply_deadline` надо сбрасывать (T23.1) | +| OQ-?? (предл. 11) | Семантика сроков и ошибок обмена: (а) `reply_deadline` по умолчанию = `now + 5 мин` для обоих pipeline (`tabular`, `image`), если у задания NULL; (б) при невозможности собрать запрос по контракту задание уходит в `error` / `INVALID_REQUEST`; (в) при `ACK_TIMEOUT × (MAX_REPUBLISH+1) ≥ 5 мин` статус `dlq` недостижим (раньше сработает `timeout`). Верно ли? | DBML `reply_deadline` note («OCR — 5 мин»); `recognition_exchange.py` (T23.4) | срез 2–3, настройка таймаутов | допущение: как описано (дефолты 30 с × 3 укладываются в 5 мин); сроки по pipeline уточнить с командами парсера и шлюза | +| OQ-?? (предл. 12) | Email пользователя сопоставляется с `app_user.email` по `lower(email)`; при двух записях, различающихся регистром, — 403 (fail-closed). Добавить `CHECK`/уникальный индекс по `lower(email)` в DBML? | `core/auth.py`, `api/dependencies/auth.py` (R-17, design #29) | DBML v5.14, срез 1 (админ-экран) | допущение: уникальность `email` — регистрозависимая (как в DBML); регистронезависимая уникальность — на v5.14 | +| OQ-?? (предл. 13) | Seed `--dev` не защищён флагом `DEBUG`: создаёт dev-пользователей (в т.ч. `ADMIN`) на любой БД, на которую указывает `.env`, и перезаписывает существующего `app_user` с совпадающим email. Нужна ли защита? | `scripts/seed.py` (T15.2, T15.4) | срез 0 (процесс), прод-развёртывание | допущение: спека не требует защиты, `--dev` помечен «только для разработки»; предложение — отказ при `DEBUG=false` или явный флаг подтверждения | +| OQ-?? (предл. 14) | Неверный метод на существующий API-маршрут (например `POST /api/v1/me`) отдаёт 404 вместо 405: SPA-монтирование на `/` перехватывает частичное совпадение, а `/api/*` в нём принудительно 404. Приемлемо? | `app.py` (`SPAStaticFiles`, T18.2) | API-контракт (клиенты, фронт) | допущение: оставлено как есть (честный 404 для несуществующих путей важнее 405); при необходимости — отдельная правка | +| OQ-?? (предл. 15) | Решение по `web/package-lock.json` и уязвимостям: шаблон lock не содержит, `raisa-*` из Nexus — lock вне банка отличался бы; в Срезе 0 lock в `.gitignore` (R-23, design #35). `npm install` сообщает 6 high (npm audit) в зависимостях шаблона web. Коммитить ли lock; кто обновляет зависимости? | `web/` (T25.1, `task-26-report.md` C1) | платформа/фронт, до прода | допущение: lock не в репозитории, зависимости не менялись; решение — с платформенной командой (Nexus, политика audit) | + +## B2. Новые вопросы из финального ревью ветки (2026-10-04) + +| ID | Вопрос | Откуда | Влияет на | Статус / допущение | +|---|---|---|---|---| +| OQ-?? (предл. 16) | Аудит `LOGIN` / `LOGIN_FAILED` (действия есть в `audit_action`) для JWT-аутентификации: в каком срезе и что считать «входом» — первый запрос с токеном, каждый запрос, отказ проверки токена? | DBML `audit_action`; `core/auth.py`, `api/dependencies/auth.py` (финальное ревью) | срез 1 (вход, админ-экран), аудит | допущение: в Срезе 0 вход не аудируется; сервис аудита готов | +| OQ-?? (предл. 17) | Шаблонные CORS `allow_origins=["*"]` + `allow_credentials=True` и `ProxyHeadersMiddleware(trusted_hosts=["*"])`: в проде любой клиент может подставить `X-Forwarded-For`, и `audit_event.ip_address` недостоверен. Какие origins и доверенные прокси на платформе? | `app.py` (шаблон команды); финальное ревью | прод-конфигурация, достоверность аудита | допущение: оставлено как в шаблоне; перечни origins и доверенных прокси — с платформенной командой до прода | +| OQ-?? (предл. 18) | `/docs` и `/openapi.json` FastAPI открыты без аутентификации, в том числе в проде. Закрывать их (выключать при `DEBUG=false`) или оставить? | `app.py` (шаблон); финальное ревью | прод-конфигурация, безопасность | допущение: открыты, как в шаблоне; решение — с платформой/ИБ | +| OQ-?? (предл. 19) | Inbox не обновляет `stored_file.status` (`UPLOADED → PROCESSING → PROCESSED / ERROR`) и `stored_file.current_recognition_job_id` по результату задания. Кто и когда это делает в Срезе 2 — inbox или маппер, в той же транзакции? | DBML `stored_file`, `file_status`; `recognition_exchange.py` (финальное ревью) | срез 2 (загрузка и маппинг) | допущение: в Срезе 0 файл остаётся `UPLOADED`; решение — при проектировании маппера Среза 2 | +| OQ-?? (предл. 11, дополнение) | Срок ответа по умолчанию 5 мин для `tabular` взят по аналогии с OCR: в DBML значение есть только для OCR («OCR — 5 мин»), для парсера — нет. Какой срок у `tabular-parser`? | DBML `recognition_job.reply_deadline` note; `DEFAULT_REPLY_TIMEOUT` в `recognition_exchange.py` (финальное ревью) | срез 2, настройка таймаутов | допущение: 5 мин для обоих pipeline (см. предл. 11) | +| OQ-?? (предл. 9, дополнение) | Append-only аудита на уровне БД — финальное ревью повторно подняло вопрос; см. предл. 9 (триггер или `REVOKE UPDATE, DELETE` для роли приложения) | инвариант «Append-only аудит»; финальное ревью | миграции, права БД | допущение: как в предл. 9 — только код + тесты | +| OQ-?? (предл. 20) | Обработчик `recognition.result` выполняется последовательно в callback подписки NATS; длинный маппер Среза 2 задержит следующие сообщения, ack сервиса не успеет за `ACK_TIMEOUT_SECONDS` (30 с) — ложный republish, затем `dlq`. Предложение: развести быстрый inbox (только фиксация сообщения и статуса) и отдельный цикл маппинга результата | `core/nats.py` (`subscribe_results`), `recognition_exchange.py` (`handle_message`, `ResultMapper`); финальное ревью | срез 2 (маппинг), архитектура обмена (D-6) | допущение: в Срезе 0 `NoopMapper` — проблема не проявляется; решение — до реализации маппера Среза 2 | +| OQ-?? (предл. 21) | Перед чтением файла результата маппер должен проверять, что ключ начинается с `results/{file_id}/{job_id}/`, а бакет — бакет роли `results` (иначе сервис может «подсунуть» чужой объект). Закрепить в контракте и `03-architecture.md`? | контракт `recognition-result-message` (`result_s3`); `_register_result_object` в `recognition_exchange.py`; финальное ревью | срез 2 (маппинг), контракт v1.x | допущение: в Срезе 0 результат только регистрируется в `s3_object`, не читается; проверку реализует маппер Среза 2 | + +## C. Кандидаты в DBML v5.14 (сводка из вопросов выше) + +- `required_document_rule`: уникальный индекс `(approach, document_type, account_id)` — `NULLS NOT DISTINCT` (предл. 4). +- `audit_action`: действие для смены статуса `recognition_job` (предл. 8). +- `recognition_job`: колонка счётчика повторных публикаций вместо `metrics.republish_count` (предл. 10). +- `app_user`: уникальность по `lower(email)` (предл. 12). +- `elimination_rule.article_2_line`: note о составных кодах «1450/1550», «1310/1350» (предл. 3). +- OQ-45 (уже в реестре): `NU` в `osv_data_type` — Срез 0 не затрагивает. + +## D. Заметки для спек/контрактов (не OQ) + +- `contracts/recognition-result-message.schema.json`: `result_schema_version` vs `schema_version` (предл. 5), нет `additionalProperties: false` (предл. 6). +- `03-architecture.md` / контракт: ключ файла результата — только под `results/{file_id}/{job_id}/` в бакете роли `results`, проверка до чтения (предл. 21). +- `docs/dev/` (compose): тома общие для всех worktree, имя проекта `arm-ka`.