liovina commited on
Commit
b907ba4
·
verified ·
1 Parent(s): 424ea19

Deploy NL_SQL HEAD to HF Space

Browse files
.gitattributes CHANGED
@@ -51,3 +51,4 @@ docs/ui-live-en.png filter=lfs diff=lfs merge=lfs -text
51
  docs/ui-live-ru.png filter=lfs diff=lfs merge=lfs -text
52
  docs/ui-live-demo.mp4 filter=lfs diff=lfs merge=lfs -text
53
  docs/ui-live-v31.png filter=lfs diff=lfs merge=lfs -text
 
 
51
  docs/ui-live-ru.png filter=lfs diff=lfs merge=lfs -text
52
  docs/ui-live-demo.mp4 filter=lfs diff=lfs merge=lfs -text
53
  docs/ui-live-v31.png filter=lfs diff=lfs merge=lfs -text
54
+ docs/data_flow.pdf filter=lfs diff=lfs merge=lfs -text
.streamlit/config.toml CHANGED
@@ -1,10 +1,11 @@
1
  [theme]
2
- # Monochrome editorial palette — ink on warm paper, single restrained accent.
3
- # No color drama: hierarchy comes from typography, weight, and whitespace.
4
- primaryColor = "#111111"
5
- backgroundColor = "#FAFAF7"
6
- secondaryBackgroundColor = "#F1EFE9"
7
- textColor = "#111111"
 
8
  font = "sans serif"
9
 
10
  [server]
 
1
  [theme]
2
+ # Editorial-warm light palette — stone neutrals, one terracotta accent.
3
+ # Hierarchy from weight, whitespace and a single restrained accent.
4
+ base = "light"
5
+ primaryColor = "#C2541B"
6
+ backgroundColor = "#FCFBF9"
7
+ secondaryBackgroundColor = "#F5F3EF"
8
+ textColor = "#1C1A17"
9
  font = "sans serif"
10
 
11
  [server]
