# review-summary-evidence-check

Локальный CLI сопоставляет подготовленную сводку с **переданным** корпусом отзывов. Проверяет существование ID, наличие дословных цитат, уникальный числитель и знаменатель всего корпуса. Не проверяет смысл и не разрешает публикацию — даже если всё сошлось.

**Версия:** 1.0.0 ([VERSION](VERSION)). **Проверенная и поддерживаемая среда:** Python 3.14.4 / macOS 15.7.7, x86_64. Остальные версии Python, ОС и архитектуры не проверены и не заявлены как поддерживаемые. Только стандартная библиотека; без установки зависимостей, сети, модели и ключей.

Это самостоятельный repository-owned публичный пакет. Копировать его в живые внутренние скиллы для запуска не нужно. Автоматическое обнаружение агентами и установка через каталоги скиллов не проверялись. Для самой сверки агент не нужен; передача текстов в разговор с агентом может передать их провайдеру его модели.

## Быстрый старт

Из папки пакета:

```sh
python3 scripts/check.py examples/reviews.json examples/summary.json
python3 scripts/check.py examples/reviews.json examples/summary-signals.json
python3 scripts/check.py examples/reviews.json examples/summary-semantic-negative.json
python3 tests/run.py
```

Ожидаемые коды первых трёх команд: **0, 1, 0**. Последний ноль демонстрирует ограничение: «запаха нет» проходит под `odor`, поскольку семантика не проверяется. JSON-сигналы не содержат исходных ID, цитат или путей — только коды, количества и нулевые индексы.

## Формат входов

Оба файла — обычные локальные файлы с JSON в строгом UTF-8 **без BOM**. Все перечисленные поля обязательны; неизвестные поля запрещены на каждом уровне. Комментарии, повторяющиеся JSON-ключи, дробные числа, NaN/Infinity, одиночные Unicode-суррогаты не принимаются. Это авторский узкий профиль, не формат WB/Ozon и не загрузчик PDF, XLSX, CSV или произвольного ответа модели.

**Корпус `REVIEWS.json`:**

```json
{
  "scope_id": "fictional-demo/v1",
  "reviews": [
    {"id": "R1", "text": "Крышка треснула. запаха нет"},
    {"id": "R2", "text": "Размер подошёл"}
  ]
}
```

**Сводка `SUMMARY.json`:**

```json
{
  "scope_id": "fictional-demo/v1",
  "source_sha256": "<64 строчных шестнадцатеричных символа SHA256 точных байтов REVIEWS.json>",
  "topics": [
    {
      "id": "damage",
      "count": 1,
      "denominator": 2,
      "evidence": [{"review_id": "R1", "quote": "Крышка треснула"}]
    }
  ]
}
```

Плейсхолдер в `source_sha256` нужно заменить; готовые запускаемые пары — в [examples](examples/README.md). Хеш вычисляется над **точными байтами**, включая пробелы и переносы строк. Для получения хеша собственного файла из папки пакета можно использовать тот же ограниченный читатель файлов. Это только подготовка `source_sha256`, не проверка сводки:

```sh
python3 - <<'PYHASH'
import hashlib
import json
import runpy
import sys
check = runpy.run_path("scripts/check.py")
try:
    raw = check["read_regular"]("REVIEWS.json")
except (OSError, check["InvalidInput"]):
    print('{"status":"invalid_input","publication_allowed":false}')
    sys.exit(2)
print(json.dumps({"source_sha256": hashlib.sha256(raw).hexdigest(),
                  "publication_allowed": False}))
PYHASH
```

Обычный CLI проверки по-прежнему принимает ровно два пути; отдельного режима вычисления хеша в нём нет.

### Поля и границы

Длина строк считается в Unicode-кодовых точках, размер файла — в байтах. Непустая строка не должна состоять только из пробельных символов. Сравнение строк точное, без обрезки пробелов, нормализации Unicode, регистра или смысла.

