Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BJKMLHdxMwSUeYcdkwqX4W
178 lines
7.4 KiB
Python
178 lines
7.4 KiB
Python
"""Шаг 8 — подтверждение неуверенного кода структурой бланка и выбор редакции (спека «Шаг 8»)."""
|
||
|
||
from collections.abc import Sequence
|
||
from dataclasses import dataclass
|
||
from datetime import date
|
||
|
||
from arm_statements.checks import form_variant, subline_parents
|
||
from arm_statements.evaluation import EvalLine, is_check_line
|
||
from arm_statements.loader import Rules
|
||
from arm_statements.model import DocumentType, Edition, FormVariant
|
||
|
||
FSBU4_FROM = date(2025, 1, 1) # эффективная дата после 2024-12-31 → fsbu4
|
||
|
||
_CODE_LEN = 4
|
||
_SUBLINE_LEN = 5
|
||
_CONFIDENT = 100
|
||
|
||
|
||
def edition_by_date(effective: date | None) -> Edition | None:
|
||
if effective is None:
|
||
return None
|
||
return Edition.FSBU4 if effective >= FSBU4_FROM else Edition.RAS_66N
|
||
|
||
|
||
@dataclass(frozen=True, slots=True)
|
||
class StructureResult:
|
||
edition: Edition | None
|
||
unconfirmed: frozenset[str] # ключи строк с неуверенным кодом, не подтверждённым структурой
|
||
|
||
|
||
# Элемент последовательности: (код, уверенный).
|
||
_Item = tuple[str, bool]
|
||
|
||
|
||
def _optimum(items: Sequence[_Item], order: Sequence[str], scale: int, skip: int | None = None) -> int:
|
||
"""Взвешенная LCS `items` × `order` по равенству кода; вес позиции `scale * уверенный + 1`.
|
||
|
||
`skip` — индекс позиции `items`, которой запрещено входить. Сложность O(|items|·|order|)."""
|
||
if not items or not order:
|
||
return 0
|
||
previous = [0] * (len(order) + 1)
|
||
for index, (code, confident) in enumerate(items):
|
||
weight = None if index == skip else scale * int(confident) + 1
|
||
current = [0] * (len(order) + 1)
|
||
for j, order_code in enumerate(order, start=1):
|
||
best = max(previous[j], current[j - 1])
|
||
if weight is not None and code == order_code:
|
||
best = max(best, previous[j - 1] + weight)
|
||
current[j] = best
|
||
previous = current
|
||
return previous[-1]
|
||
|
||
|
||
def _editions(rules: Rules) -> tuple[Edition, ...]:
|
||
return rules.code_orders.editions or tuple(Edition)
|
||
|
||
|
||
def _pick(optima: dict[Edition, int], effective: date | None) -> tuple[Edition | None, tuple[Edition, ...]]:
|
||
"""Редакция с большим оптимумом; равенство — по дате. Второе значение — редакции, чьи оптимумы лучшие."""
|
||
top = max(optima.values())
|
||
tied = tuple(edition for edition, value in optima.items() if value == top)
|
||
if len(tied) == 1:
|
||
return tied[0], tied
|
||
by_date = edition_by_date(effective)
|
||
return (by_date if by_date in tied else None), tied
|
||
|
||
|
||
def _filtered(items: Sequence[_Item], order: Sequence[str]) -> tuple[list[_Item], list[int]]:
|
||
allowed = set(order)
|
||
kept = [(item, index) for index, item in enumerate(items) if item[0] in allowed]
|
||
return [item for item, _ in kept], [index for _, index in kept]
|
||
|
||
|
||
def _select(
|
||
document_type: DocumentType,
|
||
items: Sequence[_Item],
|
||
variant: FormVariant,
|
||
effective: date | None,
|
||
rules: Rules,
|
||
) -> tuple[Edition | None, tuple[Edition, ...], dict[Edition, int]]:
|
||
scale = len(items) + 1
|
||
optima = {
|
||
edition: _optimum(
|
||
_filtered(items, order := rules.code_orders.sequence(edition, document_type, variant))[0],
|
||
order,
|
||
scale,
|
||
)
|
||
for edition in _editions(rules)
|
||
}
|
||
edition, tied = _pick(optima, effective)
|
||
return edition, tied, optima
|
||
|
||
|
||
def choose_edition(
|
||
document_type: DocumentType,
|
||
codes: Sequence[tuple[str, bool]],
|
||
effective: date | None,
|
||
rules: Rules,
|
||
*,
|
||
variant: FormVariant | None = None,
|
||
) -> Edition | None:
|
||
"""Шаги 2–3 и 6 без подтверждения кодов; `codes` — пары `(код, уверенный)` строк `ok` по порядку."""
|
||
if document_type is DocumentType.DISCLOSURE:
|
||
return edition_by_date(effective)
|
||
if variant is None:
|
||
variant = form_variant(document_type, [code for code, _ in codes]) or FormVariant.FULL
|
||
items = [(code, confident) for code, confident in codes if len(code) == _CODE_LEN]
|
||
return _select(document_type, items, variant, effective, rules)[0]
|
||
|
||
|
||
def _is_uncertain(line: EvalLine) -> bool:
|
||
return (line.code_confidence is None or line.code_confidence < _CONFIDENT) and not line.code_fixed
|
||
|
||
|
||
def _confirm_sublines(checked: Sequence[EvalLine], codes: Sequence[str], confirmed_keys: set[str]) -> set[str]:
|
||
"""Пятизначные неуверенные коды: подтверждены родителем, стоящим раньше (§11.9); возвращает неподтверждённые."""
|
||
unconfirmed: set[str] = set()
|
||
parents = subline_parents(codes)
|
||
for position, line in enumerate(checked):
|
||
code = line.line_code or ""
|
||
if len(code) == _CODE_LEN or not _is_uncertain(line):
|
||
continue
|
||
parent = parents[position]
|
||
found = None
|
||
if len(code) == _SUBLINE_LEN and parent is not None:
|
||
found = next((e for e in reversed(checked[:position]) if e.line_code == parent), None)
|
||
if found is not None and (not _is_uncertain(found) or found.key in confirmed_keys):
|
||
confirmed_keys.add(line.key)
|
||
else:
|
||
unconfirmed.add(line.key)
|
||
return unconfirmed
|
||
|
||
|
||
def confirm_codes(
|
||
lines: Sequence[EvalLine],
|
||
document_type: DocumentType,
|
||
variant: FormVariant | None,
|
||
effective: date | None,
|
||
rules: Rules,
|
||
) -> StructureResult:
|
||
if document_type is DocumentType.DISCLOSURE:
|
||
return StructureResult(edition_by_date(effective), frozenset())
|
||
checked = sorted(
|
||
(line for line in lines if is_check_line(line) and line.order is not None),
|
||
key=lambda line: line.order or 0,
|
||
)
|
||
codes = [line.line_code or "" for line in checked]
|
||
if variant is None:
|
||
variant = form_variant(document_type, codes) or FormVariant.FULL
|
||
four = [line for line in checked if len(line.line_code or "") == _CODE_LEN]
|
||
items: list[_Item] = [(line.line_code or "", not _is_uncertain(line)) for line in four]
|
||
edition, tied, _ = _select(document_type, items, variant, effective, rules)
|
||
|
||
# Четырёхзначные неуверенные коды: входят во все лучшие подпоследовательности выбранной редакции
|
||
# (при равенстве без даты — во все лучшие обеих редакций, P1-4).
|
||
unconfirmed: set[str] = set()
|
||
confirmed_keys: set[str] = set()
|
||
scale = len(items) + 1
|
||
for index, line in enumerate(four):
|
||
if not _is_uncertain(line):
|
||
confirmed_keys.add(line.key)
|
||
continue
|
||
confirmed = True
|
||
for candidate in tied if edition is None else (edition,):
|
||
order = rules.code_orders.sequence(candidate, document_type, variant)
|
||
if items[index][0] not in order:
|
||
confirmed = False
|
||
break
|
||
kept, indices = _filtered(items, order)
|
||
total = _optimum(kept, order, scale)
|
||
if _optimum(kept, order, scale, skip=indices.index(index)) >= total:
|
||
confirmed = False
|
||
break
|
||
(confirmed_keys if confirmed else unconfirmed).add(line.key)
|
||
|
||
unconfirmed |= _confirm_sublines(checked, codes, confirmed_keys)
|
||
return StructureResult(edition, frozenset(unconfirmed))
|