fix(#190): I-9 is_recognized допускает null в контракте quality-facts, тесты NULL-ячейки; M-13 решения R-1..R-3 в design.md
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3f6bd96388
commit
24fbec8988
@ -52,7 +52,7 @@
|
|||||||
"string_value": { "type": ["string", "null"] },
|
"string_value": { "type": ["string", "null"] },
|
||||||
"source_text": { "type": ["string", "null"] },
|
"source_text": { "type": ["string", "null"] },
|
||||||
"confidence": { "type": ["number", "null"] },
|
"confidence": { "type": ["number", "null"] },
|
||||||
"is_recognized": { "type": "boolean" },
|
"is_recognized": { "type": ["boolean", "null"] },
|
||||||
"needs_review": { "type": "boolean" }
|
"needs_review": { "type": "boolean" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -665,6 +665,27 @@ async def test_confirm_writes_outcome_fact_as_is_when_no_fixes(
|
|||||||
assert fact.payload["fixes"] == []
|
assert fact.payload["fixes"] == []
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.fixtures
|
||||||
|
async def test_confirm_outcome_fact_with_analyst_added_cell_matches_contract(
|
||||||
|
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||||
|
) -> None:
|
||||||
|
"""I-9: ячейка, созданная правкой аналитика (`is_recognized = NULL`), попадает в payload и проходит схему."""
|
||||||
|
office = await _rostov_office(seeded)
|
||||||
|
income, _meta = await _income_form(seeded, fixtures_dir, office=office)
|
||||||
|
value = await _set_value_fix(
|
||||||
|
seeded, income, source_text="1", original_amount=Decimal("1.00"), confirmed_amount=Decimal("2.00")
|
||||||
|
)
|
||||||
|
value.is_recognized = None
|
||||||
|
await seeded.flush()
|
||||||
|
client = await auth_client(KA_RF)
|
||||||
|
|
||||||
|
response = await client.post(f"/documents/{income.id}/confirm")
|
||||||
|
|
||||||
|
assert response.status_code == HTTP_OK, response.text
|
||||||
|
fact = await _outcome_fact(seeded, income.id) # валидирует payload по контракту
|
||||||
|
assert any(item["is_recognized"] is None for item in fact.payload["values"])
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.fixtures
|
@pytest.mark.fixtures
|
||||||
async def test_confirm_writes_outcome_fact_our_error_when_fix_found_in_raw_text(
|
async def test_confirm_writes_outcome_fact_our_error_when_fix_found_in_raw_text(
|
||||||
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
seeded: AsyncSession, fixtures_dir: Path, auth_client: AuthClientFactory
|
||||||
|
|||||||
@ -14,6 +14,7 @@ from src.models.enums import (
|
|||||||
)
|
)
|
||||||
from src.models.files import Document
|
from src.models.files import Document
|
||||||
from src.models.statements import DocumentIssue, FinancialStatementLine, FinancialStatementValue
|
from src.models.statements import DocumentIssue, FinancialStatementLine, FinancialStatementValue
|
||||||
|
from tests.quality_payload_contract import assert_intake_payload_valid, assert_outcome_payload_valid
|
||||||
|
|
||||||
ROW_COUNT = 4_200
|
ROW_COUNT = 4_200
|
||||||
|
|
||||||
@ -163,3 +164,23 @@ def test_header_source_and_row_count_absent_when_kwargs_omitted():
|
|||||||
assert "row_count" not in osv
|
assert "row_count" not in osv
|
||||||
outcome_osv = build_outcome_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[], fixes=[])
|
outcome_osv = build_outcome_payload(make_document(document_type=DocumentType.OSV), osv_warnings=[], fixes=[])
|
||||||
assert "row_count" not in outcome_osv
|
assert "row_count" not in outcome_osv
|
||||||
|
|
||||||
|
|
||||||
|
def test_payload_with_null_is_recognized_matches_contract():
|
||||||
|
"""I-9: ячейка без признака распознавания (правка аналитика, пустая ячейка источника) проходит схему контракта."""
|
||||||
|
document = make_document(document_type=DocumentType.BALANCE)
|
||||||
|
line = FinancialStatementLine(
|
||||||
|
id=uuid.uuid4(),
|
||||||
|
source_ref="row:1",
|
||||||
|
row_index=1,
|
||||||
|
line_code="1100",
|
||||||
|
line_status=StatementLineStatus.ok,
|
||||||
|
needs_review=False,
|
||||||
|
)
|
||||||
|
value = FinancialStatementValue(
|
||||||
|
line_id=line.id, column_id=uuid.uuid4(), amount=Decimal("10.00"), is_recognized=None, needs_review=False
|
||||||
|
)
|
||||||
|
intake = build_intake_payload(document, lines=[line], values=[value], issues=[])
|
||||||
|
assert intake["values"][0]["is_recognized"] is None
|
||||||
|
assert_intake_payload_valid(intake)
|
||||||
|
assert_outcome_payload_valid(build_outcome_payload(document, lines=[line], values=[value], issues=[], fixes=[]))
|
||||||
|
|||||||
@ -229,7 +229,7 @@ PDF, точный вид выгрузки, разбиение на задачи.
|
|||||||
| 05.11.2026 | В план (`docs/superpowers/plans/2026-11-05-quality-metrics-arm-core.md`) добавлена строка `**Ветка:**` (`feat/190-ocr-metrics`, worktree, issue, Jira) — без неё хук `require-plan.sh` блокировал любую правку в `api/src/**`, так как не находил имя ветки ни в одном файле `docs/superpowers/plans/*.md` | во всех остальных активных планах эта строка есть (сверено на `2026-10-28-corrections-table-ui.md`); её отсутствие — пропуск на этапе написания плана, не содержательное решение | минимальный — чисто метаданные плана |
|
| 05.11.2026 | В план (`docs/superpowers/plans/2026-11-05-quality-metrics-arm-core.md`) добавлена строка `**Ветка:**` (`feat/190-ocr-metrics`, worktree, issue, Jira) — без неё хук `require-plan.sh` блокировал любую правку в `api/src/**`, так как не находил имя ветки ни в одном файле `docs/superpowers/plans/*.md` | во всех остальных активных планах эта строка есть (сверено на `2026-10-28-corrections-table-ui.md`); её отсутствие — пропуск на этапе написания плана, не содержательное решение | минимальный — чисто метаданные плана |
|
||||||
| 06.11.2026 | Task 13: `web/src/api/types.ts` — `'QUALITY_METRICS'` дописан в конец union `PermissionObject` ОДНОЙ строкой (не многострочным форматом), итоговая строка ровно 140 символов — на пределе, но не сверх лимита ширины biome | сохраняет однострочный стиль, которым уже написана эта строка (и соседняя `PermissionAction`); многострочный вариант рассматривался как равноценная по типам альтернатива, выбор между ними непринципиален | нулевая — чисто форматирование, обе формы типобезопасно идентичны |
|
| 06.11.2026 | Task 13: `web/src/api/types.ts` — `'QUALITY_METRICS'` дописан в конец union `PermissionObject` ОДНОЙ строкой (не многострочным форматом), итоговая строка ровно 140 символов — на пределе, но не сверх лимита ширины biome | сохраняет однострочный стиль, которым уже написана эта строка (и соседняя `PermissionAction`); многострочный вариант рассматривался как равноценная по типам альтернатива, выбор между ними непринципиален | нулевая — чисто форматирование, обе формы типобезопасно идентичны |
|
||||||
| 05.11.2026 | Коммит задачи 1 включает больше файлов, чем перечислено в брифе (`Makefile`, `README.md`, `api/.env.example`, `api/.env.test`, `api/alembic/env.py`, `api/scripts/check_dbml_vs_orm.py`, `api/scripts/dbml_parity.py`, `api/src/models/base.py`, `api/src/models/__init__.py`, восемь файлов `api/tests/test_*.py`) | все они требуют правки для согласованности версии v5.25 (имя DBML-файла в `Makefile`/README, `HEAD`-ревизия и допущение «всё в dkb» в тестах) или для поддержки второй схемы; список `git add` в брифе соответствовал более узкому ожидаемому дифу до того, как выяснилось, что `check_dbml_vs_orm.py` и тесты миграций не знают о второй схеме | коммит по списку брифа оставил бы `make test`/`make lint`/`make dbml-check` красными — хуже, чем более широкий, но согласованный коммит |
|
| 05.11.2026 | Коммит задачи 1 включает больше файлов, чем перечислено в брифе (`Makefile`, `README.md`, `api/.env.example`, `api/.env.test`, `api/alembic/env.py`, `api/scripts/check_dbml_vs_orm.py`, `api/scripts/dbml_parity.py`, `api/src/models/base.py`, `api/src/models/__init__.py`, восемь файлов `api/tests/test_*.py`) | все они требуют правки для согласованности версии v5.25 (имя DBML-файла в `Makefile`/README, `HEAD`-ревизия и допущение «всё в dkb» в тестах) или для поддержки второй схемы; список `git add` в брифе соответствовал более узкому ожидаемому дифу до того, как выяснилось, что `check_dbml_vs_orm.py` и тесты миграций не знают о второй схеме | коммит по списку брифа оставил бы `make test`/`make lint`/`make dbml-check` красными — хуже, чем более широкий, но согласованный коммит |
|
||||||
| 05.11.2026 | `api/contracts/quality-facts.schema.json` НЕ скопирован из `../docs/contracts/` в Task 1 (`test_contract.py::test_vendored_contracts_are_identical_to_docs` остаётся красным на этом файле) — копирование отложено на задачу, которая реально строит `payload` по этой схеме | файл есть в `../docs/contracts/metrics/` (сиблинг-репозиторий arm-ka-docs, ветка feat/202, PR docs #203 — НЕ слита; порядок merge: docs PR #203 сливается раньше PR arm-core; позже файл вендорен — R-16), а в `api/contracts/` копии нет и несовпадение чисто механическое; копия сейчас была бы мёртвой — Task 1 (схема/ORM/миграция) не читает и не пишет `payload`, валидация полезной нагрузки — логика, не схема БД, и не входит в её рамки | следующая задача может не заметить, что это отложено осознанно, и либо повторно принять то же решение не зная о нём, либо оставить контракт невендоренным навсегда |
|
| 05.11.2026 | `api/contracts/quality-facts.schema.json` НЕ скопирован из `../docs/contracts/` в Task 1 (`test_contract.py::test_vendored_contracts_are_identical_to_docs` остаётся красным на этом файле) — копирование отложено на задачу, которая реально строит `payload` по этой схеме | файл есть в `../docs/contracts/metrics/` (сиблинг-репозиторий arm-ka-docs, ветка feat/202, PR docs #203 — НЕ слита; порядок merge: docs PR #203 сливается раньше PR arm-core; позже файл вендорен — R-16), а в `api/contracts/` копии нет и несовпадение чисто механическое; копия сейчас была бы мёртвой — Task 1 (схема/ORM/миграция) не читает и не пишет `payload`, валидация полезной нагрузки — логика, не схема БД, и не входит в её рамки | следующая задача может не заметить, что это отложено осознанно, и либо повторно принять то же решение не зная о нём, либо оставить контракт невендоренным навсегда Перекрыто R-16 (07.11.2026): контракт вендорен, `test_contract` зелёный. |
|
||||||
| 06.11.2026 | Task 7 (`CONFIRMED`): нет сырого текста для сравнения у правки суммы/кода/шапки (`source_text`/`line_code_text`/`header_source[field].text` = `None`) — атрибуция «наш разбор» (`ours`), не пропуск правки. Уточняет направление решения 27.10.2026 «Спорное — в наш разбор» на случай «данных для сравнения нет вообще» — это НЕ тот же случай, что пример `1250\n1260` в исходном решении (там текст есть, но двусмысленный); `attribute_*` (Task 3) типизированы на `str`, случай отсутствия текста не предусматривают. Этим же решением подтверждается (не меняется) уже принятое 27.10.2026 направление для добавленной строки PDF (строка «Код добавленной строки ищется среди четырёхзначных токенов…», та же дата) — черновая реализация задачи 7 свела этот случай к вызову `attribute_added_row(code, raw_line_codes=(), is_excel=False)`, который из-за пустого `raw_line_codes` всегда возвращает «сервис», что противоречило уже принятому там направлению «совпадение с суммой даёт наш разбор»; исправлено прямой атрибуцией «наш разбор» для PDF-добавленных строк без вызова `attribute_added_row` (сырых токенов всё равно нет, инфраструктура их извлечения не построена — тот же пробел, что в строке 27.10.2026) | анти-цель §2/§5 («вина сервиса там, где ошиблись мы; спорные случаи — в наш разбор, не в сервис») распространяется и на случай, когда данных для сравнения нет вовсе: отсутствие доказательства ошибки сервиса не есть доказательство ошибки сервиса | если отсутствие сырого текста окажется частым, а не редким краевым случаем, `OUR_ERROR` может быть систематически завышен, а `SERVICE_ERROR` — недосчитан; метрика ослабнет как рычаг давления на поставщика распознавания (прямая цель §1), хотя остаётся безопасным дефолтом по анти-цели §2 |
|
| 06.11.2026 | Task 7 (`CONFIRMED`): нет сырого текста для сравнения у правки суммы/кода/шапки (`source_text`/`line_code_text`/`header_source[field].text` = `None`) — атрибуция «наш разбор» (`ours`), не пропуск правки. Уточняет направление решения 27.10.2026 «Спорное — в наш разбор» на случай «данных для сравнения нет вообще» — это НЕ тот же случай, что пример `1250\n1260` в исходном решении (там текст есть, но двусмысленный); `attribute_*` (Task 3) типизированы на `str`, случай отсутствия текста не предусматривают. Этим же решением подтверждается (не меняется) уже принятое 27.10.2026 направление для добавленной строки PDF (строка «Код добавленной строки ищется среди четырёхзначных токенов…», та же дата) — черновая реализация задачи 7 свела этот случай к вызову `attribute_added_row(code, raw_line_codes=(), is_excel=False)`, который из-за пустого `raw_line_codes` всегда возвращает «сервис», что противоречило уже принятому там направлению «совпадение с суммой даёт наш разбор»; исправлено прямой атрибуцией «наш разбор» для PDF-добавленных строк без вызова `attribute_added_row` (сырых токенов всё равно нет, инфраструктура их извлечения не построена — тот же пробел, что в строке 27.10.2026) | анти-цель §2/§5 («вина сервиса там, где ошиблись мы; спорные случаи — в наш разбор, не в сервис») распространяется и на случай, когда данных для сравнения нет вовсе: отсутствие доказательства ошибки сервиса не есть доказательство ошибки сервиса | если отсутствие сырого текста окажется частым, а не редким краевым случаем, `OUR_ERROR` может быть систематически завышен, а `SERVICE_ERROR` — недосчитан; метрика ослабнет как рычаг давления на поставщика распознавания (прямая цель §1), хотя остаётся безопасным дефолтом по анти-цели §2 |
|
||||||
| 06.11.2026 | Task 8 (`DELETED`/`DELETED_AFTER_CONFIRM`): (1) документ в статусе, отличном от `CONFIRMED`/`RECOGNIZED`/`NEEDS_CHECK` (практически только `ERROR`), при удалении файла НЕ получает факт исхода — у него никогда не было содержательного состояния «как распознано» для фиксации (нет формы/строк, не проходил ни через `AUTO_CONFIRMED`, ни через intake-факт); (2) `issue_count_at_outcome` вычисляется как `counts.error + counts.info` из уже посчитанного агрегата `issue_counts_by_document` вместо отдельного запроса — тот же смысл «все нарушения документа без фильтра», что `len(issues)` в Task 6/7, дешевле на один запрос; (3) `payload` для DELETED*-фактов — единый минимальный вызов `build_outcome_payload(document, fixes=())` без дополнительных запросов к строкам/значениям формы или ОСВ (эти строки всё равно будут физически удалены этим же вызовом `delete`, повторный их снимок здесь экономически не оправдан — а в матрицу §6 эти факты и не попадают, доступны только как «удалён/заменён», drill-down уже видит прежний исход через более ранний intake/outcome факт того же `document_id`) | бриф задачи 8 явно делегировал все три пункта исполнителю («если неоднозначно — зафиксировать выбор, не угадывать»); исполнитель выбрал и задокументировал в докстрингах кода, перенесено сюда контроллером по правилу маркировки находок (решение агента без обсуждения с человеком — в design.md) | (1): при появлении у `ERROR`-документов содержательного состояния до удаления (гипотетически) — нужно будет расширить список статусов; (2)/(3): минимальный риск — агрегаты эквивалентны, а payload не используется для атрибуции (там `primary_cause=None`, вне матрицы) |
|
| 06.11.2026 | Task 8 (`DELETED`/`DELETED_AFTER_CONFIRM`): (1) документ в статусе, отличном от `CONFIRMED`/`RECOGNIZED`/`NEEDS_CHECK` (практически только `ERROR`), при удалении файла НЕ получает факт исхода — у него никогда не было содержательного состояния «как распознано» для фиксации (нет формы/строк, не проходил ни через `AUTO_CONFIRMED`, ни через intake-факт); (2) `issue_count_at_outcome` вычисляется как `counts.error + counts.info` из уже посчитанного агрегата `issue_counts_by_document` вместо отдельного запроса — тот же смысл «все нарушения документа без фильтра», что `len(issues)` в Task 6/7, дешевле на один запрос; (3) `payload` для DELETED*-фактов — единый минимальный вызов `build_outcome_payload(document, fixes=())` без дополнительных запросов к строкам/значениям формы или ОСВ (эти строки всё равно будут физически удалены этим же вызовом `delete`, повторный их снимок здесь экономически не оправдан — а в матрицу §6 эти факты и не попадают, доступны только как «удалён/заменён», drill-down уже видит прежний исход через более ранний intake/outcome факт того же `document_id`) | бриф задачи 8 явно делегировал все три пункта исполнителю («если неоднозначно — зафиксировать выбор, не угадывать»); исполнитель выбрал и задокументировал в докстрингах кода, перенесено сюда контроллером по правилу маркировки находок (решение агента без обсуждения с человеком — в design.md) | (1): при появлении у `ERROR`-документов содержательного состояния до удаления (гипотетически) — нужно будет расширить список статусов; (2)/(3): минимальный риск — агрегаты эквивалентны, а payload не используется для атрибуции (там `primary_cause=None`, вне матрицы) |
|
||||||
| 06.11.2026 | Task 10 (`quality_failure_fact`): `error_code` для отказов маппинга (`_fail`, `_register_failure`) — константа `MAPPING_FAILURE_ERROR_CODE = "MAPPING_FAILED"` (`result_mapping.py`), не свободный текст из `message`/описания исключения | `QualityFailureFact.error_code` не имеет свободнотекстового столбца-соседа для деталей отказа маппинга (в отличие от `recognition_job.error_message`); деталь остаётся в `metrics.mapping_error`/логе, код — единая константа, отличимая от кодов обмена (`ERROR_NO_ACK`/`ERROR_INVALID_REQUEST`), что проверяет отдельный тест | если деталь отказа маппинга понадобится в матрице причин без обращения к логам — нужен отдельный столбец/payload-поле, не замена константы |
|
| 06.11.2026 | Task 10 (`quality_failure_fact`): `error_code` для отказов маппинга (`_fail`, `_register_failure`) — константа `MAPPING_FAILURE_ERROR_CODE = "MAPPING_FAILED"` (`result_mapping.py`), не свободный текст из `message`/описания исключения | `QualityFailureFact.error_code` не имеет свободнотекстового столбца-соседа для деталей отказа маппинга (в отличие от `recognition_job.error_message`); деталь остаётся в `metrics.mapping_error`/логе, код — единая константа, отличимая от кодов обмена (`ERROR_NO_ACK`/`ERROR_INVALID_REQUEST`), что проверяет отдельный тест | если деталь отказа маппинга понадобится в матрице причин без обращения к логам — нужен отдельный столбец/payload-поле, не замена константы |
|
||||||
@ -265,3 +265,7 @@ PDF, точный вид выгрузки, разбиение на задачи.
|
|||||||
| 06.11.2026 | R-7 (Task 7, сведён I-2): несоответствие формы `payload.fixes`/`values` контракту `quality-facts.schema.json` было отложено и вынесено в финальное ревью | ни одна задача плана не владела приведением payload к контракту, контракт не был вендорен, тестов валидации не было | закрыто в I-2: R-16…R-21 (вендоринг, приведение payload, тест валидации) |
|
| 06.11.2026 | R-7 (Task 7, сведён I-2): несоответствие формы `payload.fixes`/`values` контракту `quality-facts.schema.json` было отложено и вынесено в финальное ревью | ни одна задача плана не владела приведением payload к контракту, контракт не был вендорен, тестов валидации не было | закрыто в I-2: R-16…R-21 (вендоринг, приведение payload, тест валидации) |
|
||||||
| 07.11.2026 | R-22 (final review M-7): условие §14.4 «нарушенное соотношение не задето правками» НЕ проверяется: `checks_accepted = meta.checks_accepted_by is not None` (`documents._write_confirm_outcome_fact`), то есть любой акцепт нарушений при ручном подтверждении | на ячейку матрицы не влияет: при любой правке (`service`/`ours`) приоритет §6 п.2-3 выше «документа клиента», а без правок задетое правками соотношение невозможно; влияет только на поле `checks_accepted` в факте и на «документ клиента» у документа без правок — где условие выполняется тривиально. Точная проверка требует пересчёта соотношений до/после правок | если акцепт дан, а часть соотношений исправлена правками, поле `checks_accepted` у такого факта завышено; ячейка остаётся «сервис»/«наш разбор» |
|
| 07.11.2026 | R-22 (final review M-7): условие §14.4 «нарушенное соотношение не задето правками» НЕ проверяется: `checks_accepted = meta.checks_accepted_by is not None` (`documents._write_confirm_outcome_fact`), то есть любой акцепт нарушений при ручном подтверждении | на ячейку матрицы не влияет: при любой правке (`service`/`ours`) приоритет §6 п.2-3 выше «документа клиента», а без правок задетое правками соотношение невозможно; влияет только на поле `checks_accepted` в факте и на «документ клиента» у документа без правок — где условие выполняется тривиально. Точная проверка требует пересчёта соотношений до/после правок | если акцепт дан, а часть соотношений исправлена правками, поле `checks_accepted` у такого факта завышено; ячейка остаётся «сервис»/«наш разбор» |
|
||||||
| 07.11.2026 | R-23 (final review I-7): дельты OpenSpec — ADDED `quality-metrics`, `quality-metrics-ui`; MODIFIED `primary-files` (загрузка, удаление), `recognition-job-exchange` (inbox, таймаут), `documents-api` (подтверждение), `osv-result-mapping` (цикл маппинга, автоподтверждение), `authn-authz` (`GET /me`), `reference-data-seed` (seed, 16 → 17 прав) | `openspec validate --strict` требует полного текста изменяемого требования, поэтому MODIFIED копируют текущее требование из `openspec/specs/` с добавлением; авторизационная спека не перечисляет права, поэтому право отражено сценарием `/me` | при архивации дельты заменят требования целиком — правки спек основной ветки, сделанные параллельно, потеряются (проверить конфликт перед `openspec archive`) |
|
| 07.11.2026 | R-23 (final review I-7): дельты OpenSpec — ADDED `quality-metrics`, `quality-metrics-ui`; MODIFIED `primary-files` (загрузка, удаление), `recognition-job-exchange` (inbox, таймаут), `documents-api` (подтверждение), `osv-result-mapping` (цикл маппинга, автоподтверждение), `authn-authz` (`GET /me`), `reference-data-seed` (seed, 16 → 17 прав) | `openspec validate --strict` требует полного текста изменяемого требования, поэтому MODIFIED копируют текущее требование из `openspec/specs/` с добавлением; авторизационная спека не перечисляет права, поэтому право отражено сценарием `/me` | при архивации дельты заменят требования целиком — правки спек основной ветки, сделанные параллельно, потеряются (проверить конфликт перед `openspec archive`) |
|
||||||
|
| 05.11.2026 | R-1 (Task 3): имена экспорта `arm_statements` в плане (`fix_lookalikes`/`parse_amount`/`parse_code`/`parse_date`/`default_rules`) предварительные; исполнитель задачи 3 сверяет их с реальным `dir(arm_statements)` до написания `domain/quality_attribution.py` и фиксирует расхождения строкой в ledger | это собственная инструкция плана, в рулинг вынесена, чтобы попала в бриф задачи | если имена не совпали, а шаг пропущен — `quality_attribution.py` не импортируется, ловится красным тестом сразу, правка дешёвая |
|
||||||
|
| 06.11.2026 | R-2 (Task 11): регистрация сырого объекта в `recognition_exchange._apply_result` не зависит от `message.status`: срабатывает при `message.raw_s3_key is not None`, независимо от проверки `status is ERROR` для факта отказа (Task 10); оба хука живут в одной функции на разных условиях | план задачи 11 уже говорит «если `message.raw_s3_key is not None`» без условия по статусу; design §9: сырой ответ нужен эталонному прогону при любом исходе | если регистрация должна быть условной, упавшее задание получит лишнюю строку `s3_object` на полезный сырой ответ — дёшево и, скорее всего, правильно |
|
||||||
|
| 06.11.2026 | R-3 (Task 2): сигнатуры `build_intake_payload` / `build_outcome_payload` расширены необязательными `header_source: Mapping \| None` (кладётся в `header` для ББ/ОФР) и `osv_row_count: int \| None` (ключ `row_count` в ветке ОСВ) | исполнитель задачи 2 заметил, что бриф «Consumes» называет `FinancialStatementMeta`, а сигнатура в «Produces» параметра для него не давала; берётся существующее поле ORM `header_source`, новой формы не изобретали; параметры необязательные — прежние тесты не менялись | если реальному хуку задачи 5 нужна иная форма, сигнатуру правят позже — локальная и дешёвая правка |
|
||||||
|
| 07.11.2026 | R-24 (повторное ревью I-9): в схеме `quality-facts.schema.json` поле `intake-payload/values/items/is_recognized` — `["boolean", "null"]` (docs, пока PR #203 не слит; копия `api/contracts/metrics/` байт-в-байт). Остальные поля сверены скриптом по моделям: NULL допускают только `is_recognized` (правка аналитика `add_value`, пустая ячейка источника при приёме) и уже допускающие null поля | `FinancialStatementValue.is_recognized` — `bool \| None`; NULL означает «не из источника», а не «не распознана» (P3-54), поэтому тип поля менять не стали, а ослабили контракт; тесты: unit-валидация NULL-ячейки и `CONFIRMED` с ячейкой аналитика | потребитель фактов обязан обрабатывать `null` в `is_recognized`; ужесточение схемы позже потребует миграции данных |
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user