Deploy NL_SQL HEAD to HF Space
Browse files- .gitattributes +1 -0
- .streamlit/config.toml +7 -6
- 27_05_26.md +535 -0
- app/components/output.py +2 -2
- app/components/welcome.py +11 -34
- app/i18n.py +4 -0
- app/static/fonts/jbmono-cyrillic.woff2 +0 -0
- app/static/fonts/jbmono-latin.woff2 +0 -0
- app/static/fonts/manrope-cyrillic.woff2 +0 -0
- app/static/fonts/manrope-latin.woff2 +0 -0
- app/streamlit_app.py +93 -55
- app/theme.py +334 -260
- audit_codex_30_05_26.md +434 -0
- chroma_data/chroma.sqlite3 +1 -1
- chroma_data/fc9668d3-4384-40d9-aa8d-0010807a5a68/data_level0.bin +1 -1
- chroma_data/fc9668d3-4384-40d9-aa8d-0010807a5a68/length.bin +1 -1
- docs/NEXT_SESSION.md +36 -4
- docs/SESSION_HANDOFF.md +8 -0
- docs/data_flow.html +688 -0
- docs/data_flow.pdf +3 -0
- scripts/rescore_arcwise.py +12 -2
.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 |
-
#
|
| 3 |
-
#
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
| 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-
|
| 68 |
-
f"
|
| 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 +
|
| 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("
|
| 67 |
</div>
|
| 68 |
""",
|
| 69 |
unsafe_allow_html=True,
|
| 70 |
)
|
| 71 |
|
| 72 |
-
|
| 73 |
-
if not samples:
|
| 74 |
st.markdown(
|
| 75 |
-
f"<div class='nl-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 49 |
-
|
| 50 |
-
st.
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 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 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
|
|
|
|
|
|
|
| 76 |
st.markdown(
|
| 77 |
-
|
| 78 |
unsafe_allow_html=True,
|
| 79 |
)
|
| 80 |
-
|
| 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:
|
| 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
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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: '
|
| 17 |
-
src: url('/app/static/fonts/
|
| 18 |
-
font-weight:
|
| 19 |
-
|
| 20 |
-
|
| 21 |
}
|
| 22 |
@font-face {
|
| 23 |
-
font-family: '
|
| 24 |
-
src: url('/app/static/fonts/
|
| 25 |
-
font-weight:
|
| 26 |
-
|
| 27 |
-
font-display: swap;
|
| 28 |
}
|
| 29 |
@font-face {
|
| 30 |
-
font-family: '
|
| 31 |
-
src: url('/app/static/fonts/
|
| 32 |
-
font-weight:
|
| 33 |
-
|
| 34 |
-
|
| 35 |
}
|
| 36 |
@font-face {
|
| 37 |
-
font-family: '
|
| 38 |
-
src: url('/app/static/fonts/
|
| 39 |
-
font-weight: 400;
|
| 40 |
-
|
| 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 |
-
--
|
| 53 |
-
--
|
| 54 |
-
--
|
| 55 |
-
--
|
| 56 |
-
--
|
| 57 |
-
--
|
| 58 |
-
--
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
padding-bottom: 4rem;
|
| 70 |
-
max-width:
|
| 71 |
}
|
| 72 |
|
| 73 |
/* Hide Streamlit chrome we don't want */
|
| 74 |
-
#MainMenu, footer,
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
/*
|
| 78 |
-
.
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
font-weight: 700;
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
}
|
| 93 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
.nl-tagline {
|
| 95 |
-
font-
|
| 96 |
font-weight: 400;
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
margin: 0 0
|
| 102 |
}
|
| 103 |
|
| 104 |
-
/*
|
| 105 |
.nl-kicker {
|
| 106 |
-
font-
|
| 107 |
-
font-
|
| 108 |
-
letter-spacing: 0.
|
| 109 |
text-transform: uppercase;
|
| 110 |
-
color: var(--
|
| 111 |
-
margin-bottom: 0.
|
| 112 |
}
|
| 113 |
-
|
| 114 |
-
/* Metric block — pure typography, no card chrome */
|
| 115 |
.nl-metric {
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
.
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
margin-bottom: 0.5rem;
|
| 125 |
-
}
|
| 126 |
.nl-metric-value {
|
| 127 |
-
font-
|
| 128 |
-
font-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
color: var(--ink);
|
| 132 |
line-height: 1;
|
| 133 |
}
|
| 134 |
.nl-metric-aside {
|
| 135 |
-
font-
|
| 136 |
-
font-
|
| 137 |
-
color: var(--
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
.
|
| 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
|
| 159 |
.nl-section-label {
|
| 160 |
-
font-
|
| 161 |
-
font-
|
| 162 |
-
letter-spacing: 0.
|
| 163 |
text-transform: uppercase;
|
| 164 |
-
color: var(--
|
| 165 |
-
margin: 2.
|
| 166 |
-
border-top: 1px solid var(--hairline);
|
| 167 |
-
padding-top: 0.7rem;
|
| 168 |
}
|
| 169 |
|
| 170 |
-
/*
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 174 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
[data-testid="stSidebar"] .nl-side-h {
|
| 176 |
-
font-
|
| 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-
|
| 184 |
-
|
| 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
|
| 208 |
.stButton > button {
|
| 209 |
-
background:
|
| 210 |
-
color: var(--
|
| 211 |
-
border: 1px solid var(--
|
| 212 |
-
border-radius:
|
| 213 |
-
|
| 214 |
-
font-weight:
|
| 215 |
-
font-size: 0.
|
| 216 |
text-align: left !important;
|
| 217 |
-
padding: 0.
|
| 218 |
-
line-height: 1.
|
| 219 |
-
transition: background 0.12s;
|
| 220 |
white-space: normal !important;
|
| 221 |
height: auto !important;
|
|
|
|
|
|
|
| 222 |
}
|
| 223 |
.stButton > button:hover {
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
}
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
color: var(--
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
[data-testid="stMetric"] {
|
| 251 |
-
background:
|
| 252 |
-
border:
|
|
|
|
|
|
|
|
|
|
| 253 |
}
|
| 254 |
[data-testid="stMetricLabel"] {
|
| 255 |
-
font-
|
| 256 |
-
|
| 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 |
-
/*
|
| 281 |
-
|
| 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 |
-
/*
|
| 294 |
-
[data-testid="
|
| 295 |
-
|
| 296 |
-
border:
|
| 297 |
-
|
|
|
|
| 298 |
}
|
| 299 |
-
[data-testid="
|
| 300 |
-
|
| 301 |
-
padding-top: 1.4rem !important;
|
| 302 |
}
|
|
|
|
| 303 |
|
| 304 |
-
/*
|
|
|
|
|
|
|
| 305 |
[data-testid="stChatMessage"] > div:first-child,
|
| 306 |
-
[data-testid="chatAvatarIcon-user"],
|
| 307 |
-
[data-testid="
|
| 308 |
-
[data-testid="
|
| 309 |
-
[data-testid="
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 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 = ["#
|
| 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="
|
| 335 |
-
font_color="#
|
| 336 |
-
paper_bgcolor="#
|
| 337 |
-
plot_bgcolor="#
|
| 338 |
colorway=CHART_PALETTE,
|
| 339 |
margin=dict(l=10, r=10, t=20, b=10),
|
| 340 |
)
|
| 341 |
-
fig.update_xaxes(gridcolor="#
|
| 342 |
-
fig.update_yaxes(gridcolor="#
|
| 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:
|
| 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:
|
| 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:
|
| 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 |
|
| 20 |
-
| Codex #8 | P2 latent | `execution_accuracy.py` `_hashable` float bucketing — verified 0 set-mismatch in v22-
|
| 21 |
-
| Codex #10 | P2 latent | `cache.py:88` cache miss/fill race — fires только при parallel workers (not currently used) |
|
| 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 |
|
| 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 & 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 & 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
|
| 162 |
transitions["lost"].append(
|
| 163 |
{"qid": qid, "variant": variant, "difficulty": difficulty}
|
| 164 |
)
|
| 165 |
-
elif (not
|
| 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 |
)
|