# Архитектура DrHider

## Общая схема

```
Пользователь (браузер)
    │
    ▼
https://drhider.pythonk8s.dev.nubes.ru/
    │
    ▼
Штурвал (Managed Flask на pythonk8s)
    │
    ▼
site/app.py  ──→  Flask (create_app)
    │
    ├── GET  /          → main_bp    → templates/index.html
    ├── GET  /health    → health_bp  → {"ok": true}
    └── POST /api/drhider → api_bp   → drhider.obfuscate_files()
                                           │
                                           ▼
                                    TwoPassObfuscator
                                           │
                              ┌────────────┼────────────┐
                              │            │            │
                         extractor     scanner      replacer
                         (текст)     (regex+LLM)   (замена)
                              │            │            │
                              └────────────┼────────────┘
                                           │
                                      builder
                                      (ZIP+CSV)
                                           │
                                           ▼
                                    application/zip
```

---

## Поддерживаемые форматы файлов

| Формат | Обработка | Детали |
|--------|-----------|--------|
| **`.docx`** | → Markdown | Заголовки (H1–H3), **жирный**, *курсив*, таблицы (`python-docx`) |
| **`.pdf`** | → Markdown | Текст + таблицы (`pdfplumber`), без форматирования |
| **`.txt`** и прочие текстовые | → как есть | Декодируется UTF-8, ошибки заменяются `�` |
| **`.zip`** | → распаковка | Файлы внутри обрабатываются рекурсивно. Защита от ZIP-бомб: ≤500 файлов, ratio ≤100:1, ≤500 MB |
| **`.doc`** (бинарный) | ❌ не поддерживается | Штурвал не даёт установить LibreOffice. Если понадобится — просить админов платформы |

Ограничение на размер загрузки: **200 MB** (`MAX_CONTENT_LENGTH`).

---

## Двухпроходная обфускация

### Проход 1: сбор сущностей

```
Файлы (.docx, .pdf, .txt, .zip)
    │
    ▼
extractor.expand_zips()          ← распаковать ZIP
    │
    ▼
extractor.convert_pdfs_to_docx() ← PDF → DOCX
    │
    ▼
extractor.extract_text()         ← извлечь текст из каждого файла
    │
    ├──→ scanner.scan_regex()    ← regex: телефоны, email, ИНН, ОГРН, КПП,
    │                               БИК, счета, паспорта, компании, ФИО
    │
    └──→ scanner.scan_llm_ner()  ← LLM: имена, адреса, паспорта
                                      (только если llm_client передан)
    │
    ▼
mapping = {оригинал → фиктивное}  ← глобальный словарь замен
```

### Проход 2: замена

```
mapping + sorted_keys (по убыванию длины)
    │
    ▼
Для каждого файла:
    │
    ├── .docx → replacer.replace_in_docx()   ← склеить runs → заменить → сохранить
    ├── .doc  → без изменений (бинарный)
    └── .txt/.pdf → replacer.replace_in_text() ← простая строковая замена
    │
    ▼
builder.build_mapping_csv()       ← mapping.csv
builder.build_zip()               ← ZIP со всеми файлами + mapping.csv
```

---

## Модули

### `drhider/config.py`
Константы уровня всего пакета. НЕ содержит логики — только данные.
- `ENTITY_PATTERNS` — 10 regex-паттернов
- `COMPANY_PATTERN`, `PERSON_PATTERN` — скомпилированные regex
- Словари: `RU_SURNAMES`, `RU_NAMES`, `RU_PATRONYMICS`, `RU_CITIES`, `RU_STREETS`, `FAKE_DOMAINS`

### `drhider/checksum.py`
Чистые функции для расчёта контрольных сумм. Без побочных эффектов.
- `checksum_inn10(inn)` → 1 цифра
- `checksum_inn12(inn)` → 2 цифры
- `checksum_ogrn(ogrn)` → 1 цифра

### `drhider/random_utils.py`
Утилиты генерации случайных строк.
- `random_digits(n)` → строка из n цифр
- `random_letters(n)` → строка из n строчных латинских букв