| Элемент | Контракт |
|---|---|
| Каждый входной файл | ≤ 1 048 576 байт (1 МиБ), проверка до полного чтения и разбора |
| Вложенность JSON | ≤ 5 контейнеров, считая корневой объект; ограничивается до `json.loads` |
| `scope_id` обоих файлов | Строка 1–128 символов, точное совпадение |
| `reviews` | 1–1000 записей |
| `reviews[].id` | Непустая строка ≤ 128 символов; ID уникальны в корпусе |
| `reviews[].text` | Непустая строка ≤ 4096 символов |
| `source_sha256` | 64 символа `0–9a–f`, совпадение с SHA256 байтов корпуса |
| `topics` | 1–100 тем |
| `topics[].id` | Непустая строка ≤ 128 символов; ID уникальны среди тем |
| `count`, `denominator` | Целые JSON-числа 0–1000; `true`, `false`, `1.0`, `1e0` запрещены |
| `evidence` одной темы | 0–1000 записей; всего во всех темах ≤ 5000 |
| `evidence[].review_id` | Непустая строка ≤ 128 символов |
| `evidence[].quote` | Непустая строка ≤ 1024 символов |
| Бюджет поиска | Сумма `len(text) + len(quote)` по ссылкам на существующие ID ≤ 10 000 000; повторные ссылки тоже расходуют бюджет |
| Каждый путь | ≤ 4096 символов, ≤ 64 компонента кроме пустых и `.` |

Все лимиты действуют **одновременно**, а не гарантируют приём максимального значения каждого поля в одном файле. Например, длинные тексты могут исчерпать байтовый лимит раньше 1000 отзывов. Лимит бюджета — детерминированная мера объёма поиска, не обещание точного числа машинных операций или времени исполнения. Проверка профиля и бюджета завершается до поиска подстрок.

### Файлы и неизменность входов

Скрипт открывает только два заданных файла на чтение; не пишет в них, не создаёт результат рядом и не использует сеть. Симлинки **в любом явно переданном компоненте пути**, директории, FIFO, сокеты и устройства не принимаются. Открытие компонентов через `dir_fd`/`O_NOFOLLOW` не позволяет подмене родительского пути перенаправить открытый дескриптор; `O_NONBLOCK` не даёт FIFO заблокировать чтение при гонке. Чтение ограничено максимумом **1 МиБ + 1 контрольный байт**, даже если файл вырос после проверки размера. Обычное изменение размера/времени/идентичности файла во время открытия и чтения отклоняется.

На macOS `/tmp` и `/var` обычно симлинки. Используйте физический путь к обычному файлу, а не путь через симлинк; CLI не преобразует их молча. Во время проверки не изменяйте входы другим процессом: два отдельных чтения не являются атомарной транзакцией двух файлов. Неизменность относится к содержимому, не к системному времени доступа.

## Как считается результат

Для каждой темы:

1. Существующий `review_id` и хотя бы одна **дословная непустая подстрока** из соответствующего текста дают один зачтённый ID. Цитата из другого отзыва не подходит.
2. Повтор того же `review_id` внутри темы даёт `duplicate_evidence_id`, даже если цитата другая. В числителе один ID учитывается не больше одного раза. Если одна цитата не найдена, а другая найдена, ID всё равно зачтён один раз; сигнал по плохой цитате сохраняется.
3. `count` сравнивается с количеством зачтённых уникальных ID. `denominator` — с **числом всех записей** в `reviews`, а не с количеством ссылок, покупок, людей или отзывов магазина.
4. Темы независимы и могут пересекаться. Один отзыв допустим в разных темах; сумма долей не обязана равняться 100 %. Повторяющиеся тексты с разными исходными ID не объединяются автоматически.

Пустой список доказательств при `count: 0` допустим, но не подтверждает, что жалоб этой темы действительно нет. Пустой корпус не допускается.

## CLI и машинный результат

```sh
python3 scripts/check.py REVIEWS.json SUMMARY.json
python3 scripts/check.py --help
python3 scripts/check.py --version
```

Пути с пробелами заключайте в кавычки. Произвольных опций, сетевых URL и чтения stdin нет. `--help` / `--version` — единственные опции; служебный JSON с кодом 0 не означает проверки входов.

В stdout — одна JSON-запись с переводом строки, без обычных логов; stderr при обработанных ошибках входа пуст. Обычные ошибки обрабатываются без traceback и без исходных значений/путей. При недоступном stdout (например, закрытом pipe) скрипт возвращает 2, не может доставить JSON и не пишет traceback.

| Код | `status` | Смысл |
|---|---|---|
| 0 | `structural_evidence_clear` | Только структурные сверки пройдены |
| 1 | `signals` | Найдены структурные несоответствия |
| 2 | `invalid_input` | Профиль, файл, лимиты или условия выполнения не подошли |