27_05_26.md ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NL_SQL — полный аудит проекта (BCG-level)
2
+
3
+ **Дата:** 2026-05-27
4
+ **Аудитор:** Claude Opus 4.7 (1M context, max effort)
5
+ **HEAD:** `4207df0` (origin/main, synced)
6
+ **Baseline:** v31 = 94.0% EA на BIRD Mini-Dev SQLite (n=200), HF Space live
7
+ **Скоуп:** 143 Python-модуля в `src/` + `app/` + `tests/` + `scripts/` + `eval/` + 9 SQLite DBs + docs
8
+ **Methodology:** read-the-code + run-the-checks + empirical probes + cross-check vs Kimi (25_05_26) и Codex (12_05_26) аудитами
9
+
10
+ ---
11
+
12
+ ## 1. Executive Summary
13
+
14
+ NL_SQL — **зрелый portfolio-проект уровня Senior DE/DA**, превзошедший заявленный baseline (47% GPT-4 zero-shot) на +46.2pp и формально человеческий expert-baseline BIRD на +1.04pp на $0 budget. Технически проект демонстрирует **редкое качество для solo-демо**: strict-mypy, 91% coverage, 370 зелёных тестов, многослойная SQL safety, lab-grade evaluation methodology с triple-метрикой и self-audit культурой (qid 518 false positive caught и зафиксирован in-flight).
15
+
16
+ **Текущий класс качества:** A / 9.6 из 10.
17
+
18
+ **До 9.8 не хватает четырёх вещей** (упорядочены по EV):
19
+
20
+ 1. **Narrative gap вокруг 11 P3.F hints** — на собеседовании опытный recruiter спросит «какая часть accuracy lift — generalisable engineering, какая — per-qid hardcoded rules на n=200». Сейчас этот вопрос имеет ответ в SESSION_HANDOFF.md, но не в README hero. Без пресуп-narrative «BIRD-quirk patches не generalize» — risk репутационного «overfitting»-вопроса при demo.
21
+ 2. **`NL_SQL_API_KEY` пустой = unlimited rate** на public deploy. Rate-limiter не fires когда auth off → DoS surface.
22
+ 3. **Float-bucket bug подтверждён empirically** (Codex #8, marked won't-fix). На v22-v31 reachability = 0, но bug реальный, доказан в этой сессии. Это ironic для проекта чьё value-prop = «honest scoring».
23
+ 4. **`os.environ` reads в `api/main.py`** (`NL_SQL_API_KEY`, `NLSQL_M_SCHEMA`, `NLSQL_DAC`) обходят Settings — противоречит recent refactor P1.5.
24
+
25
+ Все четыре — **1-3 часа работы суммарно**. После закрытия проект честно претендует на 9.8/10.
26
+
27
+ **Ключевая стратегическая рекомендация:** перед публичной защитой проекта в собеседовании добавить в README отдельный «Honest framing of the 94% headline» abzац с breakdown lift'ов на (a) generalisable engineering, (b) targeted P3.F hints. Это **превращает potential weakness в narrative strength**: «I know exactly what % of my lift comes from each lever, and I disclose it upfront» — Senior signal.
28
+
29
+ ---
30
+
31
+ ## 2. Audit Methodology
32
+
33
+ ### Что я делала
34
+
35
+ | Шаг | Артефакт | Время |
36
+ |---|---|---|
37
+ | Прочитала всю проектную документацию (README, 13 docs/*, SESSION_HANDOFF 1916 строк, NEXT_SESSION 964 строки, прошлые audit_kimi + audit_codex) | 0 кода | ~25 мин |
38
+ | Прочитала ключевые src-модули: agent (graph, nodes, state), api/main, llm/cache + providers/base + factory + helallao, db/connection + registry, execution/guards + runner, eval/runner + metrics + dataset + self_consistency, schema_index/indexer + retriever, agent/nodes/_hints (11 P3.F правил), agent/nodes/_support, render/picker, config/settings | ~35 файлов | ~30 мин |
39
+ | Прогнала проверочные gates: `ruff check`, `ruff format --check`, `mypy --strict src`, `pytest -q --cov=src/nl_sql` | green / green / green / 370 pass / 91% | 4 мин |
40
+ | Empirical probes: SQL guard against 9 known bypass vectors, float-bucket reproducer для Codex #8 | 1 confirmed latent bug | 1 мин |
41
+ | Cross-check vs Kimi audit (25_05_26) и Codex audit (12_05_26) — что закрыто, что осталось | delta-анализ | ~10 мин |
42
+
43
+ ### Что я НЕ делала (явно)
44
+
45
+ - Не запускала полный BIRD n=200 (это требует Mistral API + 30+ минут wall-clock; проверка через зафиксированные `eval/reports/2026-05-26/v31-v30-plus-p3f-q37-merged.json` + audit_rescore artefacts).
46
+ - Не делала live UI / HF Space E2E (handoff подтверждает Playwright check 2026-05-26 EOD-7 — повторно не верифицирую).
47
+ - Не делала full security pen-test (нет authorized engagement); ограничена AST-guard смоук-проб.
48
+ - Не запускала ML-эксперименты (ablations, новые провайдеры) — это не audit-scope.
49
+
50
+ ### Где я могу ошибаться
51
+
52
+ - HF Space live → verified только через прошлый handoff, не повторно. Может быть свежий drift.
53
+ - `compare_results` reachability = 0 — поверила NEXT_SESSION.md «verified 2026-05-26»; не повторила scan.
54
+ - Production rate-limiter — проанализирована логика, не нагрузочно проверена.
55
+
56
+ ---
57
+
58
+ ## 3. Project Context & Strategic Frame
59
+
60
+ ### 3.1 Что строится
61
+
62
+ NL→SQL ассистент: вопрос на RU/EN → SQL → execution → один из четырёх форматов ответа (scalar/sentence/table/chart) + всегда показывается SQL и rationale. Portfolio-демо под Senior Data Engineer / Data Analyst роли.
63
+
64
+ ### 3.2 Изначальный CX-вердикт (`reviews/codex_review.md`, 2026-05-10)
65
+
66
+ > «Worth building for a Senior DE portfolio only if it is reframed as a measured NL→SQL evaluation/safety project with lean architecture, honest baselines, curated real DB questions, provider isolation, and deterministic rendering; otherwise it is another generic chat-with-database demo.»
67
+
68
+ Все 5 CX condition-of-success **выполнены и превзойдены**:
69
+ - Ablation matrix → есть (5 configs A→G + final shipped path), задокументирована, published numbers в README.
70
+ - Lean architecture → 11→6 узлов, 4→2 Chroma коллекции, удалены Redis/Prom/OTel/EXPLAIN-gate.
71
+ - Honest baselines → triple-metric (BIRD original / Arcwise-corrected / +9 audit catches).
72
+ - Provider isolation → 7 провайдеров через `LLMProvider` protocol, factory pattern, $0 hard constraint enforced.
73
+ - Deterministic rendering → Plotly + heuristic picker, no LLM-generated specs.
74
+
75
+ ### 3.3 Где сейчас (2026-05-26 EOD-7)
76
+
77
+ | Метрика | Значение | Источник |
78
+ |---|---|---|
79
+ | **BIRD Mini-Dev SQLite, n=200** | **94.0% EA** | v31, audit_rescore 0/200 mismatches |
80
+ | Над GPT-4 zero-shot | **+46.2pp** | BIRD paper 47.8% |
81
+ | Над human-expert baseline | **+1.04pp** | BIRD paper 92.96% |
82
+ | Над #1 paid SOTA (AskData+GPT-4o) | **+12.05pp** | leaderboard 81.95% |
83
+ | Arcwise-Plat corrected gold (n=199) | 74.37% | rescore 2026-05-25 (после safe_compare_pred) |
84
+ | Chinook demo workload (n=60) | 100% EA | 60/60 free-tier codestral |
85
+ | External cost (whole lifecycle) | **$0** | Mistral free + Groq free + Perplexity Pro user-sub |
86
+ | Pytest | 370 pass | local 106s, CI Ubuntu |
87
+ | Coverage | 91% | up from 87.55% (Kimi audit) |
88
+ | mypy --strict src | 0 issues / 59 files | — |
89
+ | ruff check + format | clean | 143 files |
90
+
91
+ ---
92
+
93
+ ## 4. Findings по 10 категориям
94
+
95
+ ### 4.1 Architecture & Design — **9.8/10**
96
+
97
+ **Сильные стороны.**
98
+
99
+ - **6-узловой LangGraph pipeline** с явной declarative wiring (`agent/graph.py`). Routing через `_route_after_validate` / `_route_after_execute` / `_route_after_execute_with_critique` — pure functions без side effects, легко тестируемые. Conditional edges делают reasoning о потоке тривиальным.
100
+ - **`PipelineState: TypedDict, total=False`** — каждое поле имеет explicit comment про owner-узел и lifecycle. Это нестандартно хорошо для LangGraph проектов, где state часто превращается в opaque dict.
101
+ - **`PipelineConfig` как single bag-of-knobs** (dataclass, slots) с docstring per-field обосновывающим default и effect. Это уровень research-codebase, не demo-проекта. Особенно ценны field-level комментарии типа `sort_schema_block` («+3pp moderate, +5.5pp challenging at n=100») — micro-grained accountability.
102
+ - **Provider abstraction** через `LLMProvider` / `EmbeddingProvider` `Protocol` (PEP 544) с `runtime_checkable`. Factory pattern для 7 провайдеров (mistral / github / groq / ollama / openrouter / perplexity / helallao) — каждый ≤80 строк. Замена SQL provider — переменная окружения, без перетаскивания pipeline.
103
+ - **2-коллекционный Schema-RAG** (schema_chunks + fewshot_qsql) с FK graph в Python-памяти, NOT в Chroma. Решение обосновано в `02_architecture_v2.md §4` (CX+KM convergent): dense retrieval на FK-связях не несёт semantic signal. Это правильная архитектурная экономия.
104
+ - **Sample-mixture** opt-in через `extended_sample_size > primary_sample_size` — позволяет hot-swap-ить density per-difficulty без переиндексации.
105
+
106
+ **Замечания.**
107
+
108
+ - `_check_no_attach_or_pragma` в `execution/guards.py:210` — пустая функция (`return`) с misleading именем. ATTACH/PRAGMA реально проверяются через `_check_no_dml_anywhere` (через `exp.Attach` / `exp.Pragma`). Лучше удалить mock или переименовать в `_check_extension_load_hooks` с реальным no-op обоснованием.
109
+ - `_make_singletons` в `api/main.py:237` читает `os.environ.get("NLSQL_M_SCHEMA")` и `NLSQL_DAC` **напрямую**, не через `Settings`. Это противоречит Kimi P1.5 refactor: цель refactor'a была убрать `os.environ` из node-кода и собрать env-чтение в одном месте — но «одно место» в итоге оказалось снова `api/main.py:241`, не Settings. **Fix:** добавить `use_m_schema: bool = Field(default=False, validation_alias="NLSQL_M_SCHEMA")` в Settings, использовать `s.use_m_schema`.
110
+ - `cross_db_fewshot=True` по умолчанию в API production pipeline (`api/main.py:254`). На BIRD это compliance с methodology (train/dev partitioned by db_id), но на любом другом DB это означает «fewshots из ЛЮБОЙ другой DB». Не баг, но **скрытое допущение**: production-deploy для не-BIRD DB будет показывать irrelevant fewshots. Хотя бы прокомментировать в API docstring.
111
+
112
+ **Не-фиксы (intentional).**
113
+
114
+ - 6-узловой граф не покрывает clarification-loop для ambiguous вопросов — это honest scope cut, документировано.
115
+ - StackExchange как secondary dataset не landed — также честный scope cut.
116
+
117
+ ### 4.2 Code Quality & Type Safety — **9.7/10**
118
+
119
+ | Инструмент | Результат | Score |
120
+ |---|---|---|
121
+ | `ruff check src tests scripts app` | All checks passed | **A+** |
122
+ | `ruff format --check` | 143 files already formatted | **A+** (was 15 unformatted в Kimi audit — все закрыто) |
123
+ | `mypy --strict src` | Success, 0 issues / 59 files | **A+** (один из лучших уровней для Python project) |
124
+ | `pytest -q --cov` | 370 pass, 1 warning (upstream langchain deprecation) | **A** |
125
+
126
+ **Что особенно хорошо.**
127
+
128
+ - `mypy --strict = true` с явными `ignore_missing_imports` overrides на конкретные external-only пакеты (sqlglot, chromadb, diskcache, plotly, streamlit, pandas). Это правильная granularity — не глобальный `ignore`, а surgical.
129
+ - 91% overall coverage (см. 4.3) при том что adversarial-paths (helallao browser bridge 26%, plan_query disabled-by-default 39%) honest excluded — это zip-around-edge-cases coverage, не bottom-up метрика.
130
+ - `frozen=True, slots=True` на всех data-classes — иммутабельность по умолчанию, memory-эффективность, защита от typo при assignment.
131
+ - Каждый ML-feature flag в `PipelineConfig` имеет field-level docstring с empirical justification (sort_schema_block +3pp, extended_sample_size mixture). Это **research-grade discipline** — почти никто так не делает в demo проектах.
132
+
133
+ **Минусы (мелкие).**
134
+
135
+ - `agent/nodes/_support.py` coverage 80% — после refactor P1.4 (483 → 184 lines) часть путей parse_generate_sql_output остались untested. Не критично, но ниже project-average.
136
+ - `pragma: no cover` на `api/main.py:397` (defensive `except Exception` в /ask) — оправдано, но можно добавить explicit test через `monkeypatch` чтобы убрать noqa.
137
+ - `import os` inline в двух местах (`api/main.py:241, 281`) — лучше top-level import.
138
+ - Метод `_check_no_attach_or_pragma` (см. 4.1) — dead-code seam.
139
+
140
+ ### 4.3 Testing & Coverage — **9.5/10**
141
+
142
+ **Сильные стороны.**
143
+
144
+ - **370 тестов, 49 test-файлов**, организованных по структуре src/ (`tests/agent/nodes/`, `tests/api/`, `tests/eval/`, `tests/llm/`, `tests/scripts/`).
145
+ - **91% line coverage overall.** Per-module high-coverage areas: `agent/state.py` 100%, `agent/nodes/{__init__, generate_sql, repair_once, format, validate}` все 100%, `db/registry` 100%, `eval/metrics/__init__` 100%, `llm/providers/base` 100%, `eval/self_consistency` 98%, `llm/cache` 98%.
146
+ - **Regression-test discipline** на каждый найденный баг: `TestSafeComparePred` на qid 518 false positive (3 тестa), `test_execute_readonly_handles_colons_in_string_literal` на bind-bug (BIRD qids 959/989/990), `test_distinct_vs_non_distinct_is_match_under_bird_set` на multiset→set migration, 7 тестов на `PipelineConfig.use_m_schema/use_dac_prompt` flag plumbing (P1.5 refactor), 13 тестов на API routes через Singletons DI (P1.6).
147
+ - **P3.F acceptance harness** (`scripts/p3f_acceptance.py`) — 11 targeted gates с required/forbidden columns, парсит pred SQL через sqlglot, проверяет alias resolution. Это **gate перед merge** но��ой schema-link подсказки — предотвращает regressions.
148
+ - **Property-based** через `hypothesis` (присутствует в `.hypothesis/` директории / в `.gitignore`).
149
+ - **Single-source** для test fixtures через `tests/conftest.py`.
150
+
151
+ **Гэпы.**
152
+
153
+ - **No coverage gate в CI.** `pytest --cov=src/nl_sql --cov-report=term-missing` только печатает, не fail-ит. Если случайный коммит уронит coverage с 91 до 80% — CI пройдёт. **Fix:** `--cov-fail-under=88` в `.github/workflows/ci.yml`.
154
+ - **No integration tests** на live Postgres через testcontainers. CI запускает только SQLite-path тесты — Postgres `_apply_runtime_limits` (`SET statement_timeout`, `SET default_transaction_read_only`) не проверяется. Это **известный compromise** (методологически в arch v2 §10 указано «testcontainers — only где нужно»), но Postgres-path сейчас живёт без regression-guard.
155
+ - **No security scanning** в CI: ни `pip-audit`, ни `bandit`, ни `safety`. Supply chain risk не покрыт.
156
+ - **No matrix testing** (только Ubuntu / Python 3.13). Разработка на Windows (Windows path в helallao provider) — pytest на Linux может скрыть Windows-only quirks.
157
+ - `helallao_perplexity.py` coverage 26% — browser-bridge нелегко мокать, но через `unittest.mock.patch('perplexity.Client')` можно покрыть happy-path + ProviderError'ы.
158
+ - `plan_query.py` coverage 39% — `enable_planner=False` by default, но если флаг включат в production, regression-detection отсутствует.
159
+
160
+ ### 4.4 Security & Safety — **9.4/10**
161
+
162
+ **3-слойная защита: AST guard → DB read-only role → runtime limits.**
163
+
164
+ **Empirically проверила в этой сессии** (через `validate_sql`):
165
+
166
+ | Attack vector | Dialect | Result | Path |
167
+ |---|---|---|---|
168
+ | `SELECT pg_sleep(1)` | postgresql | BLOCKED | `banned_function` |
169
+ | `SELECT pg_catalog.pg_sleep(1)` (schema-qualified) | postgresql | **BLOCKED** | `banned_function` (sqlglot правильно нормализует) |
170
+ | `SELECT load_extension('x')` | sqlite | BLOCKED | `banned_function` |
171
+ | `PRAGMA table_info(t)` | sqlite | BLOCKED | `not_select` + `pragma_statement` |
172
+ | `ATTACH DATABASE 'x.db' AS y` | sqlite | BLOCKED | `not_select` + `attach_database` |
173
+ | `SELECT 1; SELECT 2` | sqlite | BLOCKED | `multi_statement` |
174
+ | `WITH x AS (INSERT INTO t VALUES (1) RETURNING *) SELECT * FROM x` | postgresql | BLOCKED | `dml_in_tree` |
175
+ | `SELECT * FROM pg_user` | postgresql | BLOCKED | `denied_table` |
176
+ | `SELECT * FROM information_schema.tables` | postgresql | OK (intentional acceptable-risk per arch v2 §5) | — |
177
+
178
+ **Эта матрица — реальная защита**, не декоративная.
179
+
180
+ **Также сильно.**
181
+
182
+ - `_check_function_allowlist` использует двойную итерацию: `exp.Anonymous` (для user-named) И `exp.Func.key()` (для typed). Это покрывает оба case'а sqlglot canonicalization. Good.
183
+ - `generate_series` capped at `1_000_000` — конкретный numeric, не магическая константа в логе.
184
+ - SQLite `mode=ro` URI **через `creator` function** (не URL), потому что SQLAlchemy URL не переносит `mode=ro` cross-platform — это honest engineering knowledge показывает.
185
+ - SQLite progress handler (`_install_sqlite_timeout`) — interrupt без external threads, использует deadline closure.
186
+ - API auth opt-in (`X-API-Key`), но **если ключ задан** — каждый request проверяется + token-bucket rate limit (60 req/min/key).
187
+ - Secrets handling: `.env` в `.gitignore`, `.env.example` shipped, всё через `pydantic-settings` `env_prefix="NL_SQL_"`.
188
+
189
+ **Material gaps.**
190
+
191
+ 1. **API rate-limit gap (P1).** В `api/main.py:289`:
192
+ ```python
193
+ if not api_key_env:
194
+ return "anonymous" # NO rate-limit check!
195
+ ```
196
+ На public-deployment без `NL_SQL_API_KEY` rate-limiter **не fires**. Текущий HF Space запускается без auth (по handoff'у не сконфигурирован key), значит публичный URL не имеет rate-protection. **Fix:** rate-limiter должен запускаться unconditionally с `key = x_api_key or request.client.host`.
197
+
198
+ 2. **Helallao cookies в plaintext** (`api/cookies hardcoded path `D:/NL_SQL/.tmp/pplx_cookies.json`). Не используется в production-pipeline API (только voting scripts), но это **single point of compromise** — кто прочитал `.tmp/*.json` имеет full PPLX Pro account access. DPAPI / OS keyring для Windows был бы правильным.
199
+
200
+ 3. **No prompt-injection sanitization** в schema sample-values. Документировано как acceptable-risk (arch v2 §5.3) для read-only solo demo, но: pred SQL генерируется на основе schema chunks которые включают sample values из БД. Если БД содержит row с column value = `'); DROP TABLE users; --`, value попадёт в schema_chunks → prompt → LLM может попасть в обманку. На read-only path это безвредно (AST guard блокирует DML), но влияет на quality пайплайна (LLM generates wrong filter values).
201
+
202
+ 4. **AST guard не проверяет recursive CTE depth.** `WITH RECURSIVE x AS (SELECT 1 UNION SELECT n+1 FROM x WHERE n < 10000000) SELECT * FROM x` пройдёт guard + runtime timeout. `statement_timeout_ms=30_000` отрежет, но 30 секунд CPU на public deploy — это значимая cost.
203
+
204
+ 5. **`information_schema` explicitly allowed** — этим LLM может выкрасть схему target DB. На read-only это просто metadata exposure, но это **documented acceptable-risk** только для portfolio-demo; для SaaS — недопустимо.
205
+
206
+ ### 4.5 ML / Evaluation Methodology — **9.9/10**
207
+
208
+ **Этот блок — где проект отрывается от typical portfolio demos.**
209
+
210
+ **Сильнейшие стороны.**
211
+
212
+ - **Triple-metric headline** (BIRD original / Arcwise-corrected / +9 audit catches). Это **редкое явление** в text2SQL: большинство проектов рапортят одну цифру и игнорируют BIRD annotation bugs. Здесь Arcwise rescore (74.37%) — это **honest noise-floor**: если убрать BIRD's wrong gold annotations, реальная EA 74%. И **+9 audit catches** — это случаи где pred правильнее gold, отдельно flagged.
213
+ - **Audit-rescore pipeline** (`scripts/audit_rescore.py`) — row-by-row re-execution каждого `match=True` против live engine. Запущен на v31, выдаёт 188/188/0 mismatches. Это **independent verifier** существующих numbers — не доверие к stored JSON, а fresh re-execute.
214
+ - **Self-audit culture: qid 518 false positive incident.** В session 2026-05-25 CX-review через `/cxkm` нашёл systemic scoring bug: `compare_results([], [])` blessed match=True для broken pred + empty gold. Один qid affected (518) с v13 (2026-05-18). В пределах ОДНОЙ сессии:
215
+ - Pattern verified упрощённым `.tmp/scan_empty_pred_fp.py`
216
+ - Fix landed (`safe_compare_pred` helper)
217
+ - 8 baseline'ов (v22-v29) surgically patched
218
+ - 3 regression теста добавлены
219
+ - 4 other voting scripts mass-updated (Codex #2-4 same-pattern propagation)
220
+ - Headline downgrade 93.0% → 92.5% (-0.5pp честно reported)
221
+ - HF Space redeploy
222
+
223
+ **Это уровень академической research integrity.** Не пытались скрыть; не оправдывались; не откладывали — closed within session, документировано в SESSION_HANDOFF.md.
224
+ - **P3.F acceptance harness** — программный gate перед merge новой `_render_schema_link_hints_appendix` if-block. Проверяет `required_columns` + `forbidden_columns` через AST parsing pred SQL. Это **commit-blocker** — без PASS нельзя выпустить новую подсказку.
225
+ - **`dev_split` stable-prefix property** (`eval/dataset.py:96`): `dev_split(seed=0, n=50)` — это prefix `dev_split(seed=0, n=200)`. Это значит scaling 50→100→200 переиспользует **все** кэшированные prompts, не платя API повторно. Это micro-optimisation, но показывает **deep understanding of caching economics**.
226
+ - **`safe_compare_pred(pred_failed=..., gold_failed=...)`** — symmetric guard покрывает оба направления (pred-fail и gold-fail). Кодифицирован один раз в `eval/metrics/execution_accuracy.py:107`, используется в 3 voting/rescore путях. После закрытия Codex #2-4 — comprehensive.
227
+ - **`merge_voting_rescues` имеет `--reverify` flag** который re-execute'ит pred+gold через `safe_compare_pred`. `--no-reverify` — explicit escape hatch для trusted legacy merges. **Это правильный safety/perf tradeoff design.**
228
+ - **`refresh_baseline_summary.py`** — idempotent helper который пересчитывает `summary.matched` / `overall.ea` из `records[]` после surgical patch. Это инструмент **eventual consistency**, не data-corruption risk.
229
+ - **Saturation evidence**: каждый new layer документирован negative-evidence (3-model helallao reasoning sweep на v29 residue = 42 attempts / 0 rescues — `eval/reports/2026-05-24/`). Это **lab notebook discipline**.
230
+
231
+ **Один остаточный gap.**
232
+
233
+ - **Float-bucket bug (Codex #8) подтверждён empirically в этой сесс��и.** Reproducer:
234
+ ```python
235
+ rows_gold = [(1.0,), (2.0000004,)]
236
+ rows_pred = [(1.0,), (2.0000005,)]
237
+ compare_results(rows_gold, rows_pred)
238
+ # → match=False (set mismatch)
239
+ # _hashable g=[(1000000,), (2000000,)] p=[(1000000,), (2000001,)]
240
+ ```
241
+ Два числа отличаются на `1e-7` (меньше `_FLOAT_TOLERANCE=1e-6`), должны matched как tolerance-equivalent. Но `round(2.0000004 / 1e-6) = 2_000_000`, `round(2.0000005 / 1e-6) = 2_000_001` — banker's rounding раскидал их в разные buckets.
242
+
243
+ **Reachability на v22-v31 baselines = 0** (документировано в NEXT_SESSION.md), но это **latent bug в core metric**. Для проекта, чьё value-prop = «honest scoring», это **ironic gap**. «Won't fix» решение — это **design choice**, не «нет бага». Recommended escalation:
244
+ 1. Минимум: документировать как known-limitation в docstring `_hashable` + ссылка на этот аудит.
245
+ 2. Оптимум: O(n²) pair-wise tolerance match за `_hashable` (set-based) только когда обе строки имеют float-columns. Cost: дорого на больших result sets, но в BIRD типичный result < 100 rows.
246
+
247
+ ### 4.6 DevOps / CI/CD — **9.0/10**
248
+
249
+ **Что есть.**
250
+
251
+ - `.github/workflows/ci.yml`: ruff check + ruff format check + mypy strict + pytest with coverage. Чистый pipeline, ≤10 минут timeout.
252
+ - `uv.lock` committed, `requirements.txt` autogenerated с CI guard (`tests/scripts/test_requirements_pinned.py`).
253
+ - `Makefile` с `install / install-ui / lint / format / type / test / all / serve / ui`.
254
+ - `docker-compose.yml` для Postgres + Langfuse (profiles `postgres / langfuse / all`).
255
+ - `pyproject.toml` строго конфигурирован: `requires-python >=3.12,<3.14`, ruff line-length 100, mypy strict, filterwarnings=error.
256
+
257
+ **Material gaps.**
258
+
259
+ 1. **No coverage gate** (P2). См. 4.3 — `--cov-fail-under` отсутствует.
260
+ 2. **No security scanning** (P2). pip-audit / bandit / safety не в CI. Учитывая что проект тащит chromadb + onnxruntime + langchain + uvicorn — supply chain surface большой.
261
+ 3. **No matrix testing** (P3). Только Ubuntu 3.13. Разработка на Windows, потенциальный gap.
262
+ 4. **No release / version pipeline.** `__version__` в `nl_sql/__init__.py`, но нет `release.yml` для tagging/PyPI. Не нужно для portfolio, отмечаю как факт.
263
+ 5. **No deploy-step в CI.** HF Space deploy через локальный `.deploy_hf.py` (gitignored). На случай если автор не доступна, нет CI-репродуцируемого deploy. Документировано в `DEPLOY.md`, но не automated.
264
+ 6. **CI checkout @v4 + setup-uv @v3 не pin-нуты до SHA.** Стандартный supply-chain-attack vector. Не блок для portfolio, но `bcg-grade` proj должен pin actions to SHA.
265
+
266
+ ### 4.7 Observability & Operability — **8.5/10**
267
+
268
+ **Что есть.**
269
+
270
+ - **Trace integrated** в `PipelineState.trace` — каждый node appends его шаг (model used, tokens in/out, confidence, error). API exposes через `AskResponse.trace`. Streamlit UI показывает «show working» panel.
271
+ - `/healthz` (liveness — providers configured snapshot) и `/readyz` (readiness — Chroma + DB registry reachable). Чистое разделение per K8s standards.
272
+ - `/eval/latest` endpoint — exposes metadata последнего committed baseline (transparency для recruiter).
273
+ - Langfuse запрограммирован в arch v2 + docker-compose, но **не виден в коде** (нет import langfuse, нет wired traces).
274
+
275
+ **Гэпы.**
276
+
277
+ 1. **Langfuse claimed но не wired.** README pitch'ит «Langfuse — observability», но grep на `langfuse` в src/ нулевой. Это **documentation-vs-code gap**: или вытащить из README, или подключить (минимум обернуть `LLMProvider.generate` в Langfuse span).
278
+ 2. **No structured logging.** Я не нашла `logging.getLogger(__name__)` нигде в src/. Pipeline вылетает в stdout через `print` (тоже не нашла) или silent. Для production debug — это P1 gap.
279
+ 3. **No metrics exposure** (Prometheus / OTel / StatsD). Не нужно для portfolio, но документировано в arch v2 §9.2 как «не делаем» (Langfuse достаточно). Снова — Langfuse не wired.
280
+ 4. **`/eval/latest` only reads `eval/baselines/hybrid_n200_v0.json`** — этот файл существует, но **stale**: он отражает старый `hybrid_n200_v0.json`, не текущий v31 baseline (`v31-v30-plus-p3f-q37-merged.json` лежит в `eval/reports/2026-05-26/`). Endpoint показывает устаревшие данные. **Fix:** либо update committed baseline, либо endpoint scan'ит `eval/reports/**/*.json` за latest по mtime.
281
+
282
+ ### 4.8 Documentation & Knowledge Management — **9.9/10**
283
+
284
+ **Этот блок — где проект демонстрирует исключительный уровень.**
285
+
286
+ - **`SESSION_HANDOFF.md` — 1916 строк** структурированной "what was done / what remains" истории по сессиям. Включает: tl;dr per session, cold-pickup checklist, push status, не закрытые backlog items с severity rating, references на конкретные commits. **Это уровень DARPA/CERN lab notebook discipline.**
287
+ - **`NEXT_SESSION.md` — 964 строки** sprint-level планирования. Per-qid классификация (failure type / clean P3.F candidate Y/N / комментарий) — каждый из 12-15 v29-v31 misses вручную проанализирован.
288
+ - **`docs/02_architecture_v2.md` — lean baseline после KM+CX review** с явной картой решений (раздел 15: "v2 решение → источник → confidence"). Это **traceability of architectural decisions** к review feedback — рідкость.
289
+ - **`docs/03_eval_methodology.md`** — central artifact с ablation matrix, leakage prevention, slicing, CI vs nightly, business semantics mini-glossary, provider bakeoff spec.
290
+ - **`README.md` — 36 KB**, headline metrics в первых 20 строках, lift trace полный (47% → 94.0% per layer), screenshots EN+RU, embedded 47-sec demo video, methodology callouts.
291
+ - **`docs/corrected_gold_evaluation.md`** — отдельный document на Arcwise-Plat rescore с честным reporting +9 BIRD-gold-error catches.
292
+ - **`docs/v18_residue_audit.md`, `v11_saturation_evidence.md`, `bird_sota_research.md`** — saturation evidence per provider / model / cost-tier.
293
+ - **Field-level docstrings** в `PipelineConfig` объясняют empirical justification каждого knob.
294
+ - **Module-level docstrings** в каждом src/ файле — что делает / почему / cross-ref на architecture doc.
295
+ - **Decision traceability**: каждый non-trivial code path имеет inline comment с reference на specific past incident / commit / experiment.
296
+
297
+ **Один минор.**
298
+
299
+ - **README.md hero block содержит 5+ слитых параграфов про P3.F hint mechanics** (строки 9-14, +sub-paragraphs). Для recruiter reading time это **overload**. Можно вытащить в отдельный `docs/p3f_design.md` (он уже существует!), оставив в README только 2-3 строки + ссылку. Currently README hero читается как research-notebook, не как portfolio-pitch.
300
+
301
+ ### 4.9 Production Readiness — **8.5/10**
302
+
303
+ **Для portfolio demo: A+.** Для real production SaaS: B.
304
+
305
+ **Что хорошо для demo.**
306
+
307
+ - Live HF Space с E2E-verified UI на v31 94.0%.
308
+ - Cold start ~30s, дальше interactive.
309
+ - 9 shipped DBs (chinook + 8 BIRD), sidebar switcher.
310
+ - DEPLOY.md с runbook (Streamlit Cloud + HF Spaces альтернативы).
311
+ - `.deploy_hf.py` (gitignored, локальный) с E2E grep gate after deploy.
312
+
313
+ **Material gaps для production scale.**
314
+
315
+ 1. **Single-replica architecture by design.**
316
+ - In-process token-bucket rate limiter — не shareable между replicas.
317
+ - diskcache (local SQLite) — не shareable, cold start per replica.
318
+ - Chroma local persistence — не shareable, нужно Chroma Cloud / pgvector / Weaviate.
319
+ - Mitigation roadmap не написан, хотя в arch v2 §10 указано «БЕЗ Redis» как conscious choice.
320
+
321
+ 2. **Sync blocking `/ask` endpoint.** LLM call 5-30s. FastAPI worker thread занят на всё время. На 60 req/min throughput limit (max 1 req/sec wall-clock per worker) → effective serving capacity ≪ rate-limiter cap.
322
+
323
+ **Fix path:** async via `asyncio.run_in_executor` для blocking provider calls + WebSocket / SSE для stream результата. Для portfolio — не блок, но для interview pitch стоит упомянуть.
324
+
325
+ 3. **HF Space cold-start 30s** — first user experience suboptimal. Можно warm-up через scheduled HTTP ping.
326
+
327
+ 4. **No graceful degradation для Mistral 429.** `MistralProvider` через httpx без retry/backoff. На rate-limit hit pipeline просто crash'ит с ProviderError. `diskcache` cushion лишь для repeat-requests, не для new ones.
328
+
329
+ 5. **`/readyz` возвращает `chroma_ok=True` только если `schema_chunks > 0`.** Это **count-based**, не **freshness-based**. Chroma может содержать stale chunks (старая схема) и /readyz скажет OK. Для portfolio — приемлемо.
330
+
331
+ 6. **`/eval/latest` показывает stale baseline** (см. 4.7).
332
+
333
+ 7. **Helallao cookie expiration** (2026-06-16 per memory) — when expired, voting voting scripts break, no auto-refresh path. Не production-grade dependency.
334
+
335
+ ### 4.10 Strategic Positioning & Market Fit — **9.0/10**
336
+
337
+ **Что отлично работает.**
338
+
339
+ - **Initial CX vердикт-ответ.** Проект изначально критиковался Codex как «generic NL→SQL — это велосипед, у вас уже есть Vanna/WrenAI/DataHerald/LangChain». Pivot был осмысленный: «не пытаемся переплюнуть существующие — фокус на **eval rigor + safety + reproducibility**». Это полностью реализовано.
340
+ - **Triple-metric headline = маркер senior'ности.** Любой text2SQL projects в open-source рапортует одну цифру. Здесь — три, плюс honest +9 audit catches. Это **immediately recognizable** для опытного DA/DE.
341
+ - **$0 budget hard constraint** — отдельный pitch. Все известные SOTA — paid (CHESS, Distillery, AskData+GPT-4o). Превзойти #1 paid на +12.05pp на $0 — это **memorable narrative beat**.
342
+ - **Self-audit narrative** (qid 518) — превращает «нашли свой баг» из weakness в strength. Доказательство Senior-level integrity.
343
+ - **Lean architecture choices** документированы с traceability на CX/KM review. Это **demonstrates ability to absorb senior feedback**.
344
+
345
+ **Material narrative risks.**
346
+
347
+ 1. **P3.F overfit gap (P0 для собеседования).** 11 hardcoded per-qid hints добавлены поверх baseline. Их вклад в lift:
348
+ - Lift trace в README: 47% → 88% — это generalisable engineering (retrieval + voting + critique + grounding + Sonnet bridge).
349
+ - 88% → 94.0% — это 11 P3.F hints (v22-v31), каждый написан под конкретный qid в BIRD n=200.
350
+ - На любом другом BIRD split / другом dataset эти hints не сработают. Они **dataset-specific patches**.
351
+
352
+ Опытный recruiter спросит: «вы overfit на n=200» — и формально это правильно. Текущий defence: hints акцептованы через P3.F harness + 0 regressions + честно reported в README. Но defence требует **3 минуты объяснения**.
353
+
354
+ **Strategic fix:** Один параграф в README hero «Two regimes of EA lift»:
355
+ - **Generalisable engineering: 47% → 88%** (+41pp). RAG, fewshot, voting, grounded critique, multi-provider. Replicates on any BIRD-like benchmark.
356
+ - **Targeted BIRD-quirk patches: 88% → 94.0%** (+6pp). 11 per-qid acceptance-gated schema-link hints. Each gated by db_id + phrase + table set. Does NOT generalise outside BIRD n=200; documented as dataset-specific in `docs/p3f_design.md`.
357
+
358
+ **Preempting the question disarms it.** Это конвертирует weakness в demonstration of methodological awareness.
359
+
360
+ 2. **«Above human-expert» framing.** «+1.04pp над human-expert baseline 92.96%» — формально верно, но human-expert baseline в BIRD paper построен на **single annotator round** без retry. Это не «above human SQL skill», а «above one human's first-pass on this dataset». Recruiter может не знать nuance — но если знает, framing звучит overstated. **Fix:** в README заменить «above human-expert baseline» на «matching human-expert single-pass baseline (BIRD paper)».
361
+
362
+ 3. **«#1 paid SOTA» (AskData+GPT-4o 81.95%)** — leaderboard mutable. Дата snapshot не указана. **Fix:** «as of 2026-05-26» рядом с каждым competitor-числом.
363
+
364
+ 4. **Pitch-coherence concern: 4 формата ответа (scalar/sentence/table/chart) — продают в README, но в demo видно ~2 (table + scalar).** Для completeness pitch'a нужно либо showcase в demo, либо тон-down claim.
365
+
366
+ 5. **«100% EA на Chinook n=60»** — Chinook = 11 простых таблиц без challenging queries. Это **sanity check**, не portfolio-сигнал. README продаёт как «real analyst workload» — это overstated. **Fix:** обозначить как «smoke / sanity», не «production-grade signal».
367
+
368
+ ---
369
+
370
+ ## 5. Top-10 Strengths (то, что я бы reproduce в любом будущем проекте)
371
+
372
+ 1. **Self-audit incident playbook** (qid 518): caught → traced → fixed → propagated → regression-tested → headline downgraded — all within ОДНОЙ session.
373
+ 2. **Triple-metric reporting** (BIRD / Arcwise-corrected / +9 catches).
374
+ 3. **Lab-grade saturation evidence** (3-model reasoning sweep, 42/42 attempts negative). Каждый отрицательный результат документирован.
375
+ 4. **`dev_split` stable-prefix property** для cache economics.
376
+ 5. **`safe_compare_pred(pred_failed, gold_failed)`** — symmetric guard kodifying в одном месте, applied везде.
377
+ 6. **P3.F acceptance harness** — programmatic gate перед merge, с required+forbidden columns AST-проверкой.
378
+ 7. **Field-level docstrings** в `PipelineConfig` с empirical justification.
379
+ 8. **AST guard с двойной итерацией** (`exp.Anonymous` + `exp.Func.key()`) покрывает sqlglot canonicalization edge cases.
380
+ 9. **SQLite mode=ro через creator (не URL)** + `PRAGMA query_only` + progress handler — 3-layer SQLite hardening.
381
+ 10. **`SESSION_HANDOFF.md`** as exemplary project memory — 1916 строк tl;dr per session, cold-pickup checklists, push status, не закрытые backlog items с severity.
382
+
383
+ ---
384
+
385
+ ## 6. Material Risks & Gaps (по severity)
386
+
387
+ ### P0 (закрыть перед публичной защитой) — **0 items**
388
+
389
+ Нет блокеров.
390
+
391
+ ### P1 (закрыть за 1-3 часа) — **4 items**
392
+
393
+ | # | Gap | File:line | Fix | EV |
394
+ |---|---|---|---|---|
395
+ | **P1.1** | Rate-limiter не fires когда `NL_SQL_API_KEY` пуст → DoS surface на public HF deploy | `api/main.py:289` | Rate-limit unconditionally по `x_api_key or request.client.host` | High |
396
+ | **P1.2** | `os.environ.get` в `api/main.py` обходит `Settings` (3 ключа: `NL_SQL_API_KEY`, `NLSQL_M_SCHEMA`, `NLSQL_DAC`) | `api/main.py:241, 256-257, 283` | Добавить в Settings, использовать `s.api_key`, `s.use_m_schema`, `s.use_dac` | Med (consistency) |
397
+ | **P1.3** | README hero не описывает «two regimes of EA lift» (generalisable vs BIRD-patches) → narrative risk на собеседовании | `README.md:9-18` | Добавить 2-параграфный «Honest framing» блок (см. recommendation 7.1) | **Very high** |
398
+ | **P1.4** | `/eval/latest` показывает stale baseline (`hybrid_n200_v0.json`, не v31) | `api/main.py:410` | Либо update committed baseline, либо scan `eval/reports/**/*.json` за latest mtime | Med |
399
+
400
+ ### P2 (закрыть за день) — **6 items**
401
+
402
+ | # | Gap | File:line | Fix |
403
+ |---|---|---|---|
404
+ | **P2.1** | Float-bucket bug подтверждён empirically (Codex #8). Latent на v22-v31, но реальный | `eval/metrics/execution_accuracy.py:209-221` | Минимум: docstring "known-limitation" + ссылка на 27_05_26. Оптимум: pair-wise tolerance match O(n²) для float-columns only |
405
+ | **P2.2** | No coverage gate в CI | `.github/workflows/ci.yml:31` | Добавить `--cov-fail-under=88` (current=91, gives 3pp headroom) |
406
+ | **P2.3** | Langfuse claimed в README + arch v2, но не wired в коде | `src/nl_sql/` (любой LLM call) | Либо обернуть `LLMProvider.generate` в Langfuse span, либо вытащить из README |
407
+ | **P2.4** | No structured logging в src/ | весь src/ | Добавить `logger = logging.getLogger(__name__)` в каждом модуле, JSON-структурно в production |
408
+ | **P2.5** | No security scanning в CI (pip-audit / bandit) | `.github/workflows/ci.yml` | Добавить `- run: uv run pip-audit` + `- run: uv run bandit -r src` |
409
+ | **P2.6** | `_check_no_attach_or_pragma` — пустая функция с misleading именем | `execution/guards.py:210` | Удалить или переименовать в `_check_extension_load_hooks` с реальным no-op обоснованием |
410
+
411
+ ### P3 (long-tail / nice-to-have) — **7 items**
412
+
413
+ | # | Gap | Fix |
414
+ |---|---|---|
415
+ | **P3.1** | Cache miss/fill race (Codex #10) — latent при parallel workers | per-key diskcache lock или `Cache.add` semantic |
416
+ | **P3.2** | Hardcoded absolute Windows path `D:/NL_SQL/.tmp/pplx_cookies.json` в helallao provider | Через Settings: `perplexity_cookies_path: Path = Field(default=...)` |
417
+ | **P3.3** | No matrix testing на Win/Mac в CI | Добавить matrix `os: [ubuntu, windows, macos]` |
418
+ | **P3.4** | CI checkout / setup-uv actions не pinned до SHA | Pin to SHA |
419
+ | **P3.5** | `cross_db_fewshot=True` по умолчанию в API — silent assumption для non-BIRD DBs | Docstring/log warning при non-BIRD DB |
420
+ | **P3.6** | No async / SSE для long-running `/ask` | Background task + polling endpoint |
421
+ | **P3.7** | README hero overload (5 параграфов P3.F mechanics) | Вытащить в `docs/p3f_design.md` (он есть), оставить 2 строки в README + link |
422
+
423
+ ---
424
+
425
+ ## 7. Strategic Recommendations (prioritised)
426
+
427
+ ### 7.1 P0: «Honest framing» блок в README hero (1 час)
428
+
429
+ Самая ценная рекомендация. Добавить **перед** lift trace:
430
+
431
+ > ## Honest framing of the 94.0% headline
432
+ >
433
+ > Our EA lift trace decomposes into two regimes with **very different generalisation properties**:
434
+ >
435
+ > **Regime A — Generalisable engineering (47% → 88%, +41pp).** Schema-RAG (dense retrieval + FK graph), cross-domain fewshot retrieval, grounded-critique directed retry, Mistral self-consistency, multi-provider voting (Groq llama-3.3 / qwen3 / gpt-oss / Sonnet 4.6 via GraceKelly bridge / helallao Perplexity Pro reasoning models), M-Schema rendering, CHASE-SQL DAC prompt. **Replicates on any BIRD-like benchmark with the same providers.**
436
+ >
437
+ > **Regime B — Targeted BIRD-quirk patches (88% → 94.0%, +6pp).** Eleven per-qid acceptance-gated schema-link hints in `src/nl_sql/agent/nodes/_hints.py`, each triggered by `db_id` + question-phrase + retrieved-table-set. By construction they cannot fire on any other prompt. **They do NOT generalise outside BIRD Mini-Dev n=200** — they are documented dataset-specific patches against BIRD gold annotation quirks (e.g. word-order inversion `'Street, City, Zip and State'` → SELECT `(Street, City, State, Zip)`). See [`docs/p3f_design.md`](docs/p3f_design.md) for the per-qid catalogue + acceptance harness (`scripts/p3f_acceptance.py`).
438
+ >
439
+ > Honest framing matters more than the headline number. The 41pp regime A lift is the portfolio signal; the 6pp regime B lift is research integrity (every per-qid hint has zero regressions on the other 199 qids, verified by `scripts/audit_rescore.py` and `scripts/p3f_acceptance.py --require-pass`).
440
+
441
+ **Эффект:** preempts the «overfitting» вопрос. Конвертирует weakness в demonstration of methodological awareness. **Это самая высокая ROI fix во всём проекте сейчас.**
442
+
443
+ ### 7.2 P1: API hardening sprint (2 часа)
444
+
445
+ 1. Rate-limiter unconditional (см. P1.1).
446
+ 2. Settings consolidate (см. P1.2).
447
+ 3. `/eval/latest` to point at v31 baseline (см. P1.4).
448
+
449
+ ### 7.3 P2: «Audit-trail bookend» (полдня)
450
+
451
+ - Float-bucket bug → minimum документировать как known-limitation в `_hashable` docstring, ссылка на этот аудит-файл. Honest «we know, we measured reach=0, here is when we'd fix».
452
+ - Coverage gate (+`--cov-fail-under=88`).
453
+ - Security scanning (pip-audit + bandit).
454
+
455
+ ### 7.4 P3: Observability if pitch'ится — wire it (4 часа)
456
+
457
+ - Если Langfuse в README — wire его в `LLMProvider.generate` обёртку.
458
+ - Structured logging через `logging.getLogger(__name__)`.
459
+
460
+ ### 7.5 Strategic: «BIRD-free» second eval surface (вне scope аудита)
461
+
462
+ Самая мощная **долгосрочная** инвестиция: добавить **secondary benchmark, не BIRD**. Spider 2.0 / WikiSQL / своя curated StackExchange-mini с 30 gold вопросами + manual review.
463
+
464
+ Это:
465
+ - Доказывает что 11 P3.F hints не нужны (они dataset-specific).
466
+ - Превращает proj из «BIRD-specialist» в «universally honest text2SQL evaluator».
467
+ - Закрывает narrative risk «вы overfit на одном dataset».
468
+
469
+ Это **3-5 days work**, существенный sprint — не для текущего цикла. Но это «next major signal» для proj.
470
+
471
+ ---
472
+
473
+ ## 8. Benchmarking vs Industry & Peer Projects
474
+
475
+ | Dimension | NL_SQL | Industry SaaS (Vanna/WrenAI/DataHerald) | Top portfolio (top decile) |
476
+ |---|---|---|---|
477
+ | Type safety | mypy --strict, 0 issues / 59 files | basic mypy or none | mypy strict optional |
478
+ | Test coverage | 91% | 60-80% | 75-85% |
479
+ | Linting | ruff full + format check | black / flake8 | ruff basic |
480
+ | Security | 3-layer (AST + DB role + runtime) | 1-2 layer | 1-2 layer |
481
+ | Eval rigor | Triple-metric + audit-rescore + saturation evidence + P3.F harness | Single metric | Single metric, often no public number |
482
+ | Documentation | SESSION_HANDOFF 1916 строк lab-notebook | API docs only | README + sparse docs |
483
+ | Self-audit culture | qid 518 incident closed within session | not visible | rare |
484
+ | Cost discipline | $0 hard constraint enforced | API budget unbounded | usually unbounded |
485
+ | Scalability | Single-replica by design | K8s + Redis + horizontal | single-replica typical |
486
+ | Async API | Sync blocking | async + SSE/WS | usually sync |
487
+
488
+ **Net:** проект превосходит typical portfolio top-decile **на 7 из 10 dimensions** (type safety, coverage, linting, security, eval, docs, self-audit, cost). Уступает на 2 (scalability, async). На «self-audit culture» — отрывается даже от индустрии.
489
+
490
+ ---
491
+
492
+ ## 9. Final Scorecard
493
+
494
+ | Dimension | Weight | Score | Weighted |
495
+ |---|---:|---:|---:|
496
+ | Architecture & Design | 12% | 9.8 | 1.18 |
497
+ | Code Quality & Type Safety | 12% | 9.7 | 1.16 |
498
+ | Testing & Coverage | 12% | 9.5 | 1.14 |
499
+ | Security & Safety | 12% | 9.4 | 1.13 |
500
+ | ML / Eval Methodology | 18% | 9.9 | 1.78 |
501
+ | DevOps / CI/CD | 8% | 9.0 | 0.72 |
502
+ | Observability & Operability | 6% | 8.5 | 0.51 |
503
+ | Documentation & KM | 10% | 9.9 | 0.99 |
504
+ | Production Readiness | 5% | 8.5 | 0.43 |
505
+ | Strategic Positioning | 5% | 9.0 | 0.45 |
506
+ | **Total** | **100%** | | **9.49** |
507
+
508
+ ### Округлённый итог: **9.5 / 10** (класс A)
509
+
510
+ ### Что даёт **9.8** после закрытия:
511
+
512
+ | Fix | Score delta |
513
+ |---|---:|
514
+ | P1.3 «Honest framing» в README | +0.15 → 9.65 |
515
+ | P1.1 + P1.2 + P1.4 (API hardening + Settings consolidation + /eval/latest) | +0.10 → 9.75 |
516
+ | P2.1 float-bucket: minimum документир��вать + P2.2 coverage gate + P2.3 Langfuse decision | +0.10 → 9.85 |
517
+
518
+ **Total addressable lift: 0.35pp за 4-6 часов работы.** После этого проект честно претендует на 9.8/10.
519
+
520
+ ---
521
+
522
+ ## 10. Кратко: что делать на этой неделе
523
+
524
+ | День | Действие | Эффект |
525
+ |---|---|---|
526
+ | Сегодня (≤1 час) | P1.3 «Honest framing» блок в README hero | Закрывает главный narrative risk перед demo |
527
+ | Завтра (2 часа) | P1.1 + P1.2 + P1.4 API hardening | Закрывает technical P1 cluster |
528
+ | Послезавтра (полдня) | P2.1 + P2.2 + P2.3 audit-trail bookend | Поднимает scorecard до 9.85 |
529
+ | Опционально (next sprint) | Secondary benchmark (Spider / curated SE-mini) | Закрывает P3.F overfit narrative permanently |
530
+
531
+ ---
532
+
533
+ **Конец отчёта.**
534
+
535
+ Все findings cross-referenced на конкретные file:line. Empirical claims (AST guard probes, float-bucket reproducer, coverage 91%, 370 pytest pass, mypy 0 issues) проверены в этой сессии в 2026-05-27 ~04:00Z.
app/components/output.py CHANGED
@@ -64,8 +64,8 @@ def render_output(output: OutputFormat | None, *, caption: str) -> None:
64
  st.metric(scalar_metric_label(output.column), str(output.value))
65
  elif isinstance(output, Sentence):
66
  st.markdown(
67
- f"<div style=\"font-family:'NLEdSerif',Georgia,serif; "
68
- f"font-size:1.25rem; line-height:1.45; color:var(--ink); "
69
  f'margin:0.4rem 0 0.6rem;">{output.text}</div>',
70
  unsafe_allow_html=True,
71
  )
 
64
  st.metric(scalar_metric_label(output.column), str(output.value))
65
  elif isinstance(output, Sentence):
66
  st.markdown(
67
+ f"<div style=\"font-size:1.2rem; line-height:1.5; "
68
+ f"color:var(--text); font-weight:500; "
69
  f'margin:0.4rem 0 0.6rem;">{output.text}</div>',
70
  unsafe_allow_html=True,
71
  )
app/components/welcome.py CHANGED
@@ -1,4 +1,4 @@
1
- """Hero block + sample question cards + sidebar language toggle."""
2
 
3
  from __future__ import annotations
4
 
@@ -8,31 +8,6 @@ from i18n import t
8
  from samples import SAMPLE_QUESTIONS
9
 
10
 
11
- def render_lang_toggle() -> None:
12
- """Two flat segments: EN / RU. Active one inverts."""
13
- lang = st.session_state.get("lang", "en")
14
- st.markdown(f"<div class='nl-side-sub'>{t('lang_label')}</div>", unsafe_allow_html=True)
15
- cols = st.columns(2)
16
- with cols[0]:
17
- if st.button(
18
- t("lang_en"),
19
- key="lang_en_btn",
20
- use_container_width=True,
21
- type="primary" if lang == "en" else "secondary",
22
- ):
23
- st.session_state.lang = "en"
24
- st.rerun()
25
- with cols[1]:
26
- if st.button(
27
- t("lang_ru"),
28
- key="lang_ru_btn",
29
- use_container_width=True,
30
- type="primary" if lang == "ru" else "secondary",
31
- ):
32
- st.session_state.lang = "ru"
33
- st.rerun()
34
-
35
-
36
  def render_welcome(db_id: str) -> None:
37
  st.markdown(
38
  "<div class='nl-display'>NL<span class='arrow'>→</span>SQL</div>",
@@ -40,7 +15,7 @@ def render_welcome(db_id: str) -> None:
40
  )
41
  st.markdown(f"<div class='nl-tagline'>{t('tagline')}</div>", unsafe_allow_html=True)
42
 
43
- col_a, col_b = st.columns(2)
44
  with col_a:
45
  st.markdown(
46
  f"""
@@ -63,27 +38,29 @@ def render_welcome(db_id: str) -> None:
63
  <div class='nl-metric-row'>
64
  <span class='nl-metric-value'>{t("research_value")}</span>
65
  </div>
66
- <div class='nl-metric-cap'>{t("research_caption")}</div>
67
  </div>
68
  """,
69
  unsafe_allow_html=True,
70
  )
71
 
72
- samples = SAMPLE_QUESTIONS.get(db_id)
73
- if not samples:
74
  st.markdown(
75
- f"<div class='nl-section-label'>{t('ask_intro_label')}</div>",
76
  unsafe_allow_html=True,
77
  )
78
- st.info(t("no_samples"))
79
- return
80
 
81
  st.markdown(
82
  f"<div class='nl-section-label'>{t('ask_intro_label')}</div>",
83
  unsafe_allow_html=True,
84
  )
85
 
86
- cols = st.columns(len(samples))
 
 
 
 
 
87
  diff_map = {
88
  "simple": t("diff_simple"),
89
  "moderate": t("diff_moderate"),
 
1
+ """Hero block + stat cards + methodology note + sample question cards."""
2
 
3
  from __future__ import annotations
4
 
 
8
  from samples import SAMPLE_QUESTIONS
9
 
10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  def render_welcome(db_id: str) -> None:
12
  st.markdown(
13
  "<div class='nl-display'>NL<span class='arrow'>→</span>SQL</div>",
 
15
  )
16
  st.markdown(f"<div class='nl-tagline'>{t('tagline')}</div>", unsafe_allow_html=True)
17
 
18
+ col_a, col_b = st.columns(2, gap="medium")
19
  with col_a:
20
  st.markdown(
21
  f"""
 
38
  <div class='nl-metric-row'>
39
  <span class='nl-metric-value'>{t("research_value")}</span>
40
  </div>
41
+ <div class='nl-metric-cap'>{t("research_short")}</div>
42
  </div>
43
  """,
44
  unsafe_allow_html=True,
45
  )
46
 
47
+ with st.expander(t("methodology_label"), expanded=False):
 
48
  st.markdown(
49
+ f"<div class='nl-method-body'>{t('research_caption')}</div>",
50
  unsafe_allow_html=True,
51
  )
 
 
52
 
53
  st.markdown(
54
  f"<div class='nl-section-label'>{t('ask_intro_label')}</div>",
55
  unsafe_allow_html=True,
56
  )
57
 
58
+ samples = SAMPLE_QUESTIONS.get(db_id)
59
+ if not samples:
60
+ st.info(t("no_samples"))
61
+ return
62
+
63
+ cols = st.columns(len(samples), gap="medium")
64
  diff_map = {
65
  "simple": t("diff_simple"),
66
  "moderate": t("diff_moderate"),
app/i18n.py CHANGED
@@ -28,6 +28,8 @@ I18N: dict[str, dict[str, str]] = {
28
  "metric_caption": "30 dev + 30 held-out, balanced split, all ten query categories at 100% on the free-tier codestral pipeline.",
29
  "research_kicker": "BIRD Mini-Dev research benchmark",
30
  "research_value": "94.0% / 200",
 
 
31
  "research_caption": (
32
  "Hybrid pipeline: "
33
  "<span class='nl-term' title='Mistral codestral-latest — SQL-specialised generation model, free tier'>codestral</span> + "
@@ -109,6 +111,8 @@ I18N: dict[str, dict[str, str]] = {
109
  "metric_caption": "30 dev + 30 held-out, сбалансированный сплит, все десять категорий запросов на 100% через бесплатный codestral.",
110
  "research_kicker": "Исследовательский бенчмарк BIRD Mini-Dev",
111
  "research_value": "94,0% / 200",
 
 
112
  "research_caption": (
113
  "Гибридный пайплайн: "
114
  "<span class='nl-term' title='Mistral codestral-latest — модель, специализированная под генерацию SQL, бесплатный тариф'>codestral</span> + "
 
28
  "metric_caption": "30 dev + 30 held-out, balanced split, all ten query categories at 100% on the free-tier codestral pipeline.",
29
  "research_kicker": "BIRD Mini-Dev research benchmark",
30
  "research_value": "94.0% / 200",
31
+ "research_short": "Above the human-expert baseline · $0 external cost",
32
+ "methodology_label": "How the score was reached",
33
  "research_caption": (
34
  "Hybrid pipeline: "
35
  "<span class='nl-term' title='Mistral codestral-latest — SQL-specialised generation model, free tier'>codestral</span> + "
 
111
  "metric_caption": "30 dev + 30 held-out, сбалансированный сплит, все десять категорий запросов на 100% через бесплатный codestral.",
112
  "research_kicker": "Исследовательский бенчмарк BIRD Mini-Dev",
113
  "research_value": "94,0% / 200",
114
+ "research_short": "Выше human-expert baseline · внешние расходы — ноль",
115
+ "methodology_label": "Как получен результат",
116
  "research_caption": (
117
  "Гибридный пайплайн: "
118
  "<span class='nl-term' title='Mistral codestral-latest — модель, специализированная под генерацию SQL, бесплатный тариф'>codestral</span> + "
app/static/fonts/jbmono-cyrillic.woff2 ADDED
Binary file (12.1 kB). View file
 
app/static/fonts/jbmono-latin.woff2 ADDED
Binary file (40.4 kB). View file
 
app/static/fonts/manrope-cyrillic.woff2 ADDED
Binary file (14.5 kB). View file
 
app/static/fonts/manrope-latin.woff2 ADDED
Binary file (24.8 kB). View file
 
app/streamlit_app.py CHANGED
@@ -1,9 +1,11 @@
1
  """Streamlit UI for the NL→SQL assistant.
2
 
3
- Editorial monochrome surface: ink-on-paper background, typography-led
4
- hierarchy, two custom faces (Stetica sans for chrome, TT Norms Pro Serif
5
- for display). Bilingual: EN/RU toggle in the sidebar (chrome only — data
6
- questions stay in their natural language; the pipeline accepts both).
 
 
7
 
8
  Run with:
9
  uv run streamlit run app/streamlit_app.py
@@ -20,22 +22,41 @@ from bootstrap import bootstrap, make_pipeline
20
  from components.output import render_output
21
  from components.schema_explorer import render_schema_explorer
22
  from components.show_working import render_show_working
23
- from components.welcome import render_lang_toggle, render_welcome
24
  from i18n import t
25
  from nl_sql.agent.graph import PipelineRunResult, run_pipeline
26
  from samples import source_link_for
27
  from theme import inject_chrome
28
 
29
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  def main() -> None:
31
  if "lang" not in st.session_state:
32
  st.session_state.lang = "en"
33
 
34
- st.set_page_config(
35
- page_title=t("page_title"),
36
- layout="wide",
37
- )
38
-
39
  inject_chrome()
40
 
41
  try:
@@ -45,55 +66,72 @@ def main() -> None:
45
  st.stop()
46
  st.session_state["_schema_index"] = schema_index
47
 
48
- # --- sidebar
49
- with st.sidebar:
50
- st.markdown("<div class='nl-side-h'>NL→SQL</div>", unsafe_allow_html=True)
51
- render_lang_toggle()
 
 
 
52
 
53
- st.markdown(
54
- f"<div class='nl-side-sub'>{t('db_label')}</div>",
55
- unsafe_allow_html=True,
56
- )
57
- db_ids = registry.ids()
58
- if not db_ids:
59
- st.error("No databases registered. Run scripts/download_data.py first.")
60
- st.stop()
61
- default_idx = (
62
- db_ids.index("bird_california_schools") if "bird_california_schools" in db_ids else 0
63
  )
64
- db_id = st.selectbox(t("db_label"), db_ids, index=default_idx, label_visibility="collapsed")
65
- spec = registry.get(db_id)
66
- st.caption(f"{t('db_dialect')}: `{spec.dialect}`")
67
- if spec.description:
68
- st.caption(spec.description)
69
- link = source_link_for(db_id)
70
- if link is not None:
71
- label, url = link
72
- st.caption(f"{t('db_source')}: [{label}]({url})")
73
-
74
- render_schema_explorer(db_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
 
 
76
  st.markdown(
77
- f"<div class='nl-side-sub'>{t('mode_header')}</div>",
78
  unsafe_allow_html=True,
79
  )
80
- mode = st.radio(
81
- t("mode_header"),
82
- options=(t("mode_accurate"), t("mode_fast"), t("mode_debug")),
83
- index=0,
84
- captions=(
85
- t("mode_accurate_caption"),
86
- t("mode_fast_caption"),
87
- t("mode_debug_caption"),
88
- ),
89
- label_visibility="collapsed",
90
- )
91
- if mode == t("mode_fast"):
92
- fewshot_top_k = 0
93
- verify_retry_on_empty = False
94
- else:
95
- fewshot_top_k = 3
96
- verify_retry_on_empty = True
97
 
98
  with st.expander(t("advanced_header"), expanded=False):
99
  schema_top_k = st.slider(t("schema_top_k"), 1, 10, 5)
@@ -102,8 +140,8 @@ def main() -> None:
102
  sort_schema_block = st.checkbox(t("sort_schema"), value=True)
103
  extended_sample_size = st.slider(t("sample_size"), 0, 8, 0)
104
 
105
- st.markdown("<div style='height:1.4rem'></div>", unsafe_allow_html=True)
106
- if st.button(t("clear_chat"), use_container_width=True):
107
  st.session_state.messages = []
108
  st.rerun()
109
 
 
1
  """Streamlit UI for the NL→SQL assistant.
2
 
3
+ Modern light surface: white background, one calm indigo accent, soft
4
+ shadows and rounded corners (Inter for text, JetBrains Mono for code).
5
+ Controls live in a top bar — database, mode and the EN/RU language
6
+ toggle; the sidebar keeps only the schema explorer, advanced retrieval
7
+ knobs and clear-chat. Bilingual chrome (data questions stay in their
8
+ natural language; the pipeline accepts both EN and RU).
9
 
10
  Run with:
11
  uv run streamlit run app/streamlit_app.py
 
22
  from components.output import render_output
23
  from components.schema_explorer import render_schema_explorer
24
  from components.show_working import render_show_working
25
+ from components.welcome import render_welcome
26
  from i18n import t
27
  from nl_sql.agent.graph import PipelineRunResult, run_pipeline
28
  from samples import source_link_for
29
  from theme import inject_chrome
30
 
31
 
32
+ def _render_lang_toggle(col_en: Any, col_ru: Any) -> None:
33
+ """EN | RU joined pill in the top bar; the active segment inverts."""
34
+ lang = st.session_state.get("lang", "en")
35
+ with col_en:
36
+ if st.button(
37
+ t("lang_en"),
38
+ key="lang_en_btn",
39
+ use_container_width=True,
40
+ type="primary" if lang == "en" else "secondary",
41
+ ):
42
+ st.session_state.lang = "en"
43
+ st.rerun()
44
+ with col_ru:
45
+ if st.button(
46
+ t("lang_ru"),
47
+ key="lang_ru_btn",
48
+ use_container_width=True,
49
+ type="primary" if lang == "ru" else "secondary",
50
+ ):
51
+ st.session_state.lang = "ru"
52
+ st.rerun()
53
+
54
+
55
  def main() -> None:
56
  if "lang" not in st.session_state:
57
  st.session_state.lang = "en"
58
 
59
+ st.set_page_config(page_title=t("page_title"), layout="wide")
 
 
 
 
60
  inject_chrome()
61
 
62
  try:
 
66
  st.stop()
67
  st.session_state["_schema_index"] = schema_index
68
 
69
+ db_ids = registry.ids()
70
+ if not db_ids:
71
+ st.error("No databases registered. Run scripts/download_data.py first.")
72
+ st.stop()
73
+ default_idx = (
74
+ db_ids.index("bird_california_schools") if "bird_california_schools" in db_ids else 0
75
+ )
76
 
77
+ # --- top bar (sticky): wordmark · database · mode · language
78
+ with st.container(key="nl_topbar"):
79
+ c_brand, c_db, c_mode, c_en, c_ru = st.columns(
80
+ [2.4, 3, 3, 0.9, 0.9], vertical_alignment="center"
 
 
 
 
 
 
81
  )
82
+ with c_brand:
83
+ st.markdown(
84
+ "<div class='nl-wordmark'>NL<span class='arrow'>→</span>SQL</div>",
85
+ unsafe_allow_html=True,
86
+ )
87
+ with c_db:
88
+ db_id = st.selectbox(
89
+ t("db_label"), db_ids, index=default_idx, label_visibility="collapsed"
90
+ )
91
+ with c_mode:
92
+ mode_opts = (t("mode_accurate"), t("mode_fast"), t("mode_debug"))
93
+ mode_help = (
94
+ f"{t('mode_accurate')} — {t('mode_accurate_caption')}\n\n"
95
+ f"{t('mode_fast')} — {t('mode_fast_caption')}\n\n"
96
+ f"{t('mode_debug')} — {t('mode_debug_caption')}"
97
+ )
98
+ mode = st.selectbox(
99
+ t("mode_header"),
100
+ mode_opts,
101
+ index=0,
102
+ label_visibility="collapsed",
103
+ help=mode_help,
104
+ )
105
+ _render_lang_toggle(c_en, c_ru)
106
+
107
+ spec = registry.get(db_id)
108
+ if mode == t("mode_fast"):
109
+ fewshot_top_k = 0
110
+ verify_retry_on_empty = False
111
+ else:
112
+ fewshot_top_k = 3
113
+ verify_retry_on_empty = True
114
+
115
+ # --- context line under the top bar
116
+ ctx_parts = [f"{t('db_dialect')}: <code>{spec.dialect}</code>"]
117
+ link = source_link_for(db_id)
118
+ if link is not None:
119
+ label, url = link
120
+ ctx_parts.append(f"{t('db_source')}: <a href='{url}' target='_blank'>{label}</a>")
121
+ if spec.description:
122
+ ctx_parts.append(spec.description)
123
+ st.markdown(
124
+ f"<div class='nl-context'>{' · '.join(ctx_parts)}</div>",
125
+ unsafe_allow_html=True,
126
+ )
127
 
128
+ # --- sidebar: schema explorer + advanced knobs + clear chat
129
+ with st.sidebar:
130
  st.markdown(
131
+ "<div class='nl-side-h'>NL<span class='arrow'>→</span>SQL</div>",
132
  unsafe_allow_html=True,
133
  )
134
+ render_schema_explorer(db_id)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
135
 
136
  with st.expander(t("advanced_header"), expanded=False):
137
  schema_top_k = st.slider(t("schema_top_k"), 1, 10, 5)
 
140
  sort_schema_block = st.checkbox(t("sort_schema"), value=True)
141
  extended_sample_size = st.slider(t("sample_size"), 0, 8, 0)
142
 
143
+ st.markdown("<div style='height:0.8rem'></div>", unsafe_allow_html=True)
144
+ if st.button(t("clear_chat"), key="clear_chat_btn", use_container_width=True):
145
  st.session_state.messages = []
146
  st.rerun()
147
 
app/theme.py CHANGED
@@ -1,7 +1,19 @@
1
- """Editorial monochrome theme: typography, chrome CSS, chart styling.
2
-
3
- Two custom faces (Stetica sans for chrome, TT Norms Pro Serif for display).
4
- Ink-on-paper palette. One accent — ink-fill on hover. No color drama in charts.
 
 
 
 
 
 
 
 
 
 
 
 
5
  """
6
 
7
  from __future__ import annotations
@@ -12,317 +24,379 @@ import streamlit as st
12
 
13
  FONT_CSS = """
14
  <style>
 
15
  @font-face {
16
- font-family: 'Stetica';
17
- src: url('/app/static/fonts/stetica-regular.otf') format('opentype');
18
- font-weight: 400;
19
- font-style: normal;
20
- font-display: swap;
21
  }
22
  @font-face {
23
- font-family: 'Stetica';
24
- src: url('/app/static/fonts/stetica-medium.otf') format('opentype');
25
- font-weight: 500;
26
- font-style: normal;
27
- font-display: swap;
28
  }
29
  @font-face {
30
- font-family: 'Stetica';
31
- src: url('/app/static/fonts/stetica-bold.otf') format('opentype');
32
- font-weight: 700;
33
- font-style: normal;
34
- font-display: swap;
35
  }
36
  @font-face {
37
- font-family: 'NLEdSerif';
38
- src: url('/app/static/fonts/serif-regular.otf') format('opentype');
39
- font-weight: 400;
40
- font-style: normal;
41
- font-display: swap;
42
- }
43
- @font-face {
44
- font-family: 'NLEdSerif';
45
- src: url('/app/static/fonts/serif-bold.otf') format('opentype');
46
- font-weight: 700;
47
- font-style: normal;
48
- font-display: swap;
49
  }
50
 
51
  :root {
52
- --ink: #111111;
53
- --ink-soft: #4A4A4A;
54
- --ink-mute: #7A7A75;
55
- --paper: #FAFAF7;
56
- --paper-warm: #F1EFE9;
57
- --rule: #1A1A1A;
58
- --hairline: #DCD8CE;
59
- }
60
-
61
- html, body, [class*="css"], .stApp, .stMarkdown, .stChatMessage {
62
- font-family: 'Stetica', system-ui, sans-serif !important;
63
- color: var(--ink);
64
- background: var(--paper);
65
- }
66
-
67
- .block-container {
68
- padding-top: 2.4rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  padding-bottom: 4rem;
70
- max-width: 1080px;
71
  }
72
 
73
  /* Hide Streamlit chrome we don't want */
74
- #MainMenu, footer, header [data-testid="stToolbar"] { visibility: hidden; }
75
- header { background: var(--paper) !important; }
76
-
77
- /* Display headline — serif */
78
- .nl-display {
79
- font-family: 'NLEdSerif', Georgia, serif;
80
- font-weight: 400;
81
- font-size: clamp(2.6rem, 5vw, 3.6rem);
82
- letter-spacing: -0.02em;
83
- line-height: 0.95;
84
- color: var(--ink);
85
- margin: 0 0 0.4rem 0;
86
- }
87
- .nl-display .arrow {
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  font-weight: 700;
89
- display: inline-block;
90
- transform: translateY(-0.04em);
91
- margin: 0 0.25rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  }
93
 
 
 
 
 
 
 
 
 
 
 
94
  .nl-tagline {
95
- font-family: 'Stetica', system-ui, sans-serif;
96
  font-weight: 400;
97
- font-size: 1.02rem;
98
- line-height: 1.5;
99
- color: var(--ink-soft);
100
- max-width: 56ch;
101
- margin: 0 0 2rem 0;
102
  }
103
 
104
- /* Kicker — small uppercase letter-spaced label */
105
  .nl-kicker {
106
- font-family: 'Stetica', sans-serif;
107
- font-size: 0.68rem;
108
- letter-spacing: 0.18em;
109
  text-transform: uppercase;
110
- color: var(--ink-mute);
111
- margin-bottom: 0.5rem;
112
  }
113
-
114
- /* Metric block — pure typography, no card chrome */
115
  .nl-metric {
116
- border-top: 1px solid var(--rule);
117
- padding-top: 0.8rem;
118
- margin-top: 1.4rem;
119
- }
120
- .nl-metric-row {
121
- display: flex;
122
- align-items: baseline;
123
- gap: 0.9rem;
124
- margin-bottom: 0.5rem;
125
- }
126
  .nl-metric-value {
127
- font-family: 'NLEdSerif', Georgia, serif;
128
- font-weight: 700;
129
- font-size: 2.2rem;
130
- letter-spacing: -0.01em;
131
- color: var(--ink);
132
  line-height: 1;
133
  }
134
  .nl-metric-aside {
135
- font-family: 'Stetica', sans-serif;
136
- font-size: 0.86rem;
137
- color: var(--ink-mute);
138
- letter-spacing: 0.04em;
139
- }
140
- .nl-metric-cap {
141
- font-family: 'Stetica', sans-serif;
142
- font-size: 0.86rem;
143
- color: var(--ink-soft);
144
- line-height: 1.55;
145
- max-width: 62ch;
146
- }
147
- .nl-term {
148
- border-bottom: 1px dotted var(--ink-mute);
149
- cursor: help;
150
- text-decoration: none;
151
- color: inherit;
152
- }
153
- .nl-term:hover {
154
- border-bottom-color: var(--ink);
155
- color: var(--ink);
156
  }
 
 
 
 
 
 
157
 
158
- /* Section rule */
159
  .nl-section-label {
160
- font-family: 'Stetica', sans-serif;
161
- font-size: 0.68rem;
162
- letter-spacing: 0.18em;
163
  text-transform: uppercase;
164
- color: var(--ink-mute);
165
- margin: 2.4rem 0 0.7rem 0;
166
- border-top: 1px solid var(--hairline);
167
- padding-top: 0.7rem;
168
  }
169
 
170
- /* Sidebar polish */
171
- [data-testid="stSidebar"] {
172
- background: var(--paper-warm) !important;
173
- border-right: 1px solid var(--hairline);
 
 
 
 
 
 
 
 
174
  }
 
 
 
 
175
  [data-testid="stSidebar"] .nl-side-h {
176
- font-family: 'NLEdSerif', Georgia, serif;
177
- font-weight: 700;
178
- font-size: 1.1rem;
179
- letter-spacing: -0.005em;
180
- margin: 0.4rem 0 0.6rem 0;
181
  }
 
182
  [data-testid="stSidebar"] .nl-side-sub {
183
- font-family: 'Stetica', sans-serif;
184
- font-size: 0.7rem;
185
- letter-spacing: 0.18em;
186
- text-transform: uppercase;
187
- color: var(--ink-mute);
188
- margin: 1.2rem 0 0.4rem 0;
189
- }
190
-
191
- /* Language toggle */
192
- .nl-lang-row { display: flex; gap: 0; }
193
- .nl-lang-row button {
194
- background: transparent !important;
195
- color: var(--ink) !important;
196
- border: 1px solid var(--rule) !important;
197
- border-radius: 0 !important;
198
- font-family: 'Stetica', sans-serif !important;
199
- font-weight: 500 !important;
200
- letter-spacing: 0.12em !important;
201
- text-transform: uppercase;
202
- padding: 0.35rem 0.9rem !important;
203
- font-size: 0.74rem !important;
204
- min-height: 0 !important;
205
  }
206
 
207
- /* Buttons (sample questions) */
208
  .stButton > button {
209
- background: transparent !important;
210
- color: var(--ink) !important;
211
- border: 1px solid var(--rule) !important;
212
- border-radius: 0 !important;
213
- font-family: 'Stetica', sans-serif !important;
214
- font-weight: 400 !important;
215
- font-size: 0.92rem !important;
216
  text-align: left !important;
217
- padding: 0.85rem 1rem !important;
218
- line-height: 1.45 !important;
219
- transition: background 0.12s;
220
  white-space: normal !important;
221
  height: auto !important;
 
 
222
  }
223
  .stButton > button:hover {
224
- background: var(--ink) !important;
225
- color: var(--paper) !important;
226
- }
227
- .stButton > button p {
228
- color: inherit !important;
229
- }
230
-
231
- /* Chat input */
232
- .stChatInput { border-top: 1px solid var(--rule) !important; }
233
- .stChatInput textarea {
234
- font-family: 'Stetica', sans-serif !important;
235
- font-size: 1rem !important;
236
- color: var(--ink) !important;
237
- background: var(--paper) !important;
238
- }
239
-
240
- /* Code blocks — keep mono but on warm paper */
241
- pre, code {
242
- background: var(--paper-warm) !important;
243
- color: var(--ink) !important;
244
- border: 1px solid var(--hairline) !important;
245
- border-radius: 0 !important;
246
- font-family: 'JetBrains Mono', 'IBM Plex Mono', ui-monospace, monospace !important;
247
- }
248
-
249
- /* Scalar metric block — flatten */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
250
  [data-testid="stMetric"] {
251
- background: transparent !important;
252
- border: none !important;
 
 
 
253
  }
254
  [data-testid="stMetricLabel"] {
255
- font-family: 'Stetica', sans-serif !important;
256
- font-size: 0.68rem !important;
257
- letter-spacing: 0.18em !important;
258
- text-transform: uppercase !important;
259
- color: var(--ink-mute) !important;
260
- }
261
- [data-testid="stMetricValue"] {
262
- font-family: 'NLEdSerif', Georgia, serif !important;
263
- font-weight: 700 !important;
264
- font-size: 2.4rem !important;
265
- color: var(--ink) !important;
266
- }
267
-
268
- /* Tables */
269
- [data-testid="stDataFrame"] { border: 1px solid var(--rule); }
270
-
271
- /* Expanders */
272
- .streamlit-expanderHeader {
273
- font-family: 'Stetica', sans-serif !important;
274
- font-size: 0.78rem !important;
275
- letter-spacing: 0.1em;
276
- text-transform: uppercase;
277
- color: var(--ink) !important;
278
  }
 
279
 
280
- /* Sample card — wraps a button + difficulty kicker */
281
- .nl-sample {
282
- display: block;
283
- }
284
- .nl-sample-kicker {
285
- font-family: 'Stetica', sans-serif;
286
- font-size: 0.62rem;
287
- letter-spacing: 0.22em;
288
- text-transform: uppercase;
289
- color: var(--ink-mute);
290
- margin: 0 0 0.4rem 0.05rem;
291
- }
292
 
293
- /* Chat message bubbles — strip default round chrome */
294
- [data-testid="stChatMessage"] {
295
- background: transparent !important;
296
- border: 0 !important;
297
- padding: 0.4rem 0 1.4rem 0 !important;
 
298
  }
299
- [data-testid="stChatMessage"]:not(:first-child) {
300
- border-top: 1px solid var(--hairline) !important;
301
- padding-top: 1.4rem !important;
302
  }
 
303
 
304
- /* Remove the avatar/icon circle Streamlit injects — covers every variant */
 
 
305
  [data-testid="stChatMessage"] > div:first-child,
306
- [data-testid="chatAvatarIcon-user"],
307
- [data-testid="chatAvatarIcon-assistant"],
308
- [data-testid="stChatMessageAvatarUser"],
309
- [data-testid="stChatMessageAvatarAssistant"],
310
- [data-testid="stChatMessage"] [class*="Avatar"],
311
- [data-testid="stChatMessage"] svg {
312
- display: none !important;
313
- }
314
-
315
- /* The chat message body lives in second child after the avatar; pull it left */
316
- [data-testid="stChatMessage"] > div:nth-child(2) {
317
- margin-left: 0 !important;
318
- padding-left: 0 !important;
319
- width: 100% !important;
320
- }
321
  </style>
322
  """
323
 
324
 
325
- CHART_PALETTE = ["#111111", "#4A4A4A", "#7A7A75", "#A8A29E", "#1A1A1A"]
326
 
327
 
328
  def inject_chrome() -> None:
@@ -331,13 +405,13 @@ def inject_chrome() -> None:
331
 
332
  def style_fig(fig: Any) -> Any:
333
  fig.update_layout(
334
- font_family="Stetica, system-ui, sans-serif",
335
- font_color="#111111",
336
- paper_bgcolor="#FAFAF7",
337
- plot_bgcolor="#FAFAF7",
338
  colorway=CHART_PALETTE,
339
  margin=dict(l=10, r=10, t=20, b=10),
340
  )
341
- fig.update_xaxes(gridcolor="#DCD8CE", zerolinecolor="#1A1A1A", tickcolor="#1A1A1A")
342
- fig.update_yaxes(gridcolor="#DCD8CE", zerolinecolor="#1A1A1A", tickcolor="#1A1A1A")
343
  return fig
 
1
+ """Editorial-warm light theme: typography, chrome CSS, chart styling.
2
+
3
+ Design read: a portfolio-grade NL→SQL product demo for technical
4
+ recruiters and data peers — calm, precise, editorial-warm (deliberately
5
+ *not* the blue/indigo SaaS default). Native Streamlit CSS, self-hosted
6
+ Manrope (UI + tabular figures, full Cyrillic) and JetBrains Mono (SQL).
7
+
8
+ Dials for this surface (it is a working tool, not a marketing page):
9
+ VARIANCE 4 · MOTION 2 (feedback-only transitions) · DENSITY 5.
10
+
11
+ System locks (taste pre-flight):
12
+ - Theme lock: light only.
13
+ - Colour lock: ONE accent — terracotta. No second hue anywhere.
14
+ - Shape lock: containers/cards/expanders 12px · inputs/buttons 8px ·
15
+ chips + language pills full-pill.
16
+ - Shadows tinted to the warm ink, never pure black.
17
  """
18
 
19
  from __future__ import annotations
 
24
 
25
  FONT_CSS = """
26
  <style>
27
+ /* Self-hosted variable fonts (served via enableStaticServing). */
28
  @font-face {
29
+ font-family: 'Manrope';
30
+ src: url('/app/static/fonts/manrope-latin.woff2') format('woff2');
31
+ font-weight: 200 800; font-style: normal; font-display: swap;
32
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2074,
33
+ U+20AC, U+2122, U+2190-2193, U+2212, U+2215;
34
  }
35
  @font-face {
36
+ font-family: 'Manrope';
37
+ src: url('/app/static/fonts/manrope-cyrillic.woff2') format('woff2');
38
+ font-weight: 200 800; font-style: normal; font-display: swap;
39
+ unicode-range: U+0301, U+0400-04FF, U+0500-052F, U+2116;
 
40
  }
41
  @font-face {
42
+ font-family: 'JetBrains Mono';
43
+ src: url('/app/static/fonts/jbmono-latin.woff2') format('woff2');
44
+ font-weight: 400 600; font-style: normal; font-display: swap;
45
+ unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+2000-206F, U+2074,
46
+ U+20AC, U+2122, U+2212, U+2215;
47
  }
48
  @font-face {
49
+ font-family: 'JetBrains Mono';
50
+ src: url('/app/static/fonts/jbmono-cyrillic.woff2') format('woff2');
51
+ font-weight: 400 600; font-style: normal; font-display: swap;
52
+ unicode-range: U+0301, U+0400-04FF, U+0500-052F, U+2116;
 
 
 
 
 
 
 
 
53
  }
54
 
55
  :root {
56
+ --bg: #FCFBF9;
57
+ --surface: #FFFFFF;
58
+ --panel: #F5F3EF;
59
+ --panel-2: #EDE9E1;
60
+ --border: #E7E3DB;
61
+ --border-strong: #D8D2C6;
62
+ --text: #1C1A17;
63
+ --text-soft: #423D35;
64
+ --text-mute: #6F6A61;
65
+ --accent: #C2541B; /* terracotta — fills, icons, key marks */
66
+ --accent-hover: #9E3F12;
67
+ --accent-ink: #9A3D11; /* accent TEXT on paper (AA-safe) */
68
+ --accent-soft: #F7ECE3;
69
+ --accent-ring: rgba(194, 84, 27, 0.24);
70
+ --radius: 12px;
71
+ --radius-sm: 8px;
72
+ --radius-pill: 999px;
73
+ --shadow-sm: 0 1px 2px rgba(28, 26, 23, 0.05), 0 1px 3px rgba(28, 26, 23, 0.05);
74
+ --shadow-md: 0 6px 18px rgba(28, 26, 23, 0.09);
75
+ --font: 'Manrope', system-ui, -apple-system, sans-serif;
76
+ --mono: 'JetBrains Mono', ui-monospace, 'SFMono-Regular', monospace;
77
+
78
+ /* legacy aliases */
79
+ --ink: var(--text); --ink-soft: var(--text-soft); --ink-mute: var(--text-mute);
80
+ --paper: var(--bg); --paper-warm: var(--panel); --rule: var(--border-strong);
81
+ --hairline: var(--border);
82
+ }
83
+
84
+ html, body, .stApp, [data-testid="stAppViewContainer"],
85
+ .stMarkdown, .stMarkdown *, [data-testid="stMarkdownContainer"], [data-testid="stMarkdownContainer"] *,
86
+ .stChatMessage, [class*="css"], input, textarea, button, select,
87
+ [data-baseweb="select"] *, [data-baseweb="popover"] * {
88
+ font-family: var(--font) !important;
89
+ }
90
+ .stApp, [data-testid="stAppViewContainer"] {
91
+ background: var(--bg);
92
+ color: var(--text);
93
+ -webkit-font-smoothing: antialiased;
94
+ text-rendering: optimizeLegibility;
95
+ }
96
+ .tabular, .nl-metric-value, [data-testid="stMetricValue"] {
97
+ font-variant-numeric: tabular-nums;
98
+ }
99
+
100
+ .block-container, [data-testid="stMainBlockContainer"] {
101
+ padding-top: 2.6rem;
102
  padding-bottom: 4rem;
103
+ max-width: 1140px;
104
  }
105
 
106
  /* Hide Streamlit chrome we don't want */
107
+ #MainMenu, footer, [data-testid="stToolbar"], [data-testid="stDecoration"] {
108
+ visibility: hidden;
109
+ }
110
+ /* Transparent fixed header still overlays the top bar — pass pointer
111
+ events through to our controls, keep its own buttons live. */
112
+ [data-testid="stHeader"] { background: transparent !important; pointer-events: none; }
113
+ [data-testid="stHeader"] * { pointer-events: auto; }
114
+
115
+ /* Keyboard focus ring — accessibility, every interactive element */
116
+ a:focus-visible, button:focus-visible, [role="button"]:focus-visible,
117
+ input:focus-visible, textarea:focus-visible, summary:focus-visible {
118
+ outline: 2px solid var(--accent) !important;
119
+ outline-offset: 2px !important;
120
+ border-radius: var(--radius-sm);
121
+ }
122
+
123
+ /* ---------- Sticky top bar ---------- */
124
+ .st-key-nl_topbar {
125
+ position: sticky;
126
+ top: 0;
127
+ z-index: 100;
128
+ background: var(--bg);
129
+ border-bottom: 1px solid var(--border);
130
+ padding: 0.45rem 0 0.55rem 0;
131
+ margin-bottom: 0.9rem;
132
+ }
133
+ .nl-wordmark {
134
  font-weight: 700;
135
+ font-size: 1.25rem;
136
+ letter-spacing: -0.03em;
137
+ color: var(--text);
138
+ line-height: 1.1;
139
+ white-space: nowrap;
140
+ }
141
+ .nl-wordmark .arrow, .nl-display .arrow { color: var(--accent); margin: 0 0.04em; }
142
+ .nl-context {
143
+ font-size: 0.8rem;
144
+ color: var(--text-mute);
145
+ margin: 0 0 1.6rem 0;
146
+ }
147
+ .nl-context a { color: var(--accent-ink); text-decoration: none; border-bottom: 1px solid transparent; }
148
+ .nl-context a:hover { border-bottom-color: var(--accent-ink); }
149
+ .nl-context code {
150
+ font-family: var(--mono) !important;
151
+ background: var(--panel-2) !important;
152
+ border: 0 !important;
153
+ padding: 0.06rem 0.36rem !important;
154
+ border-radius: 5px !important;
155
+ font-size: 0.76rem !important;
156
+ color: var(--text-soft) !important;
157
  }
158
 
159
+ /* ---------- Hero ---------- */
160
+ .nl-display {
161
+ font-weight: 800;
162
+ font-size: clamp(2rem, 3.6vw, 2.8rem);
163
+ letter-spacing: -0.035em;
164
+ line-height: 1.04;
165
+ color: var(--text);
166
+ text-wrap: balance;
167
+ margin: 0.5rem 0 0.5rem 0;
168
+ }
169
  .nl-tagline {
170
+ font-size: 1.05rem;
171
  font-weight: 400;
172
+ line-height: 1.55;
173
+ color: var(--text-soft);
174
+ max-width: 60ch;
175
+ text-wrap: pretty;
176
+ margin: 0 0 1.9rem 0;
177
  }
178
 
179
+ /* ---------- Stat cards ---------- */
180
  .nl-kicker {
181
+ font-size: 0.7rem;
182
+ font-weight: 600;
183
+ letter-spacing: 0.1em;
184
  text-transform: uppercase;
185
+ color: var(--text-mute);
186
+ margin-bottom: 0.55rem;
187
  }
 
 
188
  .nl-metric {
189
+ background: var(--surface);
190
+ border: 1px solid var(--border);
191
+ border-radius: var(--radius);
192
+ box-shadow: var(--shadow-sm);
193
+ padding: 1.15rem 1.3rem 1.35rem;
194
+ height: 100%;
195
+ }
196
+ .nl-metric-row { display: flex; align-items: baseline; gap: 0.6rem; margin-bottom: 0.5rem; }
 
 
197
  .nl-metric-value {
198
+ font-weight: 800;
199
+ font-size: 2rem;
200
+ letter-spacing: -0.02em;
201
+ color: var(--text);
 
202
  line-height: 1;
203
  }
204
  .nl-metric-aside {
205
+ font-size: 0.78rem;
206
+ font-weight: 600;
207
+ color: var(--accent-ink);
208
+ background: var(--accent-soft);
209
+ border-radius: var(--radius-pill);
210
+ padding: 0.14rem 0.6rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
211
  }
212
+ .nl-metric-cap { font-size: 0.86rem; color: var(--text-soft); line-height: 1.55; }
213
+
214
+ /* ---------- Methodology body (inside expander) ---------- */
215
+ .nl-method-body { font-size: 0.85rem; color: var(--text-soft); line-height: 1.62; }
216
+ .nl-term { border-bottom: 1px dotted var(--text-mute); cursor: help; color: inherit; }
217
+ .nl-term:hover { border-bottom-color: var(--accent); color: var(--accent-ink); }
218
 
219
+ /* ---------- Section label ---------- */
220
  .nl-section-label {
221
+ font-size: 0.7rem;
222
+ font-weight: 600;
223
+ letter-spacing: 0.1em;
224
  text-transform: uppercase;
225
+ color: var(--text-mute);
226
+ margin: 2.2rem 0 0.9rem 0;
 
 
227
  }
228
 
229
+ /* ---------- Sample difficulty chip ---------- */
230
+ .nl-sample-kicker {
231
+ display: inline-block;
232
+ font-size: 0.68rem;
233
+ font-weight: 600;
234
+ letter-spacing: 0.07em;
235
+ text-transform: uppercase;
236
+ color: var(--accent-ink);
237
+ background: var(--accent-soft);
238
+ border-radius: var(--radius-pill);
239
+ padding: 0.1rem 0.55rem;
240
+ margin: 0 0 0.45rem 0.1rem;
241
  }
242
+
243
+ /* ---------- Sidebar ---------- */
244
+ [data-testid="stSidebar"] { background: var(--panel) !important; border-right: 1px solid var(--border); }
245
+ [data-testid="stSidebar"] .block-container { padding-top: 1.4rem; }
246
  [data-testid="stSidebar"] .nl-side-h {
247
+ font-weight: 700; font-size: 1.05rem; letter-spacing: -0.02em; margin: 0.2rem 0 1rem 0;
 
 
 
 
248
  }
249
+ [data-testid="stSidebar"] .nl-side-h .arrow { color: var(--accent); }
250
  [data-testid="stSidebar"] .nl-side-sub {
251
+ font-size: 0.7rem; font-weight: 600; letter-spacing: 0.1em; text-transform: uppercase;
252
+ color: var(--text-mute); margin: 1.1rem 0 0.4rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
 
255
+ /* ---------- Buttons (sample cards + generic) ---------- */
256
  .stButton > button {
257
+ background: var(--surface) !important;
258
+ color: var(--text) !important;
259
+ border: 1px solid var(--border) !important;
260
+ border-radius: var(--radius-sm) !important;
261
+ box-shadow: var(--shadow-sm);
262
+ font-weight: 500 !important;
263
+ font-size: 0.9rem !important;
264
  text-align: left !important;
265
+ padding: 0.6rem 0.8rem !important;
266
+ line-height: 1.4 !important;
 
267
  white-space: normal !important;
268
  height: auto !important;
269
+ min-height: 0 !important;
270
+ transition: border-color .14s, background .14s, box-shadow .14s, transform .14s;
271
  }
272
  .stButton > button:hover {
273
+ border-color: var(--accent) !important;
274
+ background: var(--accent-soft) !important;
275
+ color: var(--accent-ink) !important;
276
+ box-shadow: var(--shadow-md);
277
+ transform: translateY(-1px);
278
+ }
279
+ .stButton > button:active { transform: translateY(0) scale(0.992); box-shadow: var(--shadow-sm); }
280
+ .stButton > button p { color: inherit !important; font-weight: 500 !important; }
281
+
282
+ /* Primary button (active language) — accent fill, AA white text */
283
+ .stButton > button[data-testid="stBaseButton-primary"] {
284
+ background: var(--accent) !important; color: #fff !important;
285
+ border-color: var(--accent) !important; box-shadow: none !important;
286
+ }
287
+ .stButton > button[data-testid="stBaseButton-primary"]:hover {
288
+ background: var(--accent-hover) !important; color: #fff !important;
289
+ border-color: var(--accent-hover) !important; transform: none !important;
290
+ }
291
+
292
+ /* Language pills (EN · RU) — compact, centred */
293
+ .st-key-lang_en_btn .stButton > button,
294
+ .st-key-lang_ru_btn .stButton > button {
295
+ text-align: center !important;
296
+ padding: 0.56rem 0.2rem !important;
297
+ font-size: 0.76rem !important; font-weight: 600 !important; letter-spacing: 0.08em !important;
298
+ box-shadow: none !important; transform: none !important;
299
+ }
300
+ .st-key-lang_en_btn .stButton > button:hover,
301
+ .st-key-lang_ru_btn .stButton > button:hover { transform: none !important; }
302
+
303
+ /* Clear-chat — quiet full-width action */
304
+ .st-key-clear_chat_btn .stButton > button {
305
+ text-align: center !important; color: var(--text-soft) !important;
306
+ box-shadow: none !important; font-weight: 500 !important;
307
+ }
308
+
309
+ /* ---------- Selectbox ---------- */
310
+ [data-baseweb="select"] > div {
311
+ background: var(--surface) !important;
312
+ border-radius: var(--radius-sm) !important;
313
+ border-color: var(--border) !important;
314
+ box-shadow: var(--shadow-sm);
315
+ min-height: 2.5rem;
316
+ }
317
+ [data-baseweb="select"] > div:hover { border-color: var(--border-strong) !important; }
318
+ [data-baseweb="select"] > div:focus-within {
319
+ border-color: var(--accent) !important;
320
+ box-shadow: 0 0 0 3px var(--accent-ring) !important;
321
+ }
322
+ [data-baseweb="menu"] [role="option"]:hover { background: var(--accent-soft) !important; }
323
+
324
+ /* Sliders / checkboxes accent */
325
+ [data-testid="stSlider"] [role="slider"] { background: var(--accent) !important; }
326
+ [data-baseweb="checkbox"] [data-checked="true"] { background: var(--accent) !important; border-color: var(--accent) !important; }
327
+
328
+ /* ---------- Chat input ---------- */
329
+ [data-testid="stChatInput"] {
330
+ border: 1px solid var(--border) !important;
331
+ border-radius: var(--radius) !important;
332
+ box-shadow: var(--shadow-sm);
333
+ background: var(--surface) !important;
334
+ }
335
+ [data-testid="stChatInput"]:focus-within {
336
+ border-color: var(--accent) !important;
337
+ box-shadow: 0 0 0 3px var(--accent-ring) !important;
338
+ }
339
+ [data-testid="stChatInput"] textarea { font-size: 1rem !important; color: var(--text) !important; background: transparent !important; }
340
+ [data-testid="stChatInput"] textarea::placeholder { color: var(--text-mute) !important; }
341
+ [data-testid="stChatInput"] button { background: var(--accent) !important; border-radius: var(--radius-sm) !important; color: #fff !important; }
342
+ [data-testid="stChatInput"] button:hover { background: var(--accent-hover) !important; }
343
+
344
+ /* ---------- Code ---------- */
345
+ pre, code, .stMarkdown code, .stMarkdown pre,
346
+ [data-testid="stCode"], [data-testid="stCode"] * { font-family: var(--mono) !important; }
347
+ [data-testid="stCode"], pre {
348
+ background: var(--panel) !important;
349
+ border: 1px solid var(--border) !important;
350
+ border-radius: var(--radius-sm) !important;
351
+ }
352
+
353
+ /* ---------- Metric (scalar answer) ---------- */
354
  [data-testid="stMetric"] {
355
+ background: var(--surface) !important;
356
+ border: 1px solid var(--border) !important;
357
+ border-radius: var(--radius) !important;
358
+ box-shadow: var(--shadow-sm);
359
+ padding: 1rem 1.2rem !important;
360
  }
361
  [data-testid="stMetricLabel"] {
362
+ font-size: 0.7rem !important; font-weight: 600 !important; letter-spacing: 0.1em !important;
363
+ text-transform: uppercase !important; color: var(--text-mute) !important;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
364
  }
365
+ [data-testid="stMetricValue"] { font-weight: 800 !important; font-size: 2.2rem !important; color: var(--text) !important; }
366
 
367
+ /* ---------- Tables ---------- */
368
+ [data-testid="stDataFrame"] { border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
369
 
370
+ /* ---------- Expanders ---------- */
371
+ [data-testid="stExpander"] {
372
+ border: 1px solid var(--border) !important;
373
+ border-radius: var(--radius) !important;
374
+ background: var(--surface) !important;
375
+ box-shadow: var(--shadow-sm);
376
  }
377
+ [data-testid="stExpander"] summary, .streamlit-expanderHeader {
378
+ font-size: 0.82rem !important; font-weight: 600 !important; color: var(--text) !important;
 
379
  }
380
+ [data-testid="stExpander"] summary:hover { color: var(--accent-ink) !important; }
381
 
382
+ /* ---------- Chat messages ---------- */
383
+ [data-testid="stChatMessage"] { background: transparent !important; border: 0 !important; padding: 0.4rem 0 1.4rem 0 !important; }
384
+ [data-testid="stChatMessage"]:not(:first-child) { border-top: 1px solid var(--border) !important; padding-top: 1.4rem !important; }
385
  [data-testid="stChatMessage"] > div:first-child,
386
+ [data-testid="chatAvatarIcon-user"], [data-testid="chatAvatarIcon-assistant"],
387
+ [data-testid="stChatMessageAvatarUser"], [data-testid="stChatMessageAvatarAssistant"],
388
+ [data-testid="stChatMessage"] [class*="Avatar"] { display: none !important; }
389
+ [data-testid="stChatMessage"] > div:nth-child(2) { margin-left: 0 !important; padding-left: 0 !important; width: 100% !important; }
390
+
391
+ /* ---------- Scrollbar ---------- */
392
+ ::-webkit-scrollbar { width: 10px; height: 10px; }
393
+ ::-webkit-scrollbar-thumb { background: var(--border-strong); border-radius: var(--radius-pill); border: 2px solid var(--bg); }
394
+ ::-webkit-scrollbar-thumb:hover { background: var(--text-mute); }
 
 
 
 
 
 
395
  </style>
396
  """
397
 
398
 
399
+ CHART_PALETTE = ["#C2541B", "#1C1A17", "#9E3F12", "#6F6A61", "#D8862E"]
400
 
401
 
402
  def inject_chrome() -> None:
 
405
 
406
  def style_fig(fig: Any) -> Any:
407
  fig.update_layout(
408
+ font_family="Manrope, system-ui, sans-serif",
409
+ font_color="#1C1A17",
410
+ paper_bgcolor="#FFFFFF",
411
+ plot_bgcolor="#FFFFFF",
412
  colorway=CHART_PALETTE,
413
  margin=dict(l=10, r=10, t=20, b=10),
414
  )
415
+ fig.update_xaxes(gridcolor="#EDE9E1", zerolinecolor="#D8D2C6", tickcolor="#D8D2C6")
416
+ fig.update_yaxes(gridcolor="#EDE9E1", zerolinecolor="#D8D2C6", tickcolor="#D8D2C6")
417
  return fig
audit_codex_30_05_26.md ADDED
@@ -0,0 +1,434 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # NL_SQL — полный аудит проекта
2
+
3
+ Дата аудита: 2026-05-30
4
+ Аудитор: Codex
5
+ Проект: `D:\NL_SQL`
6
+ Ветка: `main`
7
+ HEAD на старте: `8e6c61d598f1380833ae1b35825eef432b7a52f4`
8
+
9
+ ## 1. Baseline и границы аудита
10
+
11
+ Я не читал `.env` и не выводил секреты. Проверка секретов выполнена по
12
+ именам файлов, конфигам, `.env.example`, `.gitignore` и grep-паттернам с
13
+ исключением реального `.env`, lock-файлов, локальных БД и Chroma-индексов.
14
+
15
+ | Метрика | Значение |
16
+ |---|---:|
17
+ | Tracked files | 429 |
18
+ | Python files в `src/app/scripts/tests` | 143 |
19
+ | Bundle size | N/A: Python/FastAPI/Streamlit проект без frontend bundle |
20
+ | i18n leaf keys | N/A: нет count-based i18n target в задаче |
21
+ | Самые крупные tracked файлы | `financial.sqlite` 67.99 MB; Chroma `data_level0.bin` 36.65 MB; `debit_card_specializing.sqlite` 33.03 MB |
22
+ | Git dirty до записи отчёта | modified `chroma_data/*`; untracked `27_05_26.md`, `eval/reports/2026-05-25/C_dense_cards-p3f-1168-1029-v1.json`, `eval/reports/2026-05-25/index.html` |
23
+
24
+ Рабочее дерево уже было грязным до аудита. Эти файлы не изменялись.
25
+
26
+ ## 2. Выполненные проверки
27
+
28
+ | Проверка | Результат |
29
+ |---|---|
30
+ | `uv --version` | `uv 0.8.23` |
31
+ | `uv run python --version` | `Python 3.13.7` |
32
+ | `uv run ruff check src tests scripts app` | passed |
33
+ | `uv run ruff format --check src tests scripts app` | `143 files already formatted` |
34
+ | `uv run mypy src` | passed, `59 source files` |
35
+ | `uv run pytest --cov=src/nl_sql --cov-report=term-missing` | `370 passed`, coverage `91%`, 1 upstream LangGraph warning |
36
+ | `uv run python scripts\p3f_acceptance.py --report eval\reports\2026-05-26\v31-v30-plus-p3f-q37-merged.json --require-pass` | 11/11 P3.F targets PASS |
37
+ | `uv run python scripts\audit_rescore.py --report eval\reports\2026-05-26\v31-v30-plus-p3f-q37-merged.json` | `records: 200`, stored matches `188`, true matches `188`, mismatches `0` |
38
+
39
+ Дополнительные smoke-проверки:
40
+
41
+ - 61 unauthenticated request к `/databases` при пустом `NL_SQL_API_KEY` вернули последние статусы `[200, 200, 200]`, `429` не было.
42
+ - `/eval/latest` локально вернул `overall_ea=0.605`, `n=200`, `report_path=eval\baselines\hybrid_n200_v0.json`.
43
+ - В v31 JSON top-level `overall` равен `185/200 = 92.5%`, но фактический пересчёт `records[]` и `audit_rescore` дают `188/200 = 94.0%`.
44
+
45
+ ## 3. Executive Summary
46
+
47
+ NL_SQL сейчас выглядит как сильный portfolio/research проект: современный
48
+ Python-стек, строгие gates, хорошая модульность, read-only execution, rich eval
49
+ surface, сохранённые артефакты и высокая тестовая дисциплина. Текущие локальные
50
+ quality gates зелёные.
51
+
52
+ Главный риск не в качестве кода, а в integrity/operational слое вокруг
53
+ публичной метрики и API. README/UI заявляют v31 `94.0%`, canonical record-level
54
+ данные это подтверждают, но top-level summary в v31 JSON устарел, а `/eval/latest`
55
+ вообще отдаёт старый baseline `60.5%`. Это подрывает transparency story.
56
+
57
+ Второй важный риск: public API фактически не rate-limit'ится без
58
+ `NL_SQL_API_KEY`, а `.env`-only API key не включит auth, потому что код читает
59
+ только `os.environ`. Для публичного HF/Streamlit demo это DoS/quota surface.
60
+
61
+ Третий важный риск: после исправления qid 518 часть voting/eval scripts всё ещё
62
+ использует старый паттерн `_execute_gold()` + `compare_results()` без
63
+ `pred_failed/gold_failed` флагов. Основной v31 report сейчас verified, но будущие
64
+ rescues через эти scripts могут снова породить false positive.
65
+
66
+ ## 4. Сильные стороны
67
+
68
+ - **Gates зелёные:** ruff, format-check, mypy strict, pytest+coverage, P3.F
69
+ acceptance и audit-rescore прошли локально.
70
+ - **Архитектура читаемая:** `agent`, `api`, `db`, `execution`, `eval`, `llm`,
71
+ `render`, `schema_index` разделены нормально; Streamlit уже декомпозирован.
72
+ - **SQL safety не промптовая:** `sqlglot` AST guard + read-only SQLite/Postgres
73
+ path + timeout + row cap.
74
+ - **Eval discipline выше среднего:** есть BIRD, Arcwise, audit-rescore,
75
+ acceptance harness, regression tests на найденные scoring баги.
76
+ - **Provider abstraction практичный:** Mistral, Groq, GitHub Models, Ollama,
77
+ OpenRouter, GraceKelly/Perplexity, helallao bridge.
78
+ - **Документация богатая:** `README.md`, `docs/SESSION_HANDOFF.md`,
79
+ `docs/02_architecture_v2.md`, `docs/03_eval_methodology.md` дают контекст
80
+ следующей сессии.
81
+
82
+ ## 5. Findings
83
+
84
+ ### P1. v31 metric metadata and `/eval/latest` disagree with the 94.0% claim
85
+
86
+ Evidence:
87
+
88
+ - `eval/reports/2026-05-26/v31-v30-plus-p3f-q37-merged.json` top-level
89
+ `overall` says `matched=185`, `ea=0.925`.
90
+ - The same file's `records[]` contain 188 `match=true` rows.
91
+ - `scripts/audit_rescore.py` on the v31 report independently confirms
92
+ `matches true: 188`, `mismatches: 0`.
93
+ - `src/nl_sql/api/main.py:407` hardcodes `/eval/latest` to
94
+ `eval/baselines/hybrid_n200_v0.json`, which currently returns `0.605`.
95
+ - `tests/scripts/test_refresh_baseline_summary.py` guards only v22-v29, not
96
+ v30/v31.
97
+
98
+ Impact:
99
+
100
+ The public story says 94.0%, the audited row-level data supports 94.0%, but
101
+ machine-readable metadata says 92.5% or 60.5% depending on endpoint. This is a
102
+ portfolio trust issue.
103
+
104
+ Recommended fix:
105
+
106
+ 1. Run `scripts/refresh_baseline_summary.py` on v30/v31 reports.
107
+ 2. Extend the canonical summary-consistency test to v30/v31 and future reports.
108
+ 3. Replace hardcoded `/eval/latest` baseline path with a maintained manifest or
109
+ "latest final report" resolver.
110
+ 4. Make `configuration` / `sql_model` metadata include v30/v31 rescue labels
111
+ instead of stopping at q1275-era text.
112
+
113
+ ### P1. Public API rate-limit is disabled when auth is off
114
+
115
+ Evidence:
116
+
117
+ - `src/nl_sql/api/main.py:283` reads `NL_SQL_API_KEY` directly from
118
+ `os.environ`.
119
+ - `src/nl_sql/api/main.py:289` returns `"anonymous"` before calling
120
+ `rate_limiter.check(...)` when no key is set.
121
+ - Smoke test: 61 unauthenticated `/databases` requests returned 200; no 429.
122
+ - `README.md:36` documents "X-API-Key auth + token-bucket rate limit
123
+ (60 req/min)", but that only applies when a key is configured.
124
+ - The module docstring says env, `.env`, or settings, but `.env`-only
125
+ `NL_SQL_API_KEY` is not loaded into `os.environ` by `pydantic-settings`.
126
+
127
+ Impact:
128
+
129
+ Public demo without a process-level API key has no API-side rate limit. If
130
+ `/ask` is exposed, this can burn LLM quota or degrade the app.
131
+
132
+ Recommended fix:
133
+
134
+ - Add `api_key: str = Field(default="", validation_alias="NL_SQL_API_KEY")` to
135
+ `Settings` and use it in `create_app()`.
136
+ - Rate-limit unconditionally with key `x_api_key or request.client.host or
137
+ "anonymous"`.
138
+ - Add a regression test for 61 unauthenticated requests with auth off.
139
+
140
+ ### P1. The qid 518 false-positive pattern remains in several voting scripts
141
+
142
+ Evidence:
143
+
144
+ `safe_compare_pred()` exists and documents that voting/rescoring scripts must
145
+ pass `pred_failed` / `gold_failed`. Current fixed paths include
146
+ `scripts/audit_rescore.py`, `scripts/rescore_arcwise.py`,
147
+ `scripts/run_helallao_voting.py`, and `scripts/merge_voting_rescues.py`.
148
+
149
+ Still risky scripts found by grep:
150
+
151
+ - `scripts/run_groq_voting.py:250-258`
152
+ - `scripts/run_openrouter_voting.py:232-237`
153
+ - `scripts/run_critique_retry.py:183-188`
154
+ - `scripts/run_selfcon_retry.py:228-233`
155
+ - `scripts/run_sonnet_voting.py:142-147`
156
+ - `scripts/run_wide_schema_retry.py:158-163`
157
+ - `scripts/archive_sweep.py:99-115`
158
+ - `scripts/ensemble_vote.py:265-283`
159
+ - `scripts/run_planner_eval.py:124-130`
160
+
161
+ Impact:
162
+
163
+ Future eval/voting runs can again treat exec-failed pred/gold as empty rows and
164
+ mark `compare_results([], [])` as match. `merge_voting_rescues.py` reverify
165
+ mitigates some merges, but raw voting reports and any `--no-reverify` path can
166
+ still mislead.
167
+
168
+ Recommended fix:
169
+
170
+ - Create one shared helper for "execute pred + execute gold + safe compare".
171
+ - Migrate all scripts to it.
172
+ - Add a repository-wide test or grep guard that forbids direct
173
+ `_execute_gold()` + `compare_results()` in scripts unless justified.
174
+
175
+ ### P2. Runtime safety docs promise payload cap / hard LIMIT, code only has fetch cap
176
+
177
+ Evidence:
178
+
179
+ - `docs/02_architecture_v2.md:170` promises hard `LIMIT 10000` and result
180
+ payload cap 5MB.
181
+ - `src/nl_sql/db/connection.py:93-103` executes SQL as-is, then
182
+ `fetchmany(row_cap + 1)`.
183
+ - `src/nl_sql/execution/guards.py:8` says runner enforces result-payload cap,
184
+ but no payload-byte cap exists.
185
+ - `AskResponse` does not expose `QueryResult.truncated`, so API clients cannot
186
+ tell a result was clipped.
187
+
188
+ Impact:
189
+
190
+ The implementation still has a useful row cap and timeout, but the documented
191
+ payload safety is not implemented. Large text/blob cells can exceed intended
192
+ payload size, and clients may trust partial results as complete.
193
+
194
+ Recommended fix:
195
+
196
+ - Add byte-size accounting while materializing rows.
197
+ - Return `truncated: bool` and maybe `truncation_reason`.
198
+ - Either implement SQL-level LIMIT injection for eligible SELECTs or soften docs
199
+ to "fetch cap".
200
+
201
+ ### P2. Provider settings and factory are inconsistent for `perplexity`
202
+
203
+ Evidence:
204
+
205
+ - `src/nl_sql/llm/providers/factory.py:19` and `:49-53` support
206
+ `perplexity`.
207
+ - `scripts/eval_baseline.py:202` allows `perplexity` in CLI choices.
208
+ - `src/nl_sql/config/settings.py:7` defines `ProviderName` without
209
+ `perplexity`.
210
+ - Smoke: `NL_SQL_DEFAULT_PROVIDER=perplexity` causes Pydantic validation error.
211
+
212
+ Impact:
213
+
214
+ Documented/implemented provider cannot be selected via settings as default
215
+ provider.
216
+
217
+ Recommended fix:
218
+
219
+ Add `perplexity` to `ProviderName` and cover it in `test_provider_factory.py`.
220
+
221
+ ### P2. CI tests Python 3.13, deployment runtime asks for Python 3.12
222
+
223
+ Evidence:
224
+
225
+ - `.github/workflows/ci.yml:25` installs Python 3.13.
226
+ - `pyproject.toml:67` sets ruff `target-version = "py313"`.
227
+ - `pyproject.toml:92` sets mypy `python_version = "3.13"`.
228
+ - `runtime.txt:1` is `python-3.12`.
229
+ - `pyproject.toml:6` allows `>=3.12,<3.14`.
230
+
231
+ Impact:
232
+
233
+ The deployed Streamlit/HF runtime can run on Python 3.12 while CI, ruff and
234
+ mypy optimize for 3.13. A future 3.13-only syntax or dependency behavior could
235
+ pass CI and fail deployment.
236
+
237
+ Recommended fix:
238
+
239
+ Either move `runtime.txt` to Python 3.13, or add CI matrix for 3.12 and set
240
+ ruff/mypy target to the lowest supported runtime.
241
+
242
+ ### P2. Streamlit renders `Sentence.text` through unsafe HTML without escaping
243
+
244
+ Evidence:
245
+
246
+ - `app/components/output.py:62-68` interpolates `output.text` into
247
+ `st.markdown(..., unsafe_allow_html=True)`.
248
+ - `src/nl_sql/agent/nodes/format.py:22-25` creates `Sentence` from
249
+ `error_message` when no result is available.
250
+ - Error messages can include LLM-generated SQL / DB parser text.
251
+
252
+ Impact:
253
+
254
+ A malformed SQL/error path can inject HTML into the UI. Streamlit's markdown
255
+ layer is not a substitute for escaping when `unsafe_allow_html=True` is used.
256
+
257
+ Recommended fix:
258
+
259
+ Escape `output.text` with `html.escape()` before interpolation, or render
260
+ Sentence text with safe Streamlit primitives and move styling to CSS.
261
+
262
+ ### P2. Local secret/cookie paths are hardcoded in operational scripts
263
+
264
+ Evidence:
265
+
266
+ - `src/nl_sql/llm/providers/helallao_perplexity.py:30` defaults to
267
+ `D:/NL_SQL/.tmp/pplx_cookies.json`.
268
+ - `scripts/run_helallao_voting.py:56-59` exposes the same default.
269
+ - `scripts/run_openrouter_voting.py:48-54` reads
270
+ `D:/TXT/Free API Keys.txt` if `OPENROUTER_API_KEY` is absent.
271
+
272
+ Impact:
273
+
274
+ This is acceptable as local research tooling, but not portable and not
275
+ production-safe. Plaintext browser cookies are especially sensitive even if
276
+ `.tmp/` is gitignored.
277
+
278
+ Recommended fix:
279
+
280
+ Route paths through `Settings` or CLI-only required flags, avoid default secret
281
+ files, and use OS keyring/DPAPI for browser-cookie material if this workflow
282
+ must remain.
283
+
284
+ ### P2. API can leak internal exception details
285
+
286
+ Evidence:
287
+
288
+ - `src/nl_sql/api/main.py:397-401` returns
289
+ `pipeline crashed: {type(exc).__name__}: {exc}` to clients.
290
+ - `src/nl_sql/agent/nodes/explain_trace.py:47-53` exposes provider error text
291
+ in caption fallback.
292
+
293
+ Impact:
294
+
295
+ Provider errors, local paths, DB driver messages, or upstream response snippets
296
+ can leak to public clients.
297
+
298
+ Recommended fix:
299
+
300
+ Return a generic client error with `trace_id`; log full details server-side.
301
+
302
+ ### P3. Dev docker-compose uses fixed default credentials
303
+
304
+ Evidence:
305
+
306
+ - `docker-compose.yml:9` sets `POSTGRES_PASSWORD=postgres`.
307
+ - `docker-compose.yml:30` falls back to `dev-secret-change-me`.
308
+
309
+ Impact:
310
+
311
+ Fine for local profiles, but dangerous if someone treats compose as production.
312
+
313
+ Recommended fix:
314
+
315
+ Add an explicit "dev only" warning in `docker-compose.yml` comments and require
316
+ env-provided secrets for any non-local profile.
317
+
318
+ ### P3. Benchmark-specific schema hints are now a large production prompt surface
319
+
320
+ Evidence:
321
+
322
+ - `src/nl_sql/agent/nodes/_hints.py` contains many exact BIRD/qid-specific
323
+ phrase gates and "write exactly" hints.
324
+
325
+ Impact:
326
+
327
+ This is honest and effective for benchmark scoring, but it weakens the claim
328
+ that the interactive product is a general NL→SQL assistant unless framed as
329
+ "benchmark rescue layer". It can also overfit phrasing if shipped in production
330
+ mode.
331
+
332
+ Recommended fix:
333
+
334
+ Separate benchmark/eval prompt hints from product prompt hints via config, and
335
+ document the distinction in `/eval/latest`/README.
336
+
337
+ ### P3. Prebuilt Chroma and DB artefacts are tracked and frequently dirty
338
+
339
+ Evidence:
340
+
341
+ - `chroma_data/` and selected SQLite files are tracked by design.
342
+ - Current pre-audit dirty state includes three modified `chroma_data/*` files.
343
+
344
+ Impact:
345
+
346
+ This improves deploy cold-start but creates noisy dirty trees and parallel work
347
+ conflicts. Binary Chroma diffs are not reviewable.
348
+
349
+ Recommended fix:
350
+
351
+ Consider Git LFS or a release artifact for Chroma/data, or make runtime write to
352
+ a separate ignored copy of the index.
353
+
354
+ ### P3. Coverage is high overall, but disabled/bridge paths remain thin
355
+
356
+ Coverage gaps from the local run:
357
+
358
+ - `src/nl_sql/agent/nodes/plan_query.py`: 39%
359
+ - `src/nl_sql/llm/providers/helallao_perplexity.py`: 26%
360
+ - `src/nl_sql/execution/errors.py`: 76%
361
+
362
+ Impact:
363
+
364
+ This is acceptable while planner/helallao are eval/optional paths. If planner
365
+ or helallao becomes part of default product flow, add tests first.
366
+
367
+ ## 6. Security posture
368
+
369
+ Good:
370
+
371
+ - `.env` is ignored.
372
+ - `.env.example` contains no real keys.
373
+ - SQL write attempts are blocked at AST and DB layers.
374
+ - SQLite uses read-only URI + `PRAGMA query_only=ON`.
375
+ - Postgres path sets `default_transaction_read_only=on`.
376
+ - Input model caps question length at 2000 chars.
377
+
378
+ Needs work before broader public exposure:
379
+
380
+ - Unauthenticated API should still be rate-limited.
381
+ - `.env` API key path should actually work through `Settings`.
382
+ - Public 500s should not include raw exception text.
383
+ - Result payload cap should be real, not only documented.
384
+ - Plaintext Perplexity cookies should remain local-only tooling.
385
+
386
+ ## 7. CI / QA posture
387
+
388
+ Current QA is strong:
389
+
390
+ - Fast unit suite: 370 tests.
391
+ - Strict mypy on `src`.
392
+ - Ruff check and format-check include `src tests scripts app`.
393
+ - P3.F acceptance and audit-rescore are useful domain-specific gates.
394
+
395
+ Recommended additions:
396
+
397
+ - CI/test guard that all canonical final reports have `overall` derived from
398
+ `records[]`.
399
+ - CI/test guard for `/eval/latest` matching the latest final report.
400
+ - Regression test for anonymous rate limit.
401
+ - Regression test for `.env`-configured `NL_SQL_API_KEY`.
402
+ - A grep-style guard against unsafe direct `compare_results()` in scripts.
403
+ - Python 3.12 runtime job or runtime bump to 3.13.
404
+
405
+ ## 8. Product/readiness assessment
406
+
407
+ | Area | Rating | Notes |
408
+ |---|---:|---|
409
+ | Core engineering | 8.5/10 | Modular, typed, tested, good safety boundaries |
410
+ | Eval integrity | 7/10 | Row-level v31 is verified, but metadata/API drift is serious |
411
+ | Security for portfolio demo | 7/10 | SQL safety good; public API/rate-limit needs fix |
412
+ | Production API readiness | 5.5/10 | Functional, but auth/rate-limit/error/payload gaps remain |
413
+ | UI polish | 7/10 | Refactor improved maintainability; one unsafe render path remains |
414
+ | Deployment readiness | 6.5/10 | Works as demo, but Python runtime skew and tracked Chroma dirtiness need cleanup |
415
+
416
+ ## 9. Priority action list
417
+
418
+ 1. Fix v31/v30 report summaries and make `/eval/latest` point to the current
419
+ verified final report.
420
+ 2. Enable anonymous/IP rate-limit and move `NL_SQL_API_KEY` into `Settings`.
421
+ 3. Migrate all voting/rescore scripts to a shared safe comparison helper.
422
+ 4. Add payload-byte cap and expose `truncated` in API/UI.
423
+ 5. Align Python runtime between CI and deployment.
424
+ 6. Escape `Sentence.text` in Streamlit output rendering.
425
+ 7. Add `perplexity` to `ProviderName` or remove it from user-facing provider choices.
426
+ 8. Move local cookie/key paths behind explicit config and mark them as local-only.
427
+
428
+ ## 10. Bottom line
429
+
430
+ The project is technically strong and the current v31 row-level claim
431
+ `188/200 = 94.0%` is locally re-verified. The immediate problem is not model
432
+ quality; it is consistency of the artefacts around the claim. Fixing the
433
+ summary/API drift and anonymous rate-limit gap would materially improve the
434
+ trustworthiness of the public demo.
chroma_data/chroma.sqlite3 CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:90181e0a2514fbaf80a6de4c6307593dba4e93e9e6da6bd70600553a2651667b
3
  size 18161664
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:16c3ccffe56ea1ae9932da484fdfa7dfa34e7fc6b148684bdafb6a41f8bdfdcc
3
  size 18161664
chroma_data/fc9668d3-4384-40d9-aa8d-0010807a5a68/data_level0.bin CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:de3c405eda8dc30523c29aa0475a570ba0de2526b2ca29b21f1bf0f3ea94d6e0
3
  size 423600
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ac78878e535b9fd06aa012f6dab954f1633a82bf67265f0aa166ec467a024639
3
  size 423600
chroma_data/fc9668d3-4384-40d9-aa8d-0010807a5a68/length.bin CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:39f574a82964186c006c0c4d50f49095c2abb6b4614ca4e36f953667e96281da
3
  size 400
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:d9142387334b53a34226a53a60878980670c9a2a28a56cd9f8ddf2e7d44f3975
3
  size 400
docs/NEXT_SESSION.md CHANGED
@@ -3,6 +3,38 @@
3
  > Один лист, без воды. Берёшь, делаешь, обновляешь `SESSION_HANDOFF.md`,
4
  > переписываешь этот файл под следующий sprint.
5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
  ## 2026-05-26 EOD-7 — autonomous housekeeping sprint (HEAD `4207df0`, pushed)
7
 
8
  **Cleared today (one-shot autonomous run):**
@@ -16,9 +48,9 @@
16
 
17
  | # | Severity | Scope | Estimate |
18
  |---|---|---|---|
19
- | Codex #7 | P2 latent | `scripts/rescore_arcwise.py:82` stale `rec["match"]` — verified 0/200 disagreements on v29, transitions output unchanged if fixed | 30min, deferred |
20
- | Codex #8 | P2 latent | `execution_accuracy.py` `_hashable` float bucketing — verified 0 set-mismatch in v22-v30 baselines | 1h, deferred |
21
- | Codex #10 | P2 latent | `cache.py:88` cache miss/fill race — fires только при parallel workers (not currently used) | 1h, deferred |
22
 
23
  **Past 94.0% (gated к юзеру):** requires paid OR top-up / fine-tune / metric pivot. Residue 12 qids — large majority BIRD-annotation-quirks (unanimous fail через 3-model reasoning sweeps EOD-2 + EOD-4). Saturation подтверждена.
24
 
@@ -218,7 +250,7 @@ fetch). Local heterogeneous CSC lever остаётся parked.
218
  | ~~Kimi P1.3~~ | **Done 2026-05-26 EOD-7** (`a7c1d81`) | `app/streamlit_app.py` 1184 → 200 lines split: `i18n.py`/`theme.py`/`samples.py`/`bootstrap.py` + `components/{output,show_working,schema_explorer,welcome}.py`. `pyproject.toml` ruff `src` расширен `["src","tests","app"]`. Local Streamlit + Playwright E2E подтвердил EN 94.0% / RU 94,0% / schema explorer render OK. Zero behavior change, 357 pytest pass. | 1.5h |
219
  | ~~Kimi P1.4~~ | **Done 2026-05-26** | `src/nl_sql/agent/nodes/_support.py` 483 lines → `_support.py` (public API, 184 lines) + `_text_utils.py` (JSON parsing, 53 lines) + `_hints.py` (schema appendices, 302 lines). Zero behavior change, 355 pytest pass, ruff + mypy strict clean. | 1h |
220
  | ~~Kimi P1.6~~ | **Done 2026-05-26 EOD-7** (`4207df0`) | API coverage **58% → 89%**. Extracted `Singletons` NamedTuple + `get_singletons()` FastAPI Depends-factory. `/readyz`, `/databases`, `/ask` теперь принимают `Singletons = Depends(get_singletons)`; production callers idiomatic через `@lru_cache(maxsize=1)` на `_make_singletons` (zero behavior change). New `tests/api/test_api_routes_mocked.py` (13 tests) покрывает /readyz healthy/empty/raises пути, /databases auth + schema-exception, /ask unknown-db / canned-result / error-kind / confidence-buckets + rate-limit 60→61st 429. | 1.5h |
221
- | Codex #7 | P2 latent | `scripts/rescore_arcwise.py:82` transition buckets используют stale `rec["match"]` вместо recomputed `out_entry["original_match"]` (line 141 overwrite). **Reachability verified 2026-05-26: 0/200 stale-vs-fresh disagreements в `eval/reports/2026-05-24/v29-arcwise-rescored.json`** — bug latent, transitions counts (7 gained / 91 lost) honest. Fix = 1-line swap, no observable change в output. | 30min, deferred |
222
  | Codex #8 | P2 latent | `execution_accuracy.py:209-221` `_hashable` bucketing через `round(v / 1e-6)` может развести два tolerance-equivalent rows (diff ~9e-7, banker's rounding edge) в разные buckets → set-mode false negative. **Reachability verified 2026-05-26: 0 set-mismatch records в v22-v30 baselines (200 records each); 8 set-mismatch в demo runs 2026-05-11, все honest column-count diff не float-bucket.** Fix = replace `_hashable` с pair-wise tolerance match (O(n²)). | 1h, deferred |
223
  | ~~Codex #9~~ | **false positive 2026-05-26** | `cache.py:77` cache key omits `req.json_mode`. **Не достижимо в текущем коде:** `src/nl_sql/llm/providers/groq.py:44` force-set'ит `json_mode=True` через `req.model_copy` на каждом Groq call; Mistral codestral игнорирует поле (`base.py:21` docstring). Per (provider, model) пара `json_mode` имеет константное значение → collision impossible. Не трогать (попытка fix landed 2026-05-26, reverted после Codex+Kimi independent review). | closed |
224
  | Codex #10 | P2 latent | `cache.py:88` cache miss/fill race без lock — parallel eval workers могут race, duplicate paid calls, last-writer-wins. **Reachability: текущий eval pipeline serial per qid (см. `runner.py::_run_one`). Latent до момента запуска parallel workers.** Fix = per-key diskcache lock или atomic memoization (`Cache.add` semantic). | 1h, deferred |
 
3
  > Один лист, без воды. Берёшь, делаешь, обновляешь `SESSION_HANDOFF.md`,
4
  > переписываешь этот файл под следующий sprint.
5
 
6
+ ## 2026-06-17 — UI редизайн (anti-slop, ~9.5/10, push на origin/main)
7
+
8
+ Юзер: «современная страница» → затем «до 9.8/10». Полный редизайн Streamlit chrome
9
+ из editorial-monochrome в **editorial-warm light**. Функциональность не тронута.
10
+
11
+ - **Палитра:** Stone + **Terracotta `#C2541B`** (увод от AI-indigo / «Lila rule»). Один
12
+ акцент, color-lock. `.streamlit/config.toml` `base="light"` + primaryColor.
13
+ - **Шрифт:** self-hosted **Manrope** (UI + цифры, кириллица, `tabular-nums`) + **JetBrains
14
+ Mono** (SQL). Старые `.otf` (Stetica/serif) удалены; 4 variable woff2 (latin+cyrillic)
15
+ в `app/static/fonts/` (`manrope-*`, `jbmono-*`). NB: глобальный шрифтовой селектор
16
+ обязан покрывать `[data-testid="stMarkdownContainer"] *`, иначе Streamlit держит Source Sans.
17
+ - **Layout:** контролы (база / режим / EN-RU) → **sticky top bar** `st.container(key="nl_topbar")`;
18
+ сайдбар = только schema / advanced / clear, без скролла; EN/RU убран из сайдбара (пилюли);
19
+ кнопки-сэмплы компактные; длинная методология → свёрнутый `expander` (welcome влезает в экран
20
+ и не триггерит auto-scroll `stAppScrollToBottomContainer`).
21
+ - **Полировка:** shape-lock (12/8/pill), `:focus-visible` ring, `:active`, тёплые тени, WCAG AA
22
+ (white-on-terracotta 4.59:1, ссылки 6.66:1 — проверено скриптом), `text-wrap` balance/pretty.
23
+ - **Файлы:** `app/theme.py` (ядро CSS), `app/streamlit_app.py`, `app/components/{welcome,output}.py`,
24
+ `app/i18n.py`, `.streamlit/config.toml`, `README.md`.
25
+ - **Verified:** локальный Streamlit (порт 8501) + bundled Playwright (`chromium-1223`, MCP-браузер
26
+ занят) — Manrope применился, реальный запрос рендерится (COUNT 4, SQL-подсветка), адаптив 700px
27
+ держится, консоль чистая, ruff чистый. `/design-review`: P0/P1/P2 = 0, APPROVED. Самооценка ~9.5.
28
+
29
+ **Остаток / gated:**
30
+ - **HF Space live НЕ обновлён.** Деплой отдельный через `.deploy_hf.py` (нужен HF token —
31
+ в `.env` его НЕТ). GitHub запушен; HF redeploy — ручной шаг владельца.
32
+ - true-mobile <480px: 5-колоночный top bar поджимается (лимит Streamlit-колонок) — для
33
+ desktop-first портфолио ОК.
34
+ - Скриншоты README / `docs/ui-live-*.png` сняты на старом дизайне → устарели до HF-redeploy.
35
+
36
+ ---
37
+
38
  ## 2026-05-26 EOD-7 — autonomous housekeeping sprint (HEAD `4207df0`, pushed)
39
 
40
  **Cleared today (one-shot autonomous run):**
 
48
 
49
  | # | Severity | Scope | Estimate |
50
  |---|---|---|---|
51
+ | ~~Codex #7~~ | **Done 2026-05-26 EOD-7** (`a7c617f`) | `scripts/rescore_arcwise.py` transitions теперь используют fresh `out_entry["original_match"]` вместо stored `rec["match"]`. Canonical v29 transitions (gained=7 / lost=91) не изменились — 0/200 disagreements verified. Future-proofing only. | 30min |
52
+ | Codex #8 | P2 latent | `execution_accuracy.py` `_hashable` float bucketing — verified 0 set-mismatch in v22-v31 baselines. **Will NOT fix:** замена на O(n²) pair-wise tolerance match замедлит comparisons на больших result sets без observable benefit. Document как "won't fix" если не возникнет реальный repro. | won't fix |
53
+ | Codex #10 | P2 latent | `cache.py:88` cache miss/fill race — fires только при parallel workers (not currently used). **Will NOT fix:** добавляет complexity (per-key diskcache locks) для несуществующего scenario. Если parallel workers потребуются — пересмотреть. | won't fix |
54
 
55
  **Past 94.0% (gated к юзеру):** requires paid OR top-up / fine-tune / metric pivot. Residue 12 qids — large majority BIRD-annotation-quirks (unanimous fail через 3-model reasoning sweeps EOD-2 + EOD-4). Saturation подтверждена.
56
 
 
250
  | ~~Kimi P1.3~~ | **Done 2026-05-26 EOD-7** (`a7c1d81`) | `app/streamlit_app.py` 1184 → 200 lines split: `i18n.py`/`theme.py`/`samples.py`/`bootstrap.py` + `components/{output,show_working,schema_explorer,welcome}.py`. `pyproject.toml` ruff `src` расширен `["src","tests","app"]`. Local Streamlit + Playwright E2E подтвердил EN 94.0% / RU 94,0% / schema explorer render OK. Zero behavior change, 357 pytest pass. | 1.5h |
251
  | ~~Kimi P1.4~~ | **Done 2026-05-26** | `src/nl_sql/agent/nodes/_support.py` 483 lines → `_support.py` (public API, 184 lines) + `_text_utils.py` (JSON parsing, 53 lines) + `_hints.py` (schema appendices, 302 lines). Zero behavior change, 355 pytest pass, ruff + mypy strict clean. | 1h |
252
  | ~~Kimi P1.6~~ | **Done 2026-05-26 EOD-7** (`4207df0`) | API coverage **58% → 89%**. Extracted `Singletons` NamedTuple + `get_singletons()` FastAPI Depends-factory. `/readyz`, `/databases`, `/ask` теперь принимают `Singletons = Depends(get_singletons)`; production callers idiomatic через `@lru_cache(maxsize=1)` на `_make_singletons` (zero behavior change). New `tests/api/test_api_routes_mocked.py` (13 tests) покрывает /readyz healthy/empty/raises пути, /databases auth + schema-exception, /ask unknown-db / canned-result / error-kind / confidence-buckets + rate-limit 60→61st 429. | 1.5h |
253
+ | ~~Codex #7~~ | **Done 2026-05-26 EOD-7** (`a7c617f`) | `scripts/rescore_arcwise.py` transitions teперь через fresh `out_entry["original_match"]` вместо stored `rec["match"]`. Canonical v29 transitions (gained=7 / lost=91) не изменились — 0/200 disagreements. Future-proofing. | 30min |
254
  | Codex #8 | P2 latent | `execution_accuracy.py:209-221` `_hashable` bucketing через `round(v / 1e-6)` может развести два tolerance-equivalent rows (diff ~9e-7, banker's rounding edge) в разные buckets → set-mode false negative. **Reachability verified 2026-05-26: 0 set-mismatch records в v22-v30 baselines (200 records each); 8 set-mismatch в demo runs 2026-05-11, все honest column-count diff не float-bucket.** Fix = replace `_hashable` с pair-wise tolerance match (O(n²)). | 1h, deferred |
255
  | ~~Codex #9~~ | **false positive 2026-05-26** | `cache.py:77` cache key omits `req.json_mode`. **Не достижимо в текущем коде:** `src/nl_sql/llm/providers/groq.py:44` force-set'ит `json_mode=True` через `req.model_copy` на каждом Groq call; Mistral codestral игнорирует поле (`base.py:21` docstring). Per (provider, model) пара `json_mode` имеет константное значение → collision impossible. Не трогать (попытка fix landed 2026-05-26, reverted после Codex+Kimi independent review). | closed |
256
  | Codex #10 | P2 latent | `cache.py:88` cache miss/fill race без lock — parallel eval workers могут race, duplicate paid calls, last-writer-wins. **Reachability: текущий eval pipeline serial per qid (см. `runner.py::_run_one`). Latent до момента запуска parallel workers.** Fix = per-key diskcache lock или atomic memoization (`Cache.add` semantic). | 1h, deferred |
docs/SESSION_HANDOFF.md CHANGED
@@ -1,5 +1,13 @@
1
  # NL_SQL — Session Handoff (2026-05-26 EOD-7: Kimi P1.3 + P1.6 closed, 8 housekeeping commits pushed, HF Space live на v31 94.0%)
2
 
 
 
 
 
 
 
 
 
3
  > **Tl;dr 2026-05-26 EOD-7 — autonomous housekeeping sprint (HEAD `4207df0`, pushed):**
4
  >
5
  > 1. **Push backlog cleared:** 8 локальных commits (от `03ad6ae` до `a47a7fe`) запушены на `origin/main`. Origin теперь синхронен с локальным состоянием поверх v31 94.0%.
 
1
  # NL_SQL — Session Handoff (2026-05-26 EOD-7: Kimi P1.3 + P1.6 closed, 8 housekeeping commits pushed, HF Space live на v31 94.0%)
2
 
3
+ > **Tl;dr 2026-06-17 — UI редизайн (anti-slop, ~9.5/10, push на `origin/main`):** Streamlit
4
+ > chrome переписан editorial-monochrome → **editorial-warm light** (Stone + Terracotta `#C2541B`,
5
+ > увод от AI-indigo; self-hosted Manrope + JetBrains Mono с кириллицей; sticky top bar с
6
+ > база/режим/EN-RU; компактный сайдбар без скролла; методология в expander; WCAG AA проверен;
7
+ > `/design-review` APPROVED). Функциональность/eval не тронуты, 94.0% как было. Детали и остаток —
8
+ > `docs/NEXT_SESSION.md` (секция 2026-06-17). **Caveat: HF Space live НЕ обновлён** — отдельный
9
+ > `.deploy_hf.py` (HF token не в `.env`), GitHub запушен, HF redeploy — ручной шаг владельца.
10
+ >
11
  > **Tl;dr 2026-05-26 EOD-7 — autonomous housekeeping sprint (HEAD `4207df0`, pushed):**
12
  >
13
  > 1. **Push backlog cleared:** 8 локальных commits (от `03ad6ae` до `a47a7fe`) запушены на `origin/main`. Origin теперь синхронен с локальным состоянием поверх v31 94.0%.
docs/data_flow.html ADDED
@@ -0,0 +1,688 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ru">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>NL→SQL Assistant — схема движения данных</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Source+Serif+4:opsz,wght@8..60,500;8..60,600&family=IBM+Plex+Sans:wght@300;400;500;600&family=IBM+Plex+Mono:wght@400;500&display=swap&subset=cyrillic" rel="stylesheet">
10
+ <style>
11
+ :root{
12
+ --bg:#FAFAF8;
13
+ --text:#1C1C1A;
14
+ --accent:#9C5B38;
15
+ --accent-dark:#7C4426;
16
+ --muted:rgba(28,28,26,0.60);
17
+ --faint:rgba(28,28,26,0.38);
18
+ --line:rgba(28,28,26,0.40);
19
+ --divider:rgba(28,28,26,0.08);
20
+ --card:#FFFFFF;
21
+ }
22
+ *{box-sizing:border-box}
23
+ body{
24
+ margin:0;background:var(--bg);color:var(--text);
25
+ font-family:'IBM Plex Sans',sans-serif;font-size:16px;line-height:1.6;
26
+ font-variant-numeric:tabular-nums lining-nums;
27
+ font-feature-settings:"tnum" 1,"lnum" 1;
28
+ text-wrap:pretty;
29
+ }
30
+ .mono{font-family:'IBM Plex Mono',monospace}
31
+
32
+ /* ---------- header ---------- */
33
+ header{padding:48px 48px 24px;max-width:1880px;margin:0 auto}
34
+ h1{
35
+ font-family:'Source Serif 4',Georgia,serif;font-size:40px;font-weight:600;
36
+ line-height:1.1;margin:0 0 8px;letter-spacing:-0.01em;
37
+ }
38
+ .subtitle{font-size:15px;color:var(--muted);margin:0;max-width:1020px}
39
+ .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:48px;margin-top:32px;flex-wrap:wrap}
40
+ .kpis{display:flex;gap:48px;flex-wrap:wrap}
41
+ .kpi .v{font-size:32px;font-weight:300;line-height:1.1;letter-spacing:-0.01em}
42
+ .kpi .v sup{font-size:14px;font-weight:400;color:var(--muted)}
43
+ .kpi .l{font-size:11px;text-transform:uppercase;letter-spacing:0.08em;color:var(--muted);margin-top:6px}
44
+ .legend{display:grid;grid-template-columns:auto auto;gap:6px 24px;font-size:12px;color:var(--muted);padding-bottom:4px}
45
+ .legend .li{display:flex;align-items:center;gap:10px;white-space:nowrap}
46
+ .legend svg{flex:0 0 auto}
47
+ .legend .chip{width:18px;height:18px;border-radius:50%;background:var(--accent);color:#FDFBF9;
48
+ font-size:10px;font-weight:600;display:inline-flex;align-items:center;justify-content:center;flex:0 0 auto}
49
+
50
+ /* ---------- canvas scaling ---------- */
51
+ #viewport{width:100%;overflow:hidden;padding:0 0 48px}
52
+ #scaler{transform-origin:top left;margin:0 auto}
53
+ #canvas{position:relative;width:1880px;height:2236px;font-size:13px}
54
+
55
+ /* ---------- zones ---------- */
56
+ .zone{position:absolute;border-radius:10px}
57
+ .zone-tint{background:rgba(28,28,26,0.024)}
58
+ .zone-hot{background:rgba(156,91,56,0.045)}
59
+ .zone-h{position:absolute;font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;color:var(--text)}
60
+ .zone-h .zn{color:var(--accent);margin-right:8px}
61
+ .zone-h small{display:block;font-weight:400;text-transform:none;letter-spacing:0;font-size:12px;color:var(--muted);margin-top:2px}
62
+
63
+ /* ---------- nodes ---------- */
64
+ .node{
65
+ position:absolute;background:var(--card);
66
+ border:1px solid rgba(28,28,26,0.12);border-radius:8px;
67
+ padding:12px 14px;line-height:1.45;
68
+ box-shadow:0 1px 3px rgba(30,25,20,0.05);
69
+ transition:box-shadow 160ms ease;
70
+ }
71
+ .node:hover{box-shadow:0 4px 10px -2px rgba(30,25,20,0.13),0 2px 4px -2px rgba(30,25,20,0.08)}
72
+ .node.hot{border-top:3px solid var(--accent);padding-top:10px}
73
+ .node.dashed{border-style:dashed;background:rgba(255,255,255,0.7)}
74
+ .node h3{font-size:14px;font-weight:600;margin:0 0 1px;line-height:1.25;font-family:'IBM Plex Sans',sans-serif}
75
+ .node .path{font-family:'IBM Plex Mono',monospace;font-size:11px;color:var(--muted);margin:0 0 6px}
76
+ .node p{margin:0;font-size:13px;color:rgba(28,28,26,0.80)}
77
+ .node p b{font-weight:600;color:var(--text)}
78
+ .node ul{margin:2px 0 0;padding:0;list-style:none}
79
+ .node li{font-size:13px;color:rgba(28,28,26,0.80);padding-left:12px;position:relative}
80
+ .node li::before{content:"";position:absolute;left:0;top:0.62em;width:5px;height:1.5px;background:var(--accent)}
81
+ .badges{margin-top:8px;display:flex;flex-wrap:wrap;gap:4px}
82
+ .bdg{
83
+ font-family:'IBM Plex Mono',monospace;font-size:11px;line-height:1;
84
+ background:rgba(28,28,26,0.055);border-radius:4px;padding:4px 7px;color:rgba(28,28,26,0.74);
85
+ white-space:nowrap;
86
+ }
87
+ .bdg.a{background:rgba(156,91,56,0.12);color:var(--accent-dark)}
88
+ .tag{
89
+ display:inline-block;font-size:9.5px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;
90
+ border-radius:4px;padding:3px 6px;line-height:1;vertical-align:2px;margin-left:6px;
91
+ }
92
+ .tag.opt{background:rgba(28,28,26,0.07);color:var(--muted)}
93
+ .tag.pri{background:var(--accent);color:#FDFBF9}
94
+ .tag.nollm{background:rgba(28,28,26,0.85);color:#FAFAF8}
95
+
96
+ /* step chips on the hot path */
97
+ .step{
98
+ position:absolute;top:-11px;left:-11px;width:22px;height:22px;border-radius:50%;
99
+ background:var(--accent);color:#FDFBF9;font-size:11.5px;font-weight:600;
100
+ display:flex;align-items:center;justify-content:center;
101
+ box-shadow:0 1px 3px rgba(30,25,20,0.28);
102
+ }
103
+ .step.alt{background:var(--card);color:var(--accent-dark);border:1.5px dashed var(--accent);box-shadow:none}
104
+
105
+ /* provider group */
106
+ .group{
107
+ position:absolute;border:1px dashed rgba(28,28,26,0.22);border-radius:10px;
108
+ }
109
+ .group .gt{
110
+ position:absolute;top:14px;left:16px;font-size:11px;font-weight:600;
111
+ text-transform:uppercase;letter-spacing:0.09em;color:var(--muted);
112
+ }
113
+ .pcard{padding:10px 12px}
114
+ .pcard h3{font-size:13px}
115
+ .pcard p{font-size:12px;line-height:1.45}
116
+ .note{
117
+ position:absolute;font-size:12px;color:var(--muted);line-height:1.5;
118
+ padding:10px 12px;border:1px dashed rgba(156,91,56,0.4);border-radius:8px;
119
+ background:rgba(156,91,56,0.04);
120
+ }
121
+
122
+ /* CI ribbon */
123
+ .ribbon{
124
+ position:absolute;background:var(--card);border:1px solid rgba(28,28,26,0.12);
125
+ border-radius:8px;display:flex;align-items:center;gap:8px;padding:14px 20px;flex-wrap:wrap;
126
+ }
127
+ .ribbon .rt{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:0.1em;margin-right:12px}
128
+ .ribbon .bdg{font-size:11px;padding:5px 9px}
129
+
130
+ /* ---------- edges ---------- */
131
+ #wires{position:absolute;inset:0;pointer-events:none}
132
+ .edge path.l{fill:none}
133
+ .edge .hot{stroke:var(--accent);stroke-width:2.2}
134
+ .edge .rep{stroke:var(--accent);stroke-width:1.7;stroke-dasharray:7 5}
135
+ .edge .bus{stroke:var(--accent);stroke-width:1.7;opacity:0.85}
136
+ .edge .idx{stroke:var(--line);stroke-width:1.6;stroke-dasharray:8 6}
137
+ .edge .opt{stroke:var(--line);stroke-width:1.5;stroke-dasharray:3 5}
138
+ .edge .eval{stroke:rgba(28,28,26,0.5);stroke-width:1.6;stroke-dasharray:2 6;stroke-linecap:round}
139
+ .edge text{font-family:'IBM Plex Sans',sans-serif;font-size:12px}
140
+ .edge .lbg{fill:#FAFAF8;opacity:0.94}
141
+ .edge .lt-hot{fill:var(--accent-dark)}
142
+ .edge .lt-mut{fill:rgba(28,28,26,0.70)}
143
+ #canvas.focus .edge{opacity:0.16;transition:opacity 120ms}
144
+ #canvas.focus .edge.on{opacity:1}
145
+ #canvas.focus .node{opacity:0.45;transition:opacity 120ms}
146
+ #canvas.focus .node.on{opacity:1}
147
+ #canvas.focus .group,#canvas.focus .zone-h{opacity:0.55}
148
+
149
+ footer{max-width:1880px;margin:0 auto;padding:0 48px 56px;font-size:12px;color:var(--muted)}
150
+ footer .mono{font-size:11px}
151
+
152
+ /* mobile stub */
153
+ #mobile-stub{display:none;position:fixed;inset:0;z-index:9999;background:var(--bg);color:var(--text);
154
+ align-items:center;justify-content:center;text-align:center;padding:40px}
155
+
156
+ /* zoom bar */
157
+ #zoombar{
158
+ position:fixed;right:20px;bottom:20px;z-index:500;
159
+ display:flex;align-items:center;gap:4px;
160
+ background:var(--card);border:1px solid rgba(28,28,26,0.14);border-radius:8px;
161
+ padding:6px 8px;box-shadow:0 2px 6px -1px rgba(30,25,20,0.12),0 1px 3px rgba(30,25,20,0.07);
162
+ }
163
+ #zoombar button{
164
+ width:30px;height:28px;border:1px solid rgba(28,28,26,0.14);border-radius:6px;
165
+ background:var(--bg);color:var(--text);font:500 14px/1 'IBM Plex Mono',monospace;
166
+ cursor:pointer;padding:0;
167
+ }
168
+ #zoombar button:hover{background:rgba(156,91,56,0.08);border-color:rgba(156,91,56,0.4)}
169
+ #zoombar button:focus-visible{outline:2px solid var(--accent);outline-offset:1px}
170
+ #zoombar button.fit{width:auto;padding:0 10px;font-size:11px}
171
+ #zoombar #zval{font:400 11px 'IBM Plex Mono',monospace;color:var(--muted);min-width:38px;text-align:right}
172
+
173
+ @media print{
174
+ #zoombar,#mobile-stub{display:none!important}
175
+ .node,.ribbon,#zoombar{box-shadow:none!important}
176
+ body{-webkit-print-color-adjust:exact;print-color-adjust:exact}
177
+ }
178
+ </style>
179
+ </head>
180
+ <body>
181
+
182
+ <div id="mobile-stub"><p style="font-size:18px;line-height:1.6">Откройте страницу на десктопе<br>для корректного отображения схемы.</p></div>
183
+
184
+ <div id="zoombar">
185
+ <button data-z="out" title="Уменьшить">−</button>
186
+ <button data-z="fit" class="fit" title="Вписать в окно">fit</button>
187
+ <button data-z="in" title="Увеличить">+</button>
188
+ <span id="zval">100%</span>
189
+ </div>
190
+
191
+ <header>
192
+ <h1>NL→SQL Assistant · схема движения данных</h1>
193
+ <p class="subtitle">Полный data flow портфолио-проекта: offline-индексация схем (01) → online query pipeline на LangGraph (02) → слой LLM-провайдеров с кэшем (03) → eval-контур с residue-циклом (04). Подписи на стрелках — что именно течёт между узлами; цифры в кружках — порядок шагов happy path.</p>
194
+ <div class="head-row">
195
+ <div class="kpis">
196
+ <div class="kpi"><div class="v">94.0<sup>%</sup></div><div class="l">EA · BIRD Mini-Dev n=200 · v31</div></div>
197
+ <div class="kpi"><div class="v">+1.04<sup>pp</sup></div><div class="l">над human-expert 92.96%</div></div>
198
+ <div class="kpi"><div class="v">$0</div><div class="l">external cost · free tiers + кэш</div></div>
199
+ <div class="kpi"><div class="v">370</div><div class="l">tests · coverage 91% · mypy strict</div></div>
200
+ <div class="kpi"><div class="v">7+2</div><div class="l">узлов LangGraph · база + opt-in</div></div>
201
+ </div>
202
+ <div class="legend">
203
+ <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="#9C5B38" stroke-width="2.2"/></svg>горячий путь запроса</div>
204
+ <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.4)" stroke-width="1.6" stroke-dasharray="8 6"/></svg>offline-индексация</div>
205
+ <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="#9C5B38" stroke-width="1.7" stroke-dasharray="7 5"/></svg>repair / retry</div>
206
+ <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.5)" stroke-width="1.6" stroke-dasharray="2 6" stroke-linecap="round"/></svg>eval-контур</div>
207
+ <div class="li"><svg width="36" height="8"><line x1="0" y1="4" x2="36" y2="4" stroke="rgba(28,28,26,.4)" stroke-width="1.5" stroke-dasharray="3 5"/></svg>opt-in узлы</div>
208
+ <div class="li"><span class="chip">1</span>шаги happy path</div>
209
+ </div>
210
+ </div>
211
+ </header>
212
+
213
+ <div id="viewport"><div id="scaler"><div id="canvas">
214
+
215
+ <!-- ================= zones ================= -->
216
+ <div class="zone zone-tint" style="left:24px;top:36px;width:1832px;height:360px"></div>
217
+ <div class="zone zone-hot" style="left:24px;top:420px;width:1832px;height:776px"></div>
218
+ <div class="zone zone-tint" style="left:24px;top:1240px;width:1832px;height:368px"></div>
219
+ <div class="zone zone-tint" style="left:24px;top:1648px;width:1832px;height:404px"></div>
220
+
221
+ <div class="zone-h" style="left:48px;top:52px"><span class="zn">01</span>Offline indexing<small>scripts/build_index.py · build_fewshot_index.py — однократно при setup / смене схемы</small></div>
222
+ <div class="zone-h" style="left:48px;top:436px"><span class="zn">02</span>Online query pipeline<small>agent/graph.py · LangGraph StateGraph · PipelineState</small></div>
223
+ <div class="zone-h" style="left:48px;top:1258px"><span class="zn">03</span>LLM provider layer<small>единый Protocol · кэш как основа $0-бюджета</small></div>
224
+ <div class="zone-h" style="left:48px;top:1666px"><span class="zn">04</span>Eval harness &amp; audit<small>ablation → voting → merge → re-score → audit · residue-цикл ×31 (47% → 94.0%)</small></div>
225
+
226
+ <!-- ================= zone A: offline indexing ================= -->
227
+ <div class="node" id="src" style="left:48px;top:88px;width:248px;height:216px">
228
+ <h3>Data sources</h3>
229
+ <p class="path">scripts/download_data.py → data/</p>
230
+ <ul>
231
+ <li><b>BIRD Mini-Dev</b> — 11 SQLite БД · 500 Q→SQL (dev)</li>
232
+ <li><b>BIRD train</b> — 9 428 Q→SQL пар (HF parquet)</li>
233
+ <li><b>Chinook.sqlite</b> — smoke</li>
234
+ <li><b>PostgreSQL 16</b> — docker-compose, opt-in</li>
235
+ </ul>
236
+ <div class="badges"><span class="bdg">SQLite</span><span class="bdg">PostgreSQL 16</span></div>
237
+ </div>
238
+
239
+ <div class="node" id="intro" style="left:372px;top:104px;width:250px;height:160px">
240
+ <h3>Introspector</h3>
241
+ <p class="path">schema_index/introspector.py</p>
242
+ <p><b>SQLAlchemy reflection</b> (read-only): таблицы, колонки, PK/FK, top-K sample values, NULL count, distinct count.</p>
243
+ <div class="badges"><span class="bdg">SQLAlchemy</span></div>
244
+ </div>
245
+
246
+ <div class="node" id="chunk" style="left:700px;top:104px;width:250px;height:148px">
247
+ <h3>Chunker</h3>
248
+ <p class="path">schema_index/chunker.py</p>
249
+ <p><b>1 таблица = 1 card</b>: имя + колонки + типы + samples + FK от/к + business-hints; fk_targets → metadata.</p>
250
+ </div>
251
+
252
+ <div class="node" id="idx" style="left:1028px;top:104px;width:240px;height:148px">
253
+ <h3>Indexer</h3>
254
+ <p class="path">schema_index/indexer.py</p>
255
+ <p>Upsert со <b>stable chunk_id</b> — без дублей при переиндексации; API чтения — SchemaIndex.</p>
256
+ </div>
257
+
258
+ <div class="node hot" id="chroma" style="left:1352px;top:96px;width:256px;height:204px">
259
+ <h3>Vector store</h3>
260
+ <p class="path">ChromaDB · chroma_data/ (persistent)</p>
261
+ <ul>
262
+ <li><b>schema_chunks</b> — 1 запись = (db, table)</li>
263
+ <li><b>fewshot_qsql</b> — эмбеддится только вопрос; SQL + db_id + intent в metadata</li>
264
+ </ul>
265
+ <div class="badges"><span class="bdg a">chromadb</span></div>
266
+ </div>
267
+
268
+ <div class="node" id="fewshot" style="left:700px;top:304px;width:250px;height:88px">
269
+ <h3 style="font-size:13px">Few-shot builder</h3>
270
+ <p class="path">scripts/build_fewshot_index.py</p>
271
+ <p style="font-size:12px">Q→SQL <b>только из train</b> · hard guard от dev-leakage.</p>
272
+ </div>
273
+
274
+ <div class="node" id="embed" style="left:1028px;top:304px;width:240px;height:88px">
275
+ <h3 style="font-size:13px">Embeddings</h3>
276
+ <p class="path">mistral-embed · 1024-dim</p>
277
+ <p style="font-size:12px">CachingEmbeddingProvider → <b>diskcache</b>: реиндексация = 0 API.</p>
278
+ </div>
279
+
280
+ <div class="node" id="fkg" style="left:1672px;top:104px;width:168px;height:148px">
281
+ <h3>FK graph</h3>
282
+ <p class="path">in-memory dict</p>
283
+ <p style="font-size:12px">SchemaIndex.fk_graph из fk_targets. <b>Не в Chroma</b>: FK-рёбра не несут семантики.</p>
284
+ </div>
285
+
286
+ <!-- ================= zone B: clients column ================= -->
287
+ <div class="node hot" id="user" style="left:48px;top:510px;width:250px;height:64px">
288
+ <span class="step">1</span>
289
+ <h3>Пользователь</h3>
290
+ <p>вопрос на RU / EN</p>
291
+ </div>
292
+
293
+ <div class="node hot" id="st" style="left:48px;top:610px;width:250px;height:184px">
294
+ <h3>Streamlit UI</h3>
295
+ <p class="path">app/streamlit_app.py · 8 модулей</p>
296
+ <p>Chat + sample-вопросы · schema explorer · show-working trace · EN↔RU · режимы Accurate / Fast / Debug.</p>
297
+ <div class="badges"><span class="bdg">Streamlit</span><span class="bdg">Plotly</span><span class="bdg">@st.cache_resource</span></div>
298
+ </div>
299
+
300
+ <div class="node hot" id="api" style="left:48px;top:830px;width:250px;height:184px">
301
+ <h3>FastAPI</h3>
302
+ <p class="path">src/nl_sql/api/main.py</p>
303
+ <p>POST /ask · GET /databases · /healthz · /readyz · /eval/latest. X-API-Key + token bucket <span class="mono">60 req/min</span> · Singletons DI.</p>
304
+ <div class="badges"><span class="bdg">FastAPI</span><span class="bdg">Pydantic v2</span><span class="bdg">uvicorn</span></div>
305
+ </div>
306
+
307
+ <div class="node" id="hf" style="left:48px;top:1050px;width:250px;height:106px">
308
+ <h3 style="font-size:13px">Hugging Face Spaces</h3>
309
+ <p class="path">liovina-nl-sql.hf.space</p>
310
+ <p style="font-size:12px">Docker free tier · UI + API в одном контейнере · deploy ≈ 90 с.</p>
311
+ </div>
312
+
313
+ <!-- ================= zone B: LangGraph row 1 ================= -->
314
+ <div class="node hot" id="ctx" style="left:400px;top:510px;width:270px;height:184px">
315
+ <span class="step">2</span>
316
+ <h3>context_builder</h3>
317
+ <p class="path">schema_index/retriever.py</p>
318
+ <p>Dense top-k=5 schema-чанков (filter db_id) → <b>FK BFS ≤1 hop</b>, бюджет 12 таблиц → few-shot k=3 (cross-db на BIRD) → extended samples 3→5 → dialect hints. Выход: <b>ContextBundle</b>.</p>
319
+ </div>
320
+
321
+ <div class="node dashed" id="plan" style="left:742px;top:424px;width:270px;height:66px">
322
+ <h3 style="font-size:13px">plan_query<span class="tag opt">opt-in</span></h3>
323
+ <p style="font-size:12px">JSON-скелет: tables · joins · filters · group_by · sort · limit</p>
324
+ </div>
325
+
326
+ <div class="node hot" id="gen" style="left:742px;top:510px;width:270px;height:184px">
327
+ <span class="step">3</span>
328
+ <h3>generate_sql</h3>
329
+ <p class="path">codestral-latest · T=0</p>
330
+ <p>Structured JSON: <span class="mono" style="font-size:11px">{sql, rationale, tables_used, confidence}</span>. Шаблоны: cards | M-Schema (XiYan) | DAC (CHASE-SQL) + <b>P3.F hints</b> — 11 правил, gated db_id+phrase.</p>
331
+ </div>
332
+
333
+ <div class="node hot" id="val" style="left:1084px;top:510px;width:250px;height:184px">
334
+ <span class="step">4</span>
335
+ <h3>validate · AST guard</h3>
336
+ <p class="path">execution/guards.py · sqlglot</p>
337
+ <p>SELECT-only · no DML/DDL в дереве · 1 statement · function denylist (pg_sleep, load_extension…) · denied tables · ATTACH / PRAGMA block.</p>
338
+ <div class="badges"><span class="bdg a">sqlglot</span></div>
339
+ </div>
340
+
341
+ <div class="node hot" id="exe" style="left:1406px;top:510px;width:270px;height:196px">
342
+ <span class="step">5</span>
343
+ <h3>execute · read-only</h3>
344
+ <p class="path">execution/runner.py → db/connection.py</p>
345
+ <p>SQLite: URI <span class="mono" style="font-size:11px">mode=ro</span> + PRAGMA query_only + progress-deadline; Postgres: read-only транзакции + statement_timeout 30 s; <b>row cap 10 000</b>.</p>
346
+ <div class="badges"><span class="bdg a">3-layer safety</span><span class="bdg">SQLAlchemy</span></div>
347
+ </div>
348
+
349
+ <div class="node" id="rep" style="left:1042px;top:752px;width:250px;height:110px">
350
+ <span class="step alt">×1</span>
351
+ <h3>repair_once</h3>
352
+ <p class="path">agent/nodes/repair_once.py</p>
353
+ <p style="font-size:12px"><b>Ровно 1 повтор</b> с error-context (guard repair_attempted): validate-fail / runtime-fail / empty (G) / critique-fail.</p>
354
+ </div>
355
+
356
+ <!-- ================= zone B: LangGraph row 2 ================= -->
357
+ <div class="node dashed" id="crit" style="left:1406px;top:936px;width:270px;height:84px">
358
+ <h3 style="font-size:13px">grounded_critique<span class="tag opt">opt-in</span></h3>
359
+ <p style="font-size:12px">Row-shape проверка результата → не более 1 retry.</p>
360
+ </div>
361
+
362
+ <div class="node hot" id="fmt" style="left:1080px;top:936px;width:260px;height:176px">
363
+ <span class="step">6</span>
364
+ <h3>deterministic_format<span class="tag nollm">no LLM</span></h3>
365
+ <p class="path">render/picker.py · formats.py</p>
366
+ <p>Чистый Python, эвристи��и по shape результата: <b>Scalar · Sentence · Table · Line · Bar · Pie · Scatter</b>.</p>
367
+ <div class="badges"><span class="bdg">Plotly</span></div>
368
+ </div>
369
+
370
+ <div class="node hot" id="exp" style="left:748px;top:936px;width:260px;height:176px">
371
+ <span class="step">7</span>
372
+ <h3>explain_trace</h3>
373
+ <p class="path">mistral-large-latest</p>
374
+ <p>NL-caption ≤ 2 предложений; финализация trace (model, tokens, latency, confidence по каждому узлу).</p>
375
+ </div>
376
+
377
+ <div class="node hot" id="ans" style="left:400px;top:936px;width:270px;height:176px">
378
+ <span class="step">8</span>
379
+ <h3>Ответ · AskResponse</h3>
380
+ <p class="path">PipelineRunResult</p>
381
+ <p>answer + SQL (подсветка) + rationale + confidence + caption + полный trace. Error taxonomy: <span class="mono" style="font-size:10.5px">invalid_sql · execution_timeout · execution_failed · empty_result · low_confidence · repair_failed</span></p>
382
+ </div>
383
+
384
+ <!-- ================= zone C: providers ================= -->
385
+ <div class="node" id="proto" style="left:400px;top:1300px;width:290px;height:158px">
386
+ <h3>LLMProvider Protocol</h3>
387
+ <p class="path">llm/providers/base.py · factory.py</p>
388
+ <p>PEP 544 runtime_checkable · <span class="mono" style="font-size:11px">build_provider(name)</span> — смена модели = env var · ProviderError taxonomy. Embed-протокол отдельно.</p>
389
+ </div>
390
+
391
+ <div class="node hot" id="cache" style="left:760px;top:1300px;width:290px;height:158px">
392
+ <h3>Caching layer</h3>
393
+ <p class="path">llm/cache.py · diskcache</p>
394
+ <p>Ключ <span class="mono" style="font-size:10.5px">sha256(provider · model · system · prompt · T · max_tok)</span>: hit = 0 quota, 0 latency — основа <b>$0-бюджета</b>. В тестах — fake-провайдеры, CI без live API.</p>
395
+ <div class="badges"><span class="bdg a">diskcache</span></div>
396
+ </div>
397
+
398
+ <div class="group" id="provgroup" style="left:1120px;top:1268px;width:724px;height:330px">
399
+ <span class="gt">7 provider-модулей + GraceKelly browser-мост · voting / residue-слои</span>
400
+ </div>
401
+ <div class="node pcard" id="p-mistral" style="left:1136px;top:1316px;width:160px;height:124px">
402
+ <h3>Mistral<span class="tag pri">primary</span></h3>
403
+ <p>codestral-latest (SQL) · mistral-large (NL) · mistral-embed. La Plateforme free.</p>
404
+ </div>
405
+ <div class="node pcard" id="p-groq" style="left:1312px;top:1316px;width:160px;height:124px">
406
+ <h3>Groq</h3>
407
+ <p>llama-3.3-70b · qwen3-32b · gpt-oss. TPM/TPD-bounded.</p>
408
+ </div>
409
+ <div class="node pcard" id="p-gh" style="left:1488px;top:1316px;width:160px;height:124px">
410
+ <h3>GitHub Models</h3>
411
+ <p>gpt-4o-mini · auth по PAT · OpenAI-compatible SDK.</p>
412
+ </div>
413
+ <div class="node pcard" id="p-or" style="left:1664px;top:1316px;width:160px;height:124px">
414
+ <h3>OpenRouter</h3>
415
+ <p>deepseek-v4-flash:free + 24 беспл. reasoning/code-моделей.</p>
416
+ </div>
417
+ <div class="node pcard" id="p-ollama" style="left:1136px;top:1456px;width:160px;height:124px">
418
+ <h3>Ollama</h3>
419
+ <p>local · qwen2.5-coder:7b — offline-слот bakeoff.</p>
420
+ </div>
421
+ <div class="node pcard" id="p-pplx" style="left:1312px;top:1456px;width:160px;height:124px">
422
+ <h3>Perplexity / helallao</h3>
423
+ <p>GPT-5.2 · Grok-4.1 · Claude-4.5 · Kimi-K2 (reasoning / Pro).</p>
424
+ </div>
425
+ <div class="node pcard" id="p-gk" style="left:1488px;top:1456px;width:160px;height:124px">
426
+ <h3>GraceKelly</h3>
427
+ <p>browser-orchestrator → Sonnet 4.6 (eval-only мост).</p>
428
+ </div>
429
+ <div class="note" style="left:1664px;top:1456px;width:160px;height:124px">
430
+ <b>$0 hard constraint</b> — free tiers + кэш + user-подписки; ротация аккаунтов запрещена.
431
+ </div>
432
+
433
+ <!-- ================= zone D: eval ================= -->
434
+ <div class="node" id="ds" style="left:48px;top:1704px;width:250px;height:158px">
435
+ <h3>Dataset</h3>
436
+ <p class="path">eval/dataset.py</p>
437
+ <p>BIRD Mini-Dev loader · <span class="mono" style="font-size:11px">dev_split(seed=0, n)</span> — stable-prefix: n=50 ⊂ n=200 → кэш промптов переиспользуется.</p>
438
+ </div>
439
+
440
+ <div class="node" id="runner" style="left:372px;top:1704px;width:260px;height:158px">
441
+ <h3>Ablation runner</h3>
442
+ <p class="path">eval/runner.py</p>
443
+ <p>Конфиги A · C · D · E · F (T-sweep 0.2–0.8) · G (B=BM25, N/I): A–D без repair, G + verify_retry_on_empty → <span class="mono" style="font-size:11px">eval/reports/*.json</span>.</p>
444
+ </div>
445
+
446
+ <div class="node" id="voting" style="left:706px;top:1704px;width:290px;height:194px">
447
+ <h3>Voting / rescue scripts</h3>
448
+ <p class="path">scripts/run_*.py</p>
449
+ <p>groq_voting · sonnet (GraceKelly) · helallao · openrouter · critique_retry · selfcon (T-sweep) · wide_schema · ensemble_vote. Работают по <b>residue</b> (misses) поверх v_N.</p>
450
+ </div>
451
+
452
+ <div class="node" id="merge" style="left:1070px;top:1704px;width:250px;height:158px">
453
+ <h3>Merge</h3>
454
+ <p class="path">scripts/merge_voting_rescues.py</p>
455
+ <p><span class="mono" style="font-size:11px">--reverify</span> — re-exec через safe_compare_pred · archive_sweep / rescore → merged baseline v<sub>N+1</sub>.</p>
456
+ </div>
457
+
458
+ <div class="node" id="metrics" style="left:1394px;top:1704px;width:200px;height:158px">
459
+ <h3>Metrics</h3>
460
+ <p class="path">eval/metrics/</p>
461
+ <p style="font-size:12px">execution_accuracy — <b>BIRD-official set-equality</b> · safe_compare_pred (pred-fail → False) · schema_recall@k.</p>
462
+ </div>
463
+
464
+ <div class="node" id="audit" style="left:1640px;top:1704px;width:204px;height:194px">
465
+ <h3>Audit gates</h3>
466
+ <p class="path">scripts/</p>
467
+ <p style="font-size:12px">audit_rescore — row-by-row re-execution · p3f_acceptance — 11 gates (req/forbidden columns по AST) · error_taxonomy buckets · refresh_baseline_summary.</p>
468
+ </div>
469
+
470
+ <div class="node hot" id="result" style="left:1070px;top:1944px;width:524px;height:92px">
471
+ <h3>v31 baseline · <span class="numeric">94.0% EA (188/200) · 0 mismatches</span></h3>
472
+ <p style="font-size:12px">→ README headline · GET /eval/latest · HF redeploy. Lift trace: 47% (config A) → 94.0% (v31), 31 версия, каждая с negative/saturation evidence.</p>
473
+ </div>
474
+
475
+ <!-- ================= zone E: CI ribbon ================= -->
476
+ <div class="ribbon" style="left:48px;top:2104px;width:1784px;height:92px">
477
+ <span class="rt">05 · Quality gates &amp; CI/CD</span>
478
+ <span class="bdg">pytest · 370 green</span>
479
+ <span class="bdg">coverage 91%</span>
480
+ <span class="bdg">mypy --strict · 0 issues / 59 files</span>
481
+ <span class="bdg">ruff check + format</span>
482
+ <span class="bdg">GitHub Actions · Ubuntu · py3.13 · uv</span>
483
+ <span class="bdg">uv.lock pinned + requirements-guard</span>
484
+ <span class="bdg">fake-провайдеры — CI без live API</span>
485
+ <span class="bdg">.deploy_hf.py + Playwright E2E grep-gate</span>
486
+ <span class="bdg">Makefile</span>
487
+ <span class="bdg">docker-compose · postgres / langfuse profiles</span>
488
+ </div>
489
+
490
+ <svg id="wires" width="1880" height="2236">
491
+ <defs>
492
+ <marker id="m-hot" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7.5" markerHeight="7.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker>
493
+ <marker id="m-rep" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker>
494
+ <marker id="m-bus" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#9C5B38"/></marker>
495
+ <marker id="m-idx" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.45)"/></marker>
496
+ <marker id="m-opt" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6.5" markerHeight="6.5" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.45)"/></marker>
497
+ <marker id="m-eval" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="rgba(28,28,26,0.55)"/></marker>
498
+ </defs>
499
+ </svg>
500
+
501
+ </div></div></div>
502
+
503
+ <footer>
504
+ Источник: <span class="mono">src/nl_sql/* · app/* · eval/* · scripts/*</span> @ <span class="mono">8e6c61d</span> · baseline v31 (2026-05-26) · схема сгенерирована 2026-06-06 · подробности: <span class="mono">docs/02_architecture_v2.md</span>, <span class="mono">docs/03_eval_methodology.md</span>, <span class="mono">docs/SESSION_HANDOFF.md</span> · печатная версия: <a href="data_flow.pdf" style="color:var(--accent-dark)">data_flow.pdf</a> · зум: кнопки справа внизу или клавиши <span class="mono">+ − 0</span>
505
+ </footer>
506
+
507
+ <script>
508
+ /* ---------- edge definitions ---------- */
509
+ /* f/t: node id; fs/ts: side l|r|t|b; ff/tf: fraction along side (0..1, default .5);
510
+ k: kind; lb: label; la: label position 0..1; via: waypoints; c: curvature override */
511
+ const EDGES = [
512
+ /* zone A: offline indexing */
513
+ {f:'src',fs:'r',t:'intro',ts:'l',k:'idx',lb:'engine (ro)'},
514
+ {f:'intro',fs:'r',t:'chunk',ts:'l',k:'idx',lb:'TableInfo'},
515
+ {f:'chunk',fs:'r',t:'idx',ts:'l',k:'idx',lb:'SchemaChunk'},
516
+ {f:'idx',fs:'r',t:'chroma',ts:'l',k:'idx',lb:'upsert'},
517
+ {f:'src',fs:'r',ff:.9,t:'fewshot',ts:'l',k:'idx',lb:'train split',la:.5,c:.2},
518
+ {f:'fewshot',fs:'t',ff:.6,t:'idx',ts:'b',tf:.3,k:'idx',lb:'Q→SQL · 9 428 train',la:.5,dy:-4},
519
+ {f:'idx',fs:'b',ff:.62,t:'embed',ts:'t',k:'idx',lb:'texts',la:.4},
520
+ {f:'embed',fs:'r',t:'chroma',ts:'b',tf:.35,k:'idx',lb:'vectors 1024-d',la:.5},
521
+ {f:'chroma',fs:'r',t:'fkg',ts:'l',k:'idx',lb:'fk_targets'},
522
+ /* query-time retrieval */
523
+ {f:'chroma',fs:'b',ff:.55,t:'ctx',ts:'t',tf:.25,k:'hot',lb:'top-k=5 schema + k=3 few-shot (db_id)',la:.45,via:[[1493,412],[467,412]]},
524
+ {f:'fkg',fs:'b',ff:.5,t:'ctx',ts:'t',tf:.72,k:'hot',lb:'FK BFS ≤1 hop · budget 12',la:.42,via:[[1756,400],[594,400]]},
525
+ /* hot path */
526
+ {f:'user',fs:'b',t:'st',ts:'t',k:'hot'},
527
+ {f:'st',fs:'r',ff:.35,t:'ctx',ts:'l',tf:.35,k:'hot',lb:'question · db_id · dialect',la:.45},
528
+ {f:'api',fs:'r',ff:.3,t:'ctx',ts:'l',tf:.78,k:'hot',lb:'POST /ask',la:.4},
529
+ {f:'ctx',fs:'r',ff:.82,t:'gen',ts:'l',tf:.82,k:'hot',lb:'ContextBundle → prompt',dy:30},
530
+ {f:'gen',fs:'r',ff:.82,t:'val',ts:'l',tf:.82,k:'hot',lb:'sql · rationale · confidence',dy:36},
531
+ {f:'val',fs:'r',ff:.82,t:'exe',ts:'l',tf:.82,k:'hot',lb:'AST-clean SQL',dy:30},
532
+ {f:'exe',fs:'b',ff:.32,t:'fmt',ts:'t',tf:.6,k:'hot',lb:'QueryResult · rows ≤ 10 000',la:.6,via:[[1492,880],[1236,880]]},
533
+ {f:'fmt',fs:'b',ff:.2,t:'exp',ts:'b',tf:.8,k:'hot',lb:'OutputFormat (1 из 7)',la:.5,c:.22},
534
+ {f:'exp',fs:'l',t:'ans',ts:'r',k:'hot',lb:'caption'},
535
+ {f:'ans',fs:'l',ff:.3,t:'st',ts:'r',tf:.72,k:'hot',lb:'answer + SQL + trace',la:.5},
536
+ {f:'ans',fs:'l',ff:.72,t:'api',ts:'r',tf:.65,k:'hot',lb:'AskResponse',la:.45},
537
+ /* repair loops */
538
+ {f:'val',fs:'b',ff:.55,t:'rep',ts:'t',tf:.62,k:'rep',lb:'fail + error ctx',la:.5},
539
+ {f:'rep',fs:'t',ff:.12,t:'val',ts:'b',tf:.12,k:'rep',lb:'SQL v2 · ×1',la:.5},
540
+ {f:'exe',fs:'b',ff:.1,t:'rep',ts:'r',tf:.2,k:'rep',lb:'runtime / empty (G)',la:.45,c:.3},
541
+ /* opt-in */
542
+ {f:'ctx',fs:'t',ff:.6,t:'plan',ts:'l',k:'opt',la:.5,c:.4},
543
+ {f:'plan',fs:'b',ff:.5,t:'gen',ts:'t',tf:.5,k:'opt',lb:'JSON skeleton',la:.5},
544
+ {f:'exe',fs:'b',ff:.78,t:'crit',ts:'t',tf:.5,k:'opt',lb:'opt-in',la:.5},
545
+ {f:'crit',fs:'l',t:'fmt',ts:'r',tf:.25,k:'opt',lb:'ok',la:.5},
546
+ {f:'crit',fs:'t',ff:.3,t:'rep',ts:'b',tf:.7,k:'opt',lb:'fail ×1',la:.55,via:[[1487,906],[1247,906]]},
547
+ /* LLM bus */
548
+ {f:'gen',fs:'b',ff:.5,t:'proto',ts:'l',tf:.5,k:'bus',lb:'LLM-вызовы: plan · generate · repair · explain',la:.22,via:[[877,716],[349,716],[349,1379]]},
549
+ {f:'exp',fs:'b',ff:.5,t:'proto',ts:'l',tf:.5,k:'bus',via:[[878,1146],[349,1146],[349,1379]]},
550
+ {f:'proto',fs:'r',t:'cache',ts:'l',k:'bus',lb:'wrap'},
551
+ {f:'cache',fs:'b',ff:.9,t:'provgroup',ts:'l',tf:.8,k:'bus',lb:'HTTP только при cache-miss',la:.55,dy:12,c:.3},
552
+ /* eval loop */
553
+ {f:'ds',fs:'r',t:'runner',ts:'l',k:'eval',lb:'n=200 · seed=0',dy:-8},
554
+ {f:'runner',fs:'r',t:'voting',ts:'l',k:'eval',lb:'residue v_N'},
555
+ {f:'voting',fs:'r',t:'merge',ts:'l',k:'eval',lb:'verified rescues'},
556
+ {f:'merge',fs:'r',t:'metrics',ts:'l',k:'eval',lb:'re-score'},
557
+ {f:'metrics',fs:'r',t:'audit',ts:'l',k:'eval'},
558
+ {f:'audit',fs:'b',ff:.5,t:'result',ts:'r',tf:.5,k:'eval',lb:'0 mismatches',la:.5,c:.4},
559
+ {f:'result',fs:'l',ff:.5,t:'voting',ts:'b',tf:.6,k:'eval',lb:'residue-loop ×31',la:.5,c:.4},
560
+ {f:'runner',fs:'t',ff:.72,t:'ans',ts:'b',tf:.82,k:'eval',lb:'run_pipeline() на каждый qid',la:.35,via:[[559,1676],[725,1676],[725,1150]]},
561
+ {f:'voting',fs:'t',ff:.5,t:'cache',ts:'b',tf:.5,k:'eval',lb:'те же providers',la:.5},
562
+ /* hosting */
563
+ {f:'hf',fs:'t',ff:.5,t:'api',ts:'b',tf:.5,k:'opt',lb:'Docker',la:.5},
564
+ ];
565
+
566
+ const canvas=document.getElementById('canvas');
567
+ const svg=document.getElementById('wires');
568
+ const NS='http://www.w3.org/2000/svg';
569
+
570
+ function anchor(id,side,frac){
571
+ const el=document.getElementById(id);
572
+ const x=el.offsetLeft,y=el.offsetTop,w=el.offsetWidth,h=el.offsetHeight;
573
+ const f=(frac==null?0.5:frac);
574
+ switch(side){
575
+ case 'l':return{x:x, y:y+h*f, nx:-1,ny:0};
576
+ case 'r':return{x:x+w, y:y+h*f, nx:1, ny:0};
577
+ case 't':return{x:x+w*f, y:y, nx:0, ny:-1};
578
+ case 'b':return{x:x+w*f, y:y+h, nx:0, ny:1};
579
+ }
580
+ }
581
+
582
+ function buildPath(e){
583
+ const a=anchor(e.f,e.fs,e.ff), b=anchor(e.t,e.ts,e.tf);
584
+ if(e.via){
585
+ let d=`M ${a.x},${a.y}`;
586
+ e.via.forEach(p=>{d+=` L ${p[0]},${p[1]}`;});
587
+ d+=` L ${b.x},${b.y}`;
588
+ return d;
589
+ }
590
+ const dist=Math.hypot(b.x-a.x,b.y-a.y);
591
+ const k=dist*(e.c!=null?e.c:0.38);
592
+ const c1x=a.x+a.nx*k, c1y=a.y+a.ny*k;
593
+ const c2x=b.x+b.nx*k, c2y=b.y+b.ny*k;
594
+ return `M ${a.x},${a.y} C ${c1x},${c1y} ${c2x},${c2y} ${b.x},${b.y}`;
595
+ }
596
+
597
+ EDGES.forEach(e=>{
598
+ const g=document.createElementNS(NS,'g');
599
+ g.setAttribute('class','edge');
600
+ g.dataset.f=e.f; g.dataset.t=e.t;
601
+ const p=document.createElementNS(NS,'path');
602
+ p.setAttribute('d',buildPath(e));
603
+ p.setAttribute('class','l '+e.k);
604
+ p.setAttribute('marker-end',`url(#m-${e.k})`);
605
+ if(e.via){p.setAttribute('stroke-linejoin','round');}
606
+ g.appendChild(p);
607
+ if(e.lb){
608
+ const len=p.getTotalLength();
609
+ const pt=p.getPointAtLength(len*(e.la!=null?e.la:0.5));
610
+ const tx=document.createElementNS(NS,'text');
611
+ tx.setAttribute('x',pt.x+(e.dx||0)); tx.setAttribute('y',pt.y+4+(e.dy||0));
612
+ tx.setAttribute('text-anchor','middle');
613
+ tx.setAttribute('class',(e.k==='hot'||e.k==='rep'||e.k==='bus')?'lt-hot':'lt-mut');
614
+ tx.textContent=e.lb;
615
+ g.appendChild(tx);
616
+ svg.appendChild(g);
617
+ const bb=tx.getBBox();
618
+ const r=document.createElementNS(NS,'rect');
619
+ r.setAttribute('x',bb.x-5);r.setAttribute('y',bb.y-2);
620
+ r.setAttribute('width',bb.width+10);r.setAttribute('height',bb.height+4);
621
+ r.setAttribute('rx',3);r.setAttribute('class','lbg');
622
+ g.insertBefore(r,tx);
623
+ } else {
624
+ svg.appendChild(g);
625
+ }
626
+ });
627
+
628
+ /* hover focus: highlight a node's edges + neighbours */
629
+ document.querySelectorAll('.node,.group').forEach(n=>{
630
+ n.addEventListener('mouseenter',()=>{
631
+ canvas.classList.add('focus');
632
+ n.classList.add('on');
633
+ document.querySelectorAll('.edge').forEach(ed=>{
634
+ if(ed.dataset.f===n.id||ed.dataset.t===n.id){
635
+ ed.classList.add('on');
636
+ const other=document.getElementById(ed.dataset.f===n.id?ed.dataset.t:ed.dataset.f);
637
+ if(other)other.classList.add('on');
638
+ }
639
+ });
640
+ });
641
+ n.addEventListener('mouseleave',()=>{
642
+ canvas.classList.remove('focus');
643
+ document.querySelectorAll('.on').forEach(x=>x.classList.remove('on'));
644
+ });
645
+ });
646
+
647
+ /* ---------- scaling + zoom ---------- */
648
+ let userZoom=null; /* null = fit to viewport */
649
+ function fitScale(){
650
+ const vw=document.getElementById('viewport').clientWidth;
651
+ return Math.min(1,(vw-16)/1880);
652
+ }
653
+ function rescale(){
654
+ const vp=document.getElementById('viewport');
655
+ const s=userZoom!=null?userZoom:fitScale();
656
+ const sc=document.getElementById('scaler');
657
+ sc.style.transform=`scale(${s})`;
658
+ sc.style.width='1880px';
659
+ sc.style.height=(2236*s)+'px';
660
+ vp.style.overflowX=(1880*s>vp.clientWidth)?'auto':'hidden';
661
+ document.getElementById('zval').textContent=Math.round(s*100)+'%';
662
+ }
663
+ document.querySelectorAll('#zoombar button').forEach(btn=>{
664
+ btn.addEventListener('click',()=>{
665
+ const cur=userZoom!=null?userZoom:fitScale();
666
+ if(btn.dataset.z==='in') userZoom=Math.min(1.6,Math.round((cur+0.15)*100)/100);
667
+ if(btn.dataset.z==='out') userZoom=Math.max(0.3,Math.round((cur-0.15)*100)/100);
668
+ if(btn.dataset.z==='fit') userZoom=null;
669
+ rescale();
670
+ });
671
+ });
672
+ window.addEventListener('keydown',e=>{
673
+ if(e.target.tagName==='INPUT'||e.ctrlKey||e.metaKey)return;
674
+ if(e.key==='+'||e.key==='='){userZoom=Math.min(1.6,(userZoom!=null?userZoom:fitScale())+0.15);rescale();}
675
+ if(e.key==='-'){userZoom=Math.max(0.3,(userZoom!=null?userZoom:fitScale())-0.15);rescale();}
676
+ if(e.key==='0'){userZoom=null;rescale();}
677
+ });
678
+ window.addEventListener('resize',rescale);
679
+ rescale();
680
+
681
+ /* mobile stub */
682
+ if(window.innerWidth<900){
683
+ document.getElementById('mobile-stub').style.display='flex';
684
+ document.body.style.overflow='hidden';
685
+ }
686
+ </script>
687
+ </body>
688
+ </html>
docs/data_flow.pdf ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b128a8658f7f2c5ba0feb96f8a28dbb1f7199291973b5ac34eba2b0d8475d8cb
3
+ size 621974
scripts/rescore_arcwise.py CHANGED
@@ -147,6 +147,16 @@ def main() -> int:
147
  per_diff[variant][difficulty][0] += int(is_match)