### `drhider/generators/`
Каждый генератор — независимая функция с сигнатурой `generate_xxx(original: str) -> str`.
Параметр `original` нужен для извлечения префикса (например, «ИНН » из «ИНН 1234567890»).
Генераторы НЕ общаются друг с другом — только импортируют из `config`, `checksum`, `random_utils`.

`__init__.py` содержит `ENTITY_GENERATORS` — словарь, связывающий `entity_type` из `ENTITY_PATTERNS` с функцией-генератором. Используется в `scanner.scan_regex()`.

### `drhider/llm_client.py`
HTTP-клиент к OpenAI-совместимому API. Читает переменные окружения.
Интерфейс: `.complete(prompt: str) -> str`.

### `drhider/extractor.py`
Три независимые функции (не класс):
- `extract_text(fname, content, ctype)` → `(text, docx_document_or_None)`
- `expand_zips(files)` → распакованный список
- `convert_pdfs_to_docx(files)` → PDF заменены на DOCX

### `drhider/scanner.py`
Две функции (не класс), мутируют переданный `mapping: Dict[str, str]`:
- `scan_regex(text, mapping)` — regex-поиск
- `scan_llm_ner(all_texts, mapping, llm_client)` — LLM NER

### `drhider/replacer.py`
Три чистые функции:
- `apply_replacements(text, mapping, sorted_keys)` → строка с заменами
- `replace_in_docx(doc, mapping, sorted_keys)` → bytes (изменённый DOCX)
- `replace_in_text(text, fname, mapping, sorted_keys)` → bytes

### `drhider/builder.py`
Две функции сборки результата:
- `build_zip(files, mapping_csv)` → bytes (ZIP-архив)
- `build_mapping_csv(mapping)` → str (CSV)

### `drhider/obfuscator.py`
`TwoPassObfuscator` — оркестратор. Единственный класс, который знает о顺序е вызовов.
Метод `.obfuscate(files)` вызывает модули в правильном порядке.
`obfuscate_files()` — удобная функция-обёртка.

### `site/app.py`
Точка входа для Штурвала. `create_app()` создаёт Flask, регистрирует blueprint'ы.
`app = create_app()` — глобальный экземпляр.

### `site/routes/`
Blueprint'ы — каждый в своём файле:
- `main_bp` — только `GET /`
- `health_bp` — только `GET /health`
- `api_bp` — только `POST /api/drhider`

`__init__.py` содержит `register_routes(app)` — единая точка регистрации.

---

## Поток данных

```
HTTP POST /api/drhider (multipart/form-data)
    │
    ▼
api_bp.drhider()
    │  request.files.getlist("files")
    │  → [(filename, bytes, mimetype), ...]
    ▼
obfuscate_files(files, llm_client=LLMClient())
    │  Scanner → Extractor → LLM NER → Obfuscator → Replacer → Builder
    ▼
bytes (ZIP-архив с обезличенными документами + mapping.csv)
```

---

## Требования к Flask-приложению для Штурвала

### 1. `/health` — liveness probe

```python
@health_bp.route("/health")
def health():
    return jsonify({"ok": True, "version": "0.0.26"})
```

**Обязательно.** Штурвал дёргает `/health` для readiness/liveness checks. Без этого под не поднимется. Ответ должен быть `200 OK`.

### 2. `no_cache` на все ответы

```python
@app.after_request
def no_cache(response):
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "0"
    return response
```

**Критично для SPA-подобного UI.** Без этого nginx/браузер кэшируют страницу → F5 показывает старую версию, кнопки не работают, версия не обновляется.

### 3. `X-Accel-Buffering: no` для SSE

```python
return Response(
    stream_with_context(generate()),
    content_type="text/event-stream",
    headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"}
)
```

**Критично для SSE-прогресса.** Без этого nginx буферизует поток → прогресс не отображается в реальном времени, все события приходят одним блоком в конце.

### 4. `MAX_CONTENT_LENGTH`

