fix: аудит ветки source — критические и высокие замечания

- api_settings: убраны реальные API-ключи из кода (чтение из env/файла) — 1.1
- variables: {{is_security_requirements}} больше не подставляет всегда УЗ-4;
  extended_fields/defence_level передаются в контекст — 1.2
- defence.py: эталонная таблица ПП-1119 (п.9-12), 48 комбинаций;
  ui.py: авторасчёт УЗ переведён на неё (public больше не 'всегда 4',
  спец/био/общедоступные/иные разделены) — 2.1
- generator: убран дубль хвоста параграфа в _highlight_unmatched — 2.2
- importer: категории ПДн взаимоисключающие ('иные' только если нет
  спец/био); УЗ пересчитывается при импорте — 2.3
- import_settings(.py/.json): убран перехват 'администратора безопасности'
  правилом ответственного за ПДн — 3.1
- variables: {{category}}/{{is.category}} резолвятся из personal_data_category — 3.2
- verify: ИИ-предложения ИНН/ОГРН/КПП валидируются контрольными суммами
  перед применением — 3.3
- models: defence_level None вместо '' (не маскирует 'не рассчитано')
- .gitignore: api_settings.json/.env не попадают в git
- test_defence.py: юнит-тест всех 48 комбинаций таблицы УЗ
This commit is contained in:
LocoAgent
2026-08-05 08:58:17 +04:00
parent ac122451b3
commit 230a392a15
12 changed files with 277 additions and 57 deletions
+63
View File
@@ -389,6 +389,65 @@ def compare_company_values(old_data, new_data):
return diffs
# ============================================================
# ВАЛИДАЦИЯ РЕКВИЗИТОВ (контрольные суммы ИНН/ОГРН/КПП)
# ============================================================
def _digits(s) -> str:
return re.sub(r'\D', '', str(s or ''))
def valid_inn(inn) -> bool:
"""Проверка контрольной суммы ИНН (10 цифр — юрлицо, 12 — ИП)."""
d = _digits(inn)
if len(d) == 10:
w = [2, 4, 10, 3, 5, 9, 4, 6, 8]
return (sum(int(d[i]) * w[i] for i in range(9)) % 11) % 10 == int(d[9])
if len(d) == 12:
w1 = [7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
w2 = [3, 7, 2, 4, 10, 3, 5, 9, 4, 6, 8]
n10 = (sum(int(d[i]) * w1[i] for i in range(10)) % 11) % 10
n11 = (sum(int(d[i]) * w2[i] for i in range(11)) % 11) % 10
return n10 == int(d[10]) and n11 == int(d[11])
return False
def valid_ogrn(ogrn) -> bool:
"""Проверка контрольной суммы ОГРН (13 цифр) / ОГРНИП (15 цифр)."""
d = _digits(ogrn)
if len(d) == 13:
return (int(d[:-1]) % 11) % 10 == int(d[-1])
if len(d) == 15:
return (int(d[:-1]) % 13) % 10 == int(d[-1])
return False
def valid_kpp(kpp) -> bool:
"""КПП: 9 цифр (контрольной суммы нет — проверяем формат)."""
d = _digits(kpp)
return len(d) == 9
def validate_requisites(proposals: dict) -> dict:
"""Фильтрует предложения ИИ: невалидные ИНН/ОГРН/КПП не применяются.
Возвращает (валидные_пропозалы, список_замечаний).
"""
if not isinstance(proposals, dict):
return {}, []
validators = {'inn': valid_inn, 'ogrn': valid_ogrn, 'kpp': valid_kpp}
notes = []
out = dict(proposals)
for key, fn in validators.items():
val = out.get(key)
if val and not fn(val):
notes.append(f"ИИ предложил невалидный {key.upper()} ({val}) — значение не применено")
out.pop(key, None)
return out, notes
# ============================================================
# ИИ-ПРОВЕРКА
# ============================================================
def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None):
"""ИИ-проверка реквизитов организации.
@@ -425,6 +484,10 @@ def check_company_ai(company_data, api_key, model='deepseek-chat', base_url=None
if not isinstance(fields, dict):
fields = {}
# Валидация контрольных сумм реквизитов перед применением (фикс 3.3)
proposals, req_notes = validate_requisites(proposals)
notes = list(notes) + req_notes
return {
'source': 'ИИ',
'source_url': 'openai-совместимый API',