Пример при коде 1 (часть полей):

```json
{
  "status": "signals",
  "publication_allowed": false,
  "signals": [{"topic_index": 0, "evidence_index": 0, "code": "unknown_review_id"}],
  "topics": [{"topic_index": 0, "valid_evidence_count": 0, "source_texts": 5}]
}
```

Полный результат проверки также содержит `version` и `not_checked`. `signals` — массив в порядке тем/доказательств, затем сверки числителя и знаменателя темы; индексы отсчитываются с нуля. Ни в обычном результате, ни в ошибках нет сырых ID, цитат, scope, хешей или путей. Их ищут локально по индексам. `publication_allowed` **всегда `false`** во всех видах ответа.

Сигналы: `unknown_review_id`, `quote_not_verbatim`, `duplicate_evidence_id`, `count_mismatch`, `denominator_mismatch`.

Ошибка: `{"status":"invalid_input","publication_allowed":false,"error":{"code":"fields_mismatch"},"version":"1.0.0"}`. Возвращается первая найденная ошибка; если нарушено несколько границ, код зависит от порядка проверок.

| Коды `error.code` | Что проверить |
|---|---|
| `two_paths_required`, `path_invalid` | Аргументы, длину/форму пути |
| `regular_file_required`, `file_unavailable`, `input_changed` | Тип/наличие/доступность файла, симлинки, внешнюю запись во время чтения |
| `input_limit`, `nesting_limit`, `number_limit` | Байтный размер, глубину, числовой токен (не больше 4 цифр без знака) |
| `utf8_required`, `utf8_without_bom_required`, `json_invalid` | Кодировку и JSON-синтаксис |
| `duplicate_json_key`, `non_finite_number`, `integer_required` | Повтор ключей, NaN/Infinity, дробные/экспоненциальные числа |
| `fields_mismatch`, `string_invalid`, `array_limit`, `count_invalid` | Поля, типы, пустые значения и границы профиля |
| `duplicate_source_id`, `duplicate_topic_id` | Уникальность идентификаторов |
| `digest_invalid`, `scope_mismatch`, `snapshot_mismatch` | Формат хеша и привязку сводки к корпусу |
| `total_evidence_limit`, `search_budget_limit` | Суммарные доказательства и бюджет поиска |
| `interrupted`, `internal_error` | Прерванный запуск или непредвиденный сбой; результат проверки не получен |

## Чего проверка не подтверждает

Смысл/полярность цитаты, верность темы, полноту жалоб, репрезентативность корпуса, правдивость автора, права на чужие данные, качество модели/сервиса и влияние на продажи. Настоящее «запаха нет» под `odor` проходит **по замыслу** — это ограничение, не распознавание жалобы. Для примеров используются только авторские вымышленные данные.

Инструмент не собирает отзывы, не подключает кабинеты, не обходит ограничения площадок, не отправляет ответы, не вызывает модель/API, не выполняет инструкции из текста, не исправляет входы и ничего не публикует.

## Проверка и состав

```sh
python3 tests/run.py
python3 -m unittest discover -s tests -v
```

Production-suite содержит независимые проверки границ, чтения до лимита, ошибок/CLI, корпуса из 1000 отзывов, инертных инструкций и отрицательного смыслового контроля. `tests/run.py` выводит отчёт JSON в stdout и детали unittest в stderr, код 0 означает успешный прогон. Тесты создают только временные входы; сохранённые результаты редакционного прогона **не входят** в пользовательский пакет.

| Путь | Назначение |
|---|---|
| [SKILL.md](SKILL.md) | Пошаговый сценарий для человека/агента |
| [scripts/check.py](scripts/check.py) | Локальный CLI |
| [examples/README.md](examples/README.md) | Запускаемые пары вымышленных входов и ожидаемые исходы |
| [tests/test_check.py](tests/test_check.py), [tests/run.py](tests/run.py) | Воспроизводимая production-suite |
| [VERSION](VERSION) | Версия поставки |
| [LICENSE](LICENSE) | MIT для собственного кода/документации, © 2026 Редакция Кометы |
| [examples/CC0-1.0.txt](examples/CC0-1.0.txt) | Отдельное посвящение собственных вымышленных JSON-примеров в общественное достояние |