```python
app.config["MAX_CONTENT_LENGTH"] = 200 * 1024 * 1024  # 200 MB
```

Без этого Flask использует дефолтный лимит (~1MB) и отвергает docx/pdf.

### 5. `GeneratorExit` в SSE-генераторе

```python
try:
    yield f"event: start\n..."
except GeneratorExit:
    return  # клиент отключился — не продолжаем обработку
```

**Критично.** При закрытии вкладки браузер рвёт SSE-соединение. Без `GeneratorExit` генератор продолжает слать запросы в LLM, тратя токены впустую.

### 6. `stream_with_context`

```python
from flask import stream_with_context

return Response(stream_with_context(generate()), ...)
```

**Обязательно для SSE.** Без этого Flask держит приложение заблокированным на время SSE-соединения, другие запросы не обрабатываются.

### 7. `sys.path` для импорта пакета drhider

```python
_sys_path_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _sys_path_root not in sys.path:
    sys.path.insert(0, _sys_path_root)
```

Штурвал запускает из `site/`, пакет `drhider/` на уровень выше. Без этого импорт не работает.

### 8. In-memory session storage

Сессии хранятся в `site/session.py` как `Dict[str, dict]`. **Не персистентны** — теряются при рестарте пода. Очистка по таймауту (2 часа).

### 9. LLM-клиент через переменные окружения

```python
LLM_KEY       # API ключ
LLM_URL       # эндпоинт (по умолчанию https://api.aillm.ru/v1/chat/completions)
LLM_MODEL     # модель (по умолчанию gpt-oss-120b)
```

### 10. BOM в CSV

```python
buf.write('\ufeff'.encode('utf-8') + csv_str.encode('utf-8'))
```

BOM (`\ufeff`) нужен для корректного открытия CSV в Excel (чтобы русские буквы не ломались).
    │
    ▼
TwoPassObfuscator.obfuscate(files)
    │
    ├── expand_zips → convert_pdfs_to_docx → extract_text
    ├── scan_regex + scan_llm_ner → mapping
    ├── replace_in_docx / replace_in_text → обфусцированные файлы
    └── build_mapping_csv + build_zip → (zip_bytes, csv_str)
    │
    ▼
send_file(BytesIO(zip_data), mimetype="application/zip")
    │
    ▼
HTTP 200 + ZIP-архив
```

### Имена скачиваемых файлов

Формат: `drhider_YYYY-MM-DD_HH-MM-SS.zip` и `mapping_YYYY-MM-DD_HH-MM-SS.csv`

Время — московское (UTC+3). Генерируется на сервере в момент запроса:

```python
# api_bp.py — download() / csv_download()
ts = (datetime.now() + timedelta(hours=3)).strftime("%Y-%m-%d_%H-%M-%S")
download_name=f"drhider_{ts}.zip"   # → Content-Disposition
download_name=f"mapping_{ts}.csv"   # → Content-Disposition
```

Фронтенд (JS) читает имя из заголовка `Content-Disposition`, **не** хардкодит.

---

## Принципы

1. **Модули — чистые функции.** Где возможно — без классов, без состояния.
2. **Единственный класс — TwoPassObfuscator.** Только он управляет состоянием (mapping, sorted_keys).
3. **Flask — тонкая прослойка.** Только принимает запрос, вызывает `obfuscate_files()`, отдаёт ответ.
4. **Без БД.** Всё в памяти. Никаких внешних зависимостей кроме LLM API.
5. **⛔ Ноль внешних зависимостей на фронтенде.** Никаких CDN, внешних шрифтов, иконок, JS-библиотек. ВСЁ в коде. Страница грузится за 1 запрос.
6. **⛔ ВСЯ обработка данных — на бэкенде.** Фронтенд только отображает и скачивает. Никакой JS-логики для ZIP, парсинга, обфускации. Один запрос → один ответ.
7. **⛔ Пофайловая обработка — на бэкенде.** Файлы обрабатываются строго последовательно (по одному), без параллельных LLM-запросов. Прогресс — бэкенд сообщает статус каждого файла.