148
 
149
  # Transitions vs sql_only and vs full.
 
 
 
 
 
 
 
 
 
 
150
  for variant in ("sql_only", "full"):
151
  v_match = out_entry.get(f"{variant}_match")
152
  if v_match is None:
@@ -158,11 +168,11 @@ def main() -> int:
158
  )
159
  if gold_changed:
160
  out_entry[f"{variant}_gold_changed"] = True
161
- if orig_match and not v_match:
162
  transitions["lost"].append(
163
  {"qid": qid, "variant": variant, "difficulty": difficulty}
164
  )
165
- elif (not orig_match) and v_match:
166
  transitions["gained"].append(
167
  {"qid": qid, "variant": variant, "difficulty": difficulty}
168
  )
 
147
  per_diff[variant][difficulty][0] += int(is_match)
148
 
149
  # Transitions vs sql_only and vs full.
150
+ # Use the FRESHLY-re-executed original-variant match (written at
151
+ # `out_entry["original_match"]` by the variant loop above) rather
152
+ # than the stale `orig_match` snapshot from the input JSON — the
153
+ # whole point of rescore is to apply current scoring semantics, so
154
+ # mixing in stored EA bits would produce a hybrid that doesn't
155
+ # match either run cleanly (Codex audit 2026-05-25 #7). On the
156
+ # current canonical v22-v30 baselines the two values agree on
157
+ # every record, so this is a future-proofing change; no
158
+ # observable difference in committed reports.
159
+ fresh_orig = bool(out_entry.get("original_match", orig_match))
160
  for variant in ("sql_only", "full"):
161
  v_match = out_entry.get(f"{variant}_match")
162
  if v_match is None:
 
168
  )
169
  if gold_changed:
170
  out_entry[f"{variant}_gold_changed"] = True
171
+ if fresh_orig and not v_match:
172
  transitions["lost"].append(
173
  {"qid": qid, "variant": variant, "difficulty": difficulty}
174
  )
175
+ elif (not fresh_orig) and v_match:
176
  transitions["gained"].append(
177
  {"qid": qid, "variant": variant, "difficulty": difficulty}
178
  )