# TenderLens

Путь от HTML-документа до векторного поиска и ответа со ссылками на фрагменты. Реальные очередь, индексатор и pgvector работают с четырьмя собственными учебными документами.

Статус: passed

Версия: `a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16`. История реализации: с 20 августа 2026. Публичное демо: 7 сентября 2026.

## Задача

Ответ по набору документов должен объяснять происхождение утверждений и явно сообщать, когда релевантного контекста нет.

## Аудитория

Разработчики поиска и RAG-систем, аналитики документов.

## Личный вклад

- Контракты нормализации, событийная индексация и хранение chunks.

- Поиск, API ответа с источниками и обработка отсутствующего контекста.

- Публичный корпус, детерминированные AI-моки и независимые сессионные лимиты.

## Решения

- Seed отправляет настоящее TenderChangedV1 через NATS; индексатор извлекает HTML и формирует chunks.

- PostgreSQL VECTOR(1024) выполняет cosine search. FakeAIProvider создаёт embeddings через hashing trick.

- Повтор события использует content_hash: неизменённый документ не дублирует chunks.

- Генерация ответа — обозначенный шаблон из найденных фрагментов; внешний crawler и Ollama выключены.

## Границы и ограничения

- Четыре синтетических документа и 16 chunks — демонстрационный корпус, не база реальных закупок.

- Cosine score и шаблонный ответ не являются оценкой качества промышленной модели.

- До 30 запросов в минуту на сессию и 1000 живых demo-ключей. Запросы пользователя не сохраняются.

- Worker health подтверждает соединение при старте; отдельный детектор зависшего вычисления не реализован.

## Что попробовать

- Найдите документ о серверах или Angular-портале.

- Задайте вопрос и откройте источники ответа.

- Введите запрос вне корпуса и проверьте отсутствие контекста.

- Повторите индексационные события: четыре документа и 16 chunks сохранятся без дублирования.

## Проверки

- 118 unit-тестов пройдено; 18 интеграционных/внешних проверок исключены из этого запуска.

- Search and Ask return sources; unrelated query returns no context

- Queue replay preserves 16 chunks and ready status

- Independent signed expiring HttpOnly sessions

- Подтверждённого GitHub CI для этого SHA нет; перечислены выполненные локальные проверки и сценарии опубликованной версии. Исторические CI-ссылки, если есть, отмечены отдельно.

## Результаты

- Сохранено 12 содержательных кадров фактического интерфейса.

- Дата, сценарий и версия указаны у каждого кадра.

- Максимум RAM контейнеров в коротком тесте: 127.04 MiB. Общие службы учитываются отдельно; магазин включает свою БД.

- Измерено 6 повторений сценария; медиана 997.0 мс, максимум 2738 мс. Время включает браузер и HTTPS; методика и ограничения приложены.

- Четыре ready-документа, 16 chunks. Повтор четырёх NATS-событий не создал дубликаты.

## Запуск и эксплуатация

- Live-архитектура исходного проекта предусматривает crawler и AI provider. Публичная версия запрещает live AI конфигурацией и принимает только собственный корпус.

- Dockerfile.portfolio собирается вне VPS; API и worker используют один закреплённый образ и отдельные лимиты. PostgreSQL и NATS доступны только по внутренней сети, API — через localhost:18403 и Nginx.

- При запуске выполнить миграции и CLI portfolio seed. Readiness API проверяет зависимости. Повтор индексации безопасен благодаря content_hash; после рестарта сверять четыре ready-документа и 16 chunks. Сессионные ApiKey удаляются по TTL каждый пять минут.

- Общие правила эксплуатации: готовые файлы и закреплённые образы собираются вне VPS; секреты не входят в артефакты. Общесерверный docker prune не применяется. Логи backend ограничены ротацией, изменения log options требуют пересоздания контейнера.

- Перед изменением пройти code-map от функции к экрану/API, модулю и данным. После изменения обновить карту и схемы, выполнить связанные сценарии, включая ошибку, повтор и сброс. Источник истины — конкретная версия кода, а не старая диаграмма.

- Откат проверен 2026-09-07T01:04:24.534304+00:00: 20260907-verified → 20260907c → 20260907-verified. После каждого переключения подтверждены HTTPS, readiness и SHA. Это проверка конфигурации и артефактов на одном проверенном SHA; обратная миграция БД не выполнялась.

## Стек и роль

- **FastAPI / Pydantic**: HTTP-контракты, валидация и ответы поиска.
- **NATS JetStream**: Доставка событий индексатору и повтор.
- **PostgreSQL / pgvector**: Документы, attachments, chunks и cosine search.
- **SQLAlchemy / Python**: Нормализация, извлечение, chunking и проверка content_hash.

## Code-map

