diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e0ff9c3b..2c57bf69 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -51,3 +51,10 @@ jobs:
- name: Test
if: ${{ !cancelled() }}
run: pnpm test
+ # Docs integrity (#102): fail on a dangling docs reference in source or an
+ # unindexed doc. The checker self-tests first, so the gate is itself gated.
+ - name: Docs integrity
+ if: ${{ !cancelled() }}
+ run: |
+ pnpm check:docs:test
+ pnpm check:docs
diff --git a/README.md b/README.md
index 1c3deecc..3a65a504 100644
--- a/README.md
+++ b/README.md
@@ -10,9 +10,11 @@
Дизайнът и решенията живеят в [`docs/`](docs/) — започнете оттам:
+- [`docs/architecture.md`](docs/architecture.md) — преглед на системата и карта към решенията.
+- [`docs/adr/`](docs/adr/README.md) — Architecture Decision Records (по едно решение на файл).
+- [`docs/core-scope.md`](docs/core-scope.md) — доменен модел и речник на данните.
- [`docs/etl.md`](docs/etl.md) — ETL pipeline-ът и емисията от ЦАИС ЕОП.
- [`docs/deploy.md`](docs/deploy.md) — деплой към Cloudflare.
-- [`docs/architecture.md`](docs/architecture.md) — архитектурни решения (ADR).
- [`docs/spec/ai-assistant.md`](docs/spec/ai-assistant.md) — спецификация на AI асистента (планиран).
- [`docs/README.md`](docs/README.md) — **пълен индекс** на документацията, вкл. достъпност и стандартите за ревю.
diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx
index 344060af..bddb9119 100644
--- a/apps/web/app/routes/contract.tsx
+++ b/apps/web/app/routes/contract.tsx
@@ -306,7 +306,7 @@ export default function Contract({ loaderData }: Route.ComponentProps) {
не е посочен в данните
),
// Break the gross count down by status/category — surfaces what „Брой оферти" actually
- // means (it's the gross submitted count, including rejections — see docs/etl-pipeline.md
+ // means (it's the gross submitted count, including rejections — see docs/etl.md
// and the staging columns at packages/db/migrations/0000_init.sql:363-365). Each clause
// only appears when the source published a non-zero value, so contracts without any
// rejection/SME data fall back to the original „самите оферти…" footnote.
diff --git a/docs/README.md b/docs/README.md
index 278e4ffe..568550ad 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -2,12 +2,20 @@
Дизайнът, решенията и спецификациите на платформата за прозрачност на обществените поръчки живеят тук. За преглед на продукта и бърз старт вижте [`README.md`](../README.md) в корена; работните конвенции са в [`AGENTS.md`](../AGENTS.md).
-- [`architecture.md`](architecture.md) — архитектурните решения: рендериране (React Router v7 SSR на Workers), сигурност и достъп до D1.
+- [`architecture.md`](architecture.md) — преглед на системата (поток на данните, двата Worker-а) и карта към решенията.
+- [`adr/`](adr/README.md) — Architecture Decision Records: по едно архитектурно решение на файл, с индекс и шаблон.
+- [`core-scope.md`](core-scope.md) — доменният модел и **речникът на данните**: таблици, rollup-и, `value_flag`/`date_flag`, семантиката на `amount_eur`.
- [`etl.md`](etl.md) — ETL pipeline-ът и open-data емисията на ЦАИС ЕОП (`storage.eop.bg`): зареждане, опресняване и производни таблици.
+- [`etl-pipeline-state.md`](etl-pipeline-state.md) — анализ на текущото състояние на ETL pipeline-а.
+- [`etl-architecture.md`](etl-architecture.md) — целевата ETL архитектура (RFC): предложение за състоянието и реда на изпълнение.
+- [`v1-implementation-plan.md`](v1-implementation-plan.md) — precompute слоят и пагинацията (защо rollup-и и keyset вместо per-request GROUP BY / OFFSET).
+- [`integrity-gate.md`](integrity-gate.md) — reconciliation gate-ът: hard asserts върху тоталите при import/CI.
+- [`anomaly-report.md`](anomaly-report.md) — cross-row аномалии при опресняване: какво `value_flag` не хваща на ниво отделен договор.
- [`deploy.md`](deploy.md) — деплой към Cloudflare: двата Worker-а (`sigma`, `sigma-etl`) и споделеният D1 per environment.
- [`api.md`](api.md) — публичните данни и машинно четими endpoint-и (CSV/JSON/sitemap), query грамата на филтрите и лицензът — за разработчици, които строят върху данните.
- [`accessibility.md`](accessibility.md) — достъпност (WCAG 2.1 AA / EN 301 549): какво покрива платформата и наблюденията за вградената приставка за достъпност.
- [`spec/ai-assistant.md`](spec/ai-assistant.md) — спецификация на разговорния аналитичен слой над СИГМА (BgGPT, текст и глас).
+- [`spec/assistant-contracts.md`](spec/assistant-contracts.md) — контрактите BE↔FE за AI асистента (Фаза 1 → Фаза 2).
## Стандарти за ревю
diff --git a/docs/adr/0001-rendering-and-security.md b/docs/adr/0001-rendering-and-security.md
new file mode 100644
index 00000000..e251f26a
--- /dev/null
+++ b/docs/adr/0001-rendering-and-security.md
@@ -0,0 +1,81 @@
+# ADR-0001 — Стратегия за рендиране на front-end-а и модел на сигурност
+
+- **Статус:** Прието
+- **Дата:** 2026-05-21
+- **Обхват:** Итерация 1 — публичният, read-only explorer на обществените поръчки (АОП). Документът отбелязва и пътя напред към по-широката платформа (оценка на риска, аномалии, картелен анализ — roadmap).
+
+> **Актуализация (2026-05-21):** Решението за фреймуърк по-долу беше променено от SvelteKit на **React Router v7 върху Cloudflare Workers** след претегляне на екосистемата, набирането на хора и контрибутори, primitives за достъпност и екосистемата за визуализации/AI. Решенията за рендиране (§2) и за сигурност (§3) са независими от фреймуърка и остават непроменени; обновени бяха само техните специфични за фреймуърка механики.
+
+## Контекст
+
+Итерация 1 е **публичен, read-only** отчетен и визуализационен слой върху ~190 хил. реда договори от АОП в Cloudflare D1, насочен към граждани, журналисти и НПО, с интерфейс на български. Уеб приложението първоначално беше скелетирано върху **SvelteKit**; този ADR преразглежда този избор. Сървърната среда е **Cloudflare Workers**, които четат D1. Данните пристигат като **периодични bulk зареждания** (`scripts/import.mjs`), а не като жива емисия; в тази итерация **няма публичен write път и няма автентикация**.
+
+Три въпроса стояха в основата на решението:
+
+1. Трябва ли front-end-ът да премине от Svelte към React?
+2. Трябва ли страниците да се рендират на сървъра (SSR), да се пре-рендират (статично), или да са класическо клиентско SPA срещу API?
+3. Променят ли отговора атаките през SSR / „hydration“?
+
+## Решение
+
+### 1. React с React Router v7 (framework mode) върху Cloudflare Workers
+
+Избран пред първоначалния SvelteKit скелет след претегляне на дълготрайните фактори:
+
+- **Екосистема и преизползване** — най-голямата екосистема от компоненти/библиотеки и най-богатите опции точно за повърхностите, върху които СИГМА стъпва: мрежови графи на връзки (`@xyflow/react`), таблици за големи данни (TanStack Table / AG Grid), графики (visx / Recharts), а за по-късния асистент — AI SDK (`@ai-sdk/react`) и зрели UI-та за асистенти.
+- **Набиране на хора и open-source контрибутори** — значително по-голям talent pool; релевантно за вероятно OSS гражданска платформа („Лицензът ще бъде определен преди публичното пускане“).
+- **Primitives за достъпност** — React Aria / Radix са сред най-добрите в класа си за изричната цел **WCAG 2.2 AA**.
+- **Дълготрайност** — най-защитимият дългосрочен избор за дълголетна платформа от обществена полза.
+
+**React Router v7 (framework mode), а не Next.js** — той върви чисто върху **Cloudflare Workers** (през `@cloudflare/vite-plugin`) и запазва модела SSR-на-edge и хибридното рендиране от §2. Приети компромиси спрямо SvelteKit: по-голям клиентски runtime (смекчен от SSR + кеширане) и липса на first-party CF *Pages* адаптер — RR7 се деплойва като Worker, което освен това уеднаквява `apps/web` с останалите `apps/*`.
+
+### 2. Рендиране: хибридно, избрано според повърхността
+
+По подразбиране **SSR + edge кеширане**; пре-рендиране само за реално статичните страници; клиентско рендиране само за интерактивните „острови“. **Не** чисто SPA за публично съдържание (SEO/споделяемост + цена на първото изрисуване); **не** build-time пре-рендиране на целия корпус (десетки хиляди страници за компании/институции, а търсенето изобщо не може да се пре-рендира).
+
+| Повърхност | Стратегия | Защо |
+| --- | --- | --- |
+| Начало, За проекта, **методология / „как работят червените флагове“**, документация за отворените данни | Пре-рендиране (статично) | Рядко се променят → надеждно + безплатно |
+| Профил на компания (по ЕИК), страница на институция, детайл на поръчка/обособена позиция | **SSR + edge кеш** (`s-maxage` + `stale-while-revalidate`), purge при презареждане на набора данни | Голям обем, променя се само при bulk презареждане → скорост като при статика + устойчивост на DDoS, актуалност след презареждане |
+| Explorer: търсене / филтри / класации („най-големи бенефициенти“) | SSR, кратък кеш, варира според заявката | Безкрайно URL пространство; задвижван от SQL |
+| Интерактивни визуализации (граф на мрежа/потоци, карта на България, сортируеми таблици) | Клиентски рендиран **остров**, хидриран върху SSR-ната страница + първоначални данни | Нуждае се от клиентска интерактивност; SSR на обвивката заради SEO/първо изрисуване |
+
+Тъй като наборът данни е периодична снимка (а не жив), кеш TTL-ите може да са дълги, а едно презареждане просто **purge-ва** кеша — почти всички предимства на статиката, без да се изброява корпусът при build.
+
+### 3. Модел на сигурност
+
+Приоритети за публичен сайт за прозрачност: **интегритет ≈ наличност ≫ конфиденциалност** — данните са публични по дизайн; тяхната *достоверност* и *uptime* са продуктът.
+
+**Контроли в итерация 1:**
+
+- **Read път да остане read-only.** Ingestion-ът е офлайн (`scripts/import.mjs`); няма публичен write endpoint. Това се запазва — без публична мутация, без админ в итерация 1.
+- **Edge модел + кеширане като поглъщане на DDoS.** Cloudflare поглъща L3/4; кешираният HTML означава, че атакуващ / scrape трафик удря кеша, а не D1.
+- **WAF + rate limiting** върху задвижвания от SQL explorer/търсене и върху всеки export на отворени данни; ограничаване на `limit` / пагинация / размер на export-а, така че нито една заявка да не сканира цялата таблица.
+- **Строг CSP + security headers** — генериране на per-request nonce в `entry.server.tsx`, подаван към `` / `` на React Router и към header-а `Content-Security-Policy` (за да са разрешени hydration скриптовете на фреймуърка без `unsafe-inline`). Добавяне на HSTS, `X-Content-Type-Options`, `Referrer-Policy`.
+- **D1 prepared statements само с bound параметри**; никога конкатениране на SQL чрез низове.
+
+**По въпроса за „hydration атаките“** — реалните класове и как този дизайн ги адресира:
+
+- **Изтичане между потребители през кеширан персонализиран SSR** — неприложимо в итерация 1 (без автентикация; всяка страница е анонимна и идентична → безопасна за кеширане). Остава предотвратено и по-късно, като се кешират *само* анонимни публични страници и никога автентикираните.
+- **Свръх-сериализиране на тайни в hydration payload-а** — смекчено чрез типизираните response DTO-та в `packages/api-contract` (отдават проекции на същностите, никога сурови редове) и чрез държане на код, носещ credentials, извън уеб/read пътя. DTO-тата да остават без вътрешни-само полета.
+- **XSS през сериализирано състояние / рендирани данни** — разчита се на авто-escaping-а на React/JSX; **никога `dangerouslySetInnerHTML`** върху текст от АОП (институция / компания / предмет са от външен източник); строгият CSP е резервната защита.
+
+**Отложено за по-късни фази** (повърхностите на по-широката платформа):
+
+- **Workflow-и за възложители/участници + админ** → Cloudflare Access (SSO + MFA) за админ; строго разделяне на публичен/персонализиран кеш; изолация на write пътя в отделен worker.
+- **AI асистент за обществени поръчки** → маршрутизиране през **AI Gateway** (лимити за rate/разход, кеширане, логване); read-only, параметризирани инструменти; заземяване на всяко твърдение в изчислени данни (риск от клевета); никакво кеширане; рендиране на изхода като текст. Спецификация — [`spec/ai-assistant.md`](../spec/ai-assistant.md).
+
+## Последствия / scaffold follow-up-и
+
+- Конфигуриране на пре-рендирането в `react-router.config.ts` (`prerender` пътища) за статичните информационни route-ове.
+- Задаване на `Cache-Control` (`s-maxage` + `stale-while-revalidate`) през `headers` export-ите на route-овете при SSR data route-овете; иницииране на cache purge (по URL/таг) в края на скрипта за зареждане на данните.
+- Задаване на CSP + security headers в `entry.server.tsx` (или Worker middleware) с per-request nonce върху ``.
+- Добавяне на Cloudflare rate-limit правило + WAF managed ruleset за explorer/export endpoint-ите (infra конфигурация, проследявана отделно).
+- Премахване или auth-gate на отворения `POST /etl/run` от скелета в `apps/etl` преди какъвто и да е деплой.
+
+## Свързани документи
+
+- [`etl.md`](../etl.md) — ETL pipeline-ът и емисията от ЦАИС ЕОП.
+- [`deploy.md`](../deploy.md) — деплой към Cloudflare и оперативна сигурност.
+- [`spec/ai-assistant.md`](../spec/ai-assistant.md) — спецификация на планирания AI асистент.
+- [`README.md`](../../README.md) на хранилището и [`AGENTS.md`](../../AGENTS.md) — общ преглед и работни конвенции.
diff --git a/docs/adr/0002-d1-as-datastore.md b/docs/adr/0002-d1-as-datastore.md
new file mode 100644
index 00000000..1678c308
--- /dev/null
+++ b/docs/adr/0002-d1-as-datastore.md
@@ -0,0 +1,29 @@
+# ADR-0002 — Cloudflare D1 като обслужващо хранилище
+
+- **Статус:** Прието
+- **Дата:** 2026-06-30 (записано със задна дата)
+- **Обхват:** Итерация 1 — обслужваната база за explorer-а и ETL-а.
+
+## Контекст
+
+Данните пристигат като **периодични bulk зареждания** от ЦАИС ЕОП, не като жива емисия; v1 е
+публичен, read-only слой без write път. Изпълнението е на **Cloudflare Workers** на edge.
+Търси се хранилище, което: е колокирано с Workers (нисък latency без отделен DB слой), има
+релационни/SQL семантики за класациите и join-овете, и е евтино за read-heavy публичен трафик.
+
+## Решение
+
+Ползваме **Cloudflare D1** (SQLite на edge) като единствено обслужвано хранилище — **един D1 на
+среда**, споделян от двата Worker-а: `sigma` чете, `sigma-etl` пише (виж [`deploy.md`](../deploy.md)).
+Схемата е **консолидиран baseline** ([`0000_init.sql`](../../packages/db/migrations/0000_init.sql)) плюс
+тънки добавъчни миграции (напр. `0001_flow_pairs_bidder_index.sql`), а не верига от самото начало —
+v1 е pre-production и всеки импорт стартира от свежа база; пълна верига инкрементални миграции се
+въвежда чак когато има деплойнати данни, които не може да се загубят.
+
+## Последствия
+
+- **D1 таксува прочетените редове, не върнатите** — това директно мотивира precompute слоя и
+ keyset пагинацията (виж [`v1-implementation-plan.md`](../v1-implementation-plan.md)).
+- FK се налагат от D1 → пълните ребилди трият в child→parent ред; `normalize-raw.sql` се изпълнява
+ като един атомарен batch (явен `BEGIN/COMMIT` се отхвърля), така че провален run се отменя.
+- Rollback на refresh се прави на ниво слот, не на ниво ред (виж [ADR-0005](0005-blue-green-d1-rollback.md)).
diff --git a/docs/adr/0003-value-flag-data-quality.md b/docs/adr/0003-value-flag-data-quality.md
new file mode 100644
index 00000000..0c98b1f8
--- /dev/null
+++ b/docs/adr/0003-value-flag-data-quality.md
@@ -0,0 +1,34 @@
+# ADR-0003 — Verdict за качество на данните (`value_flag` / `date_flag`) и единна стойностна база
+
+- **Статус:** Прието
+- **Дата:** 2026-06-30 (записано със задна дата)
+- **Обхват:** Итерация 1 — `contracts` и всички сумирани повърхности.
+
+## Контекст
+
+Изходните стойности от регистъра са шумни: подозрителни анекси, нереалистични суми, договори,
+подписани преди публикуване. На **министерски-видим** сайт мълчаливото сумиране на боклук е
+недопустимо, но и мълчаливото изтриване на редове крие данни. Нужен е **недеструктивен verdict**:
+редовете остават видими, но проблемните се изключват от тоталите по едно ясно правило.
+
+## Решение
+
+Всеки договор носи `value_flag` (`ok` | `review` | `value_low` | `value_suspect` | `annex_suspect`)
+и `date_flag` (`ok` | `signed_after_publication`). Каноничната сумирана колона е **`amount_eur`** —
+попълнена за **всичките пет** флага (`value_suspect` се repair-ва до процедурната оценка,
+`annex_suspect` пада към `signing`/`current`). Оттук — едно правило за всички суми:
+
+> Сумирай само `amount_eur` с предиката `amount_eur IS NOT NULL` — единната стойностна база за
+> rollup-ите и за страниците. `amount_eur` е `NULL` (и така изключен) само когато няма надеждна EUR
+> стойност: чуждовалутен ред без покрит ECB курс, `value_suspect` без процедурна оценка, или ред без
+> `signing`/`current` стойност.
+
+`home_totals.suspect` е отделен KPI — брой на `value_suspect` редовете, които сами се сумират — а не
+множеството, изключено от сумите. Пълната семантика е в [`core-scope.md`](../core-scope.md).
+
+## Последствия
+
+- Всяка нова заявка/страница, която сумира, трябва да ползва същия предикат — иначе rollup ↔ страница
+ се разминават. Това е merge-блокер по точност.
+- [`integrity-gate.md`](../integrity-gate.md) налага инварианта като hard assert на import/CI.
+- `annex_suspect` е частичен случай: брои се в `amount_eur`, но `current_value_eur` се потиска.
diff --git a/docs/adr/0004-style-src-unsafe-inline.md b/docs/adr/0004-style-src-unsafe-inline.md
new file mode 100644
index 00000000..002dd6a8
--- /dev/null
+++ b/docs/adr/0004-style-src-unsafe-inline.md
@@ -0,0 +1,26 @@
+# ADR-0004 — `style-src` запазва `'unsafe-inline'` (CSP)
+
+- **Статус:** Прието
+- **Дата:** 2026-06-30 (записано със задна дата)
+- **Обхват:** Итерация 1 — CSP в [`apps/web/app/lib/security.ts`](../../apps/web/app/lib/security.ts). Свързано с #11 (follow-up от security-audit PR #6).
+
+## Контекст
+
+`script-src` вече е строг: per-request nonce за некешираната SSR и per-script SHA-256 hash-ове за
+edge-кеширания HTML (`workers/app.ts`) — без `'unsafe-inline'`. `style-src` обаче пази
+`'unsafe-inline'`, защото приложението слага inline `style={…}` атрибути за динамични стойности.
+
+Ключов факт: **CSP nonce се прилага към `