|Функция|Экран / API|Модуль|Данные / проверка|
|---|---|---|---|
|Начальная индексация|CLI portfolio|cli._upsert_demo_record, NatsBroker, IndexerService|sources, tenders, attachments, chunks|
|Поиск/ответ|api/v1/search, ask|SearchService, FakeAIProvider|chunks VECTOR(1024)|
|Источники|локальные HTML|web/demo|собственный корпус|
|Повтор события|api/demo/replay|NatsBroker / hash guard|общий read-only корпус|
|Сессия и лимит|cookie / middleware|portfolio, auth, rate_limit|api_keys (только хеш и счётчики)|

## Схемы

- Компоненты опубликованного демо — `diagrams/components.mmd`
- Основной сценарий — `diagrams/scenario.mmd`
- Размещение опубликованной версии — `diagrams/deployment.mmd`
- Состав данных и границы хранения — `diagrams/data.mmd`
- Исходный интеграционный контур — не развёрнут в демо — `diagrams/original.mmd`

## Индекс изображений

- 01-overview: Поиск и явные границы демонстрационного режима; 2026-09-06T23:42:53.262Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 02-index-status: Фактические состояния четырёх документов и 16 чанков из PostgreSQL; 2026-09-06T23:42:53.631Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 03-server-search: Результат реального cosine search с mock embeddings; 2026-09-06T23:42:54.156Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 04-result-detail: Фрагмент, имя вложения, cosine и ссылка на источник; 2026-09-06T23:42:54.242Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 05-source: Собственный синтетический HTML-источник; 2026-09-06T23:42:54.468Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 06-answer: Шаблонный ответ на основании найденных источников; 2026-09-06T23:42:54.982Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 07-angular-answer: Другой сценарий: требования к Angular-порталу; 2026-09-06T23:42:55.514Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 08-no-context: Отказ от ответа при отсутствии подходящих фрагментов; 2026-09-06T23:42:55.991Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 09-validation: Проверка минимальной длины запроса; 2026-09-06T23:42:56.427Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 10-replay: Реальный повтор четырёх событий через NATS, идемпотентный worker; 2026-09-06T23:42:57.274Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 11-mobile-form: Мобильный интерфейс, источники моков и форма запроса; 2026-09-06T23:42:59.048Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16
- 12-mobile-results: Мобильные карточки результатов; 2026-09-06T23:42:59.587Z; SHA a618d1e7be8ac2d9ffc7b6818dc5c6b550089e16

## Подробности runtime из репозитория

# TenderLens: публичный учебный корпус

Подготовлено 07.09.2026. Четыре собственных синтетических HTML-документа: серверы, резервное копирование, Angular-портал, мониторинг сети. Реальных заказчиков и закупок нет. Внешний crawler и Ollama не запускаются.

Реально работают нормализация TenderRecordV1, NATS JetStream, извлечение HTML, chunking, SQLAlchemy, PostgreSQL VECTOR(1024), cosine search, контроль отсутствия контекста и атомарный rate limiter. FakeAIProvider использует hashing trick; генерация — шаблон из найденных фрагментов. Cosine и вероятности не выдаются за качество модели.

Seed проходит `_upsert_demo_record` → TenderChangedV1 → NATS → IndexerService. Кнопка повторяет реальные события; unchanged content_hash позволяет worker идемпотентно пропустить повтор. Документы общие и read-only. Пользовательские запросы не сохраняются; счётчик лимита принадлежит подписанной часовой HttpOnly/Secure cookie. Просроченные demo ApiKey удаляются каждые пять минут. Сброс очищает интерфейс, сохраняя лимит сессии.

Публичная конфигурация запрещает live AI. До 30 запросов в минуту на сессию, до 1000 живых demo-ключей. JetStream ограничен 1000 событиями, 8 MiB и одним часом хранения. API и worker имеют собственные лимиты и ротацию логов. Worker readiness означает установленное соединение с очередью при старте; отдельный механизм определения зависшего вычисления не реализован.

| Функция | API/экран | Код | Данные |
|---|---|---|---|
| Начальная индексация | CLI portfolio | cli._upsert_demo_record, NatsBroker, IndexerService | sources, tenders, attachments, chunks |
| Поиск/ответ | api/v1/search, ask | SearchService, FakeAIProvider | chunks VECTOR(1024) |
| Источники | локальные HTML | web/demo | собственный корпус |
| Повтор события | api/demo/replay | NatsBroker / hash guard | общий read-only корпус |
| Сессия и лимит | cookie / middleware | portfolio, auth, rate_limit | api_keys (только хеш и счётчики) |

Перед изменением проследить DTO → событие → индексатор → SQL → SearchResponse. После изменения обновить code-map, схемы и проверить повтор события, ссылки на источники, отсутствие контекста и независимые лимиты двух сессий. Документация исходной live-архитектуры сохраняется отдельно от демо-архитектуры.
