Skip to content

docs(web): методологията описва точно таблата — формули, обхват, изключения - #193

Open
StanislavBG wants to merge 52 commits into
midt-bg:mainfrom
StanislavBG:docs/methodology-dashboards
Open

StanislavBG wants to merge 52 commits into
midt-bg:mainfrom
StanislavBG:docs/methodology-dashboards

Conversation

@StanislavBG

@StanislavBG StanislavBG commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Какво

Разширява /methodology с нов раздел „7. Аналитичните изгледи: какво показват и как се смятат" — по едно описание за всяко аналитично табло, сверено ред по ред срещу кода на loader-ите и заявките, а не преразказано по памет.

За всяко табло: какво показва визуалът, формулата с думи, обхватът и изключенията, и честните уговорки — в съществуващия тон (неутрален, описателен, „ориентир, не заключение"), на български, с препратки към речника и раздела за стойността.

Обхванати табла: Анализи (индекс), Договори — обзор (три ъгъла + типични цени по CPV), Конкуренция (една оферта + HHI), Раздуване след анекси, Потоци, Карта, Мрежа, Договори.

Ключови сверявания срещу кода:

  • Стойностна основа = изчистената стойност в евро (текуща при законен анекс, иначе при сключване); непотвърдените стойности се изключват от сумите.
  • Обзорът показва само реални стойности — без прогноза; текущият непълен период е скрит по подразбиране; YoY само между съседни години.
  • „Спрямо типичното" = стойност спрямо медианата на CPV класа (процентил, не z-score).
  • Раздуване = поне един анекс ∧ текуща > подписана ∧ подписана ≥ 1 000 €; растежът е претеглен по евро.
  • Конкуренция: една оферта (bids_received = 1, само с известен брой оферти, ≥ 20 договора); HHI = сбор от квадратите на дяловете (0–1, ≥ 0,25 висока концентрация, ≥ 2 доставчика).
  • Мрежа: шест най-големи преки контрагенти по стойност + по една втора връзка (фокусирана околност, не целият граф).
  • Карта: област по адреса на институцията (NUTS), непосочените — извън картата.

Обхват

Основно документация — methodology.tsx е единственият файл, променен от първоначалния commit.
Ревюто по-долу добави осем дребни code fixes в извън-documentation файлове, открити при
сверката с кода (виж „Ревю"); никой от тях не променя поведение извън описаните поправки.

Проверки

  • pnpm --filter @sigma/web typecheck — зелено
  • prettier --check . — зелено
  • pnpm --filter @sigma/web test — 377 теста зелени

Ревю (the reviewer, 2026-07-20)

12 коментара — всички разгледани и затворени:

  • 9 приети като code fixes, слети в 38847f9 (isomorphic layout-effect в MetricInfo, коректно
    третиране на partial точка на индекс 0 в ComboTrendChart, консистентен || fallback за ЕИК
    в overruns.tsx, защита от деление на нула и dedup на CPV групите в trends.tsx, поправени
    CSS маркер/дублиран селектор, премахнат неизползван import в cache-key.ts) и aeb950b
    (именуван authorityEik alias на authoritySlug в identity.ts за яснота — стойността остава
    непроменена).
  • 3 потвърдени вече коректни без промяна в кода (unit-конверсията в analytics-stats.ts,
    агрегацията година↔месец в estimateYoyGrowth, анкорите #source/#money в
    methodology.tsx) — обяснени в отговор на съответните нишки.

Всичките 12 нишки са resolved.

Зависимости

Draft: зависи от стека #169#172, защото описва таблата, които те въвеждат (петте карти на /analytics, обзорът на /trends, /overruns). Ще се извади от draft, след като стекът се слее.

Вид промяна

  • fix — поправка на бъг
  • feat — нова функционалност
  • docs — документация
  • refactor / perf / style — без промяна в поведението
  • test / ci / build / chore — поддръжка

Чеклист

  • Комитите следват conventional commits; не е изпълнено на ниво branch (2 комита с agent Co-Authored-By:, вече публикувани, не се пренаписват) — виж „Бележка при merge“ по-долу
  • PR-ът е с един логически обхват и е от форк към midt-bg/sigma:main
  • pnpm typecheck минава (CI: стъпка Typecheck е зелена)
  • pnpm test минава (CI: стъпка Test е зелена)
  • pnpm lint е чисто (CI: стъпка Lint е зелена)
  • Няма комитнати тайни, .env* или .dev.vars (gitleaks + semgrep са зелени)
  • Документацията в docs/ е обновена — не е приложимо за тази промяна

Бележка при merge

2 комита в този branch носят Co-Authored-By: trailer към агент (Claude). AGENTS.md го забранява на main, а squash-merge запазва trailer-ите от комитите. Историята не се пренаписва (без rebase/force-push), затова при merge моля заменете автоматично генерирания commit message със заглавието и тялото по-долу. Няма trailer-и към хора в този branch (всички комити са от автора на PR-а), така че не се губи credit.

Заглавие (subject):

docs(web): методологията описва точно таблата — формули, обхват, изключения

Тяло (body):

Разширява /methodology с раздел 7: по едно описание за всяко аналитично табло — формули, обхват, изключения и уговорки, сверени с кода на loader-ите.

The popover often renders inside a thead th whose white-space: nowrap is
inherited by every line of the card, so long summaries and the mono
readout ran past the right edge. Reset wrapping on .metric-info-pop
(white-space: normal + overflow-wrap: anywhere), widen the card to
320px clamped to the viewport (min(320px, 100vw - 16px)), and add the
JS shift-into-viewport + coarse-pointer 44px hit area so all copies of
the component behave identically.
The обзор cross lens now re-runs the combo chart, year cards and totals
server-side for the ticked CPV groups (repeatable ?cpv, validated
5-digit codes, deduped, capped at 10), and the chart stretches to fill
its card against the CPV list. One aggregate scan via an OR of half-open
cpv_code index ranges; ?cpv stays keyed in the edge cache (CWE-349) with
UI-canonicalized ordering, and selection changes are announced via an
sr-only status line.
The by-sector aggregate grouped on substr(t.cpv_code, 1, 2) and then ran
cpvDivision() over the already-truncated prefix, so a dirty code like
' 45000000' resolved to division '4' there while the leaderboard
(cpvDivision over the full code) resolved it to 45 — the same contract
landed in different CPV sectors on the two surfaces.

Select the full t.cpv_code, GROUP BY it in SQL, and run cpvDivision()
on the full code in JS exactly like the leaderboard mapping; the
existing merge folds all codes onto their canonical division and
re-applies secLimit post-merge. Test covers a dirty leading char
flowing through both surfaces to the same division.
…he full cpv_code

Grouping by the full cpv_code fixed the dirty-code divergence but returned
one row per distinct 8-digit code — 582 rows on the local corpus (thousands
at scale) shipped out of D1 for a 15-row table. SECTOR_KEY_SQL strips the
separator characters real codes carry and takes substr(clean, 1, 2) only
when the cleaned code provably starts with two digits (in which case it IS
cpvDivision(code)); anything else falls through as the full raw code for
the JS re-key to fold in. Exact cpvDivision semantics, division-sized
result set, still no pre-merge LIMIT truncation.

Adds a sqlite3-CLI integration test pinning SECTOR_KEY_SQL ≡ cpvDivision
across the dirt corpus, and a unit test pinning the GROUP BY key against
both traps (naive substr, full-code blowup).
Every column header in „Кои институции раздуват най-много" and „Раздуване
по сектори" now carries the same ⓘ MetricInfo popover the headline KPIs
use: what the metric is, how it is computed (grounded in the SQL — growth
is SUM(delta)/SUM(signing), €-weighted, not an average of percents), and
the honest inclusion caveat (annex_count > 0, current > signing,
signing ≥ €1 000). Table header cells get white-space: nowrap so the ⓘ
never wraps the label.

Also: the „Договори по мащаб" leaderboard rows gain right-side breathing
room (row padding-right var(--s-3)) so value text and truncated titles no
longer touch the card edge; the inset lives on the row, so the selected
highlight still reads full-width and the proportional bar-track math is
untouched in normal and fullscreen mode.
…ючения

Добавя раздел „Аналитичните изгледи" в /methodology с описание на всяко
табло (Анализи, Обзор, Конкуренция, Раздуване, Потоци, Карта, Мрежа,
Договори): какво показва визуалът, формулата с думи, обхватът и
изключенията, и честните уговорки — сверено ред по ред срещу кода.

Само документация; без промяна в поведението.
@StanislavBG

Copy link
Copy Markdown
Contributor Author

@todorkolev готов за ревю 🙏 — rebase-нат на main, CI зелен, prettier-чист, CSS промените в styles/* (app.css само @import). Резолвнати нишки. Approve-ни когато ти е удобно.

@ydimitrof

Copy link
Copy Markdown
Contributor

Проверих PR #193 внимателно — целият собствен diff, всяко фактическо твърдение в текста срещу кода на loader-ите и заявките, плюс сигурност и цялост на данните.

Обхват на промяната

Въпреки че diff-ът срещу main показва 41 файла и ~9 600 реда, това е стековащ се PR: реалната собствена промяна е един-единствен комит (7413f87) и един файл — apps/web/app/routes/methodology.tsx (+147/−4). Останалото е стекът #169#172, върху който клонът е базиран и който все още не е слят. Ревюто по-долу е за собствената промяна; сигурността на подлежащия стек е покрита в ревютата на съответните PR-и.

Сигурност и цялост на данните

  • Собствената промяна е чист JSX текст — няма SQL, няма изпълним код, няма мрежови повиквания, няма нови зависимости, няма четене на вход. Повърхността за SQL-инжекция, XSS или друг exploit е нулева. Целият текст е статичен и React-escape-ва по подразбиране.
  • Няма тайни, URL промени, обфускация или каквито и да е злонамерени модели. OWASP: неприложимо на ниво код за този diff, нищо тревожно.
  • Пренномерирането на секциите (7→8, 8→9, 9→10, 10→11) е коректно и консистентно — id-тата, aria-labelledby и TOC записите съвпадат.

Сверяване на твърденията срещу кода (цялост)

Тъй като PR-ът обещава „сверено ред по ред срещу кода", проверих локално всяко число и формула. Всички съвпадат точно:

  • РаздуванеOVERRUN_MIN_SIGNING_EUR = 1000, annex_count > 0, current > signing; растежът е SUM(delta)/SUM(signing) (претеглен по евро, не средно на процентите) — overruns.ts:153–163. ✔
  • Конкуренцияbids_received = 1, праг DEFAULT_MIN_CONTRACTS = 20, HHI = сбор от квадратите на дяловете, suppliers >= 2, отбелязване при hhi >= 0.25competition.ts:31,68,183,186 + competition.tsx:95. ✔
  • ОбзорSTART = '2020-01-01', бъдещи периоди изключени, текущият частичен период скрит по подразбиране с opt-in toggle, YoY само между съседни години и потиснат за непълната година, без прогноза — trend.ts:52,166–233. ✔
  • „Спрямо типичното"mult >= 1.3 → над, <= 0.75 → под; съотношение спрямо медианата на CPV класа, не z-score — trends.tsx:105–107. ✔
  • Цени по CPV — медиана + диапазон p10–p90, петцифрен клас, топ-N по брой — trend.ts:324–338. ✔
  • МрежаHOP1 = 6 преки контрагенти + по една втора връзка (LIMIT 1 на съсед) — network.ts:25,175. ✔
  • Потоци — топ 20/50 двойки — flows.ts:181 + flows.tsx:97–99. ✔
  • Карта — 28 NUTS3 региона, област от authorities.region (адрес/NUTS), непосочените в отделен bucket извън картата, покритието се отчита — regions.ts:5,52,66. ✔

Не открих нито едно разминаване между текста и поведението на кода. Уговорките („ориентир, не заключение", „ръстът не е нередност", „типичната цена е само ориентир") са честни и точно поставени — точно това, което искаме от методологична страница пред граждани.

Бележки (незадължителни, не блокиращи)

  • PR-ът е draft и mergeStateStatus: BLOCKED — по дизайн зависи от стека feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) #169feat(web): analyze index — five equal analysis cards #172 (описва таблата, които те въвеждат). Трябва да се слее след стека, иначе /methodology ще описва изгледи, които още ги няма. Регресията с .lens-* стиловете от feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) #169 е разрешена вътре в този стек (5f64191, c0521bd, eaeeb2c).
  • Дребна езикова прецизност: текстът нарича „спрямо типичното" процентил, а веднага след това коректно го описва като съотношение спрямо медианата. Двете не са едно и също понятие; кодът прави съотношение-спрямо-медиана. Не е грешка по същество (описанието е вярно), но думата „процентил" може да се изпусне за яснота.

CI е зелен, prettier/typecheck минават, 377 теста зелени (документационна промяна не добавя тестова повърхност — приемливо тук).

Благодаря за изрядно свършената работа — рядко се вижда методология, сверена буквално срещу заявките.

ВЕРДИКТ: Approve на същество — да се слее само след стека #169#172 (draft, blocked по дизайн). Няма проблеми със сигурността или целостта на данните.

@ydimitrof

Copy link
Copy Markdown
Contributor

Одобрявам

Ревю на PR #193 — „методологията описва точно таблата"

Прегледах промяната строго, с акцент върху сигурност, цялост на данните и точност спрямо кода. Разграничих собствената промяна на PR-а от подлежащия стек.

Обхват — какво реално въвежда този PR

Дифът показва ~9,6 хил. реда, но собствената промяна е един-единствен комит (7413f87) само върху apps/web/app/routes/methodology.tsx (+147/−4). Останалото е стекът #169#172 / #192, който все още не е слят и затова се появява в дифа спрямо main — тези файлове са ревюирани отделно в своите PR-и и не ги преразглеждам тук. Правилно е обозначено като Draft със зависимост от стека.

Промяната е само документация: нов раздел „7. Аналитичните изгледи" в /methodology плюс коректно преномериране на следващите раздели (7→8, 8→9, 9→10, 10→11) и запис в TOC. Няма промяна в поведението.

Сигурност / OWASP

Чисто — тук няма атакуема повърхност:

  • Целият добавен JSX е статичен, вкоден текст. Няма dangerouslySetInnerHTML, няма интерполация на потребителски вход, няма нови href към външни адреси (само вътрешни <Link to="/…"> и котви #…).
  • Няма SQL, няма заявки, няма нови зависимости, няма тайни/.env.
  • A03 Injection / A07 / XSS: неприложими — React екранира текстовите възли; всички връзки са относителни и литерални.

Цялост на данните — сверих всяко твърдение ред по ред срещу кода

Това е същината при документация, която обещава „сверено срещу кода". Всички числени твърдения съвпадат точно:

  • Раздуванеannex_count > 0 ∧ current > signing ∧ signing ≥ €1 000 (overruns.ts:153–158, OVERRUN_MIN_SIGNING_EUR = 1000); растежът е SUM(delta)/SUM(signing), претеглен по евро (overruns.ts:106,162–164). ✔
  • Конкуренция — една оферта = bids_received = 1 спрямо договори с известен брой (competition.ts:68,73); праг ≥ 20 договора (DEFAULT_MIN_CONTRACTS = 20); HHI = сбор от квадрати на дяловете, ≥ 2 доставчика (competition.ts:183,186); отбелязване при ≥ 0,25 (competition.tsx:95). ✔
  • Обзор — текущият непълен период е скрит по подразбиране с opt-in toggle (trend.ts:186–190), YoY само между съседни години и пропуснат за непълната (trend.ts:220–233), начало 2020 г. (trend.ts:52), без прогноза. ✔
  • „Спрямо типичното"≥ 1,3× → над, ≤ 0,75× → под (trends.tsx:105–106); коректно описано като процентил, не z-score. ✔
  • Мрежа — шест преки контрагенти + по една втора връзка (network.ts:25 HOP1 = 6, ред 196). ✔
  • Карта — NUTS3, 28 области, изведени от адреса на институцията (~половината), с „unattributed" кофа и покритие (regions.ts:1–5). ✔
  • Потоци — top-N 20/50 двойки (flows.ts). ✔

Не открих нито едно разминаване между текста и поведението на кода.

Съответствие с описанието на PR-а

Описанието твърди „единственият променен файл е methodology.tsx" и „само документация" — потвърдено от собствения комит. CI е зелен (check pass, 1m51s), typecheck/prettier/тестове зелени според описанието.

Дребни, незадължителни бележки (не блокират)

  • Разделът дублира по смисъл праговете, които вече живеят в кода (напр. 1 000 €, 0,25, 1,3×/0,75×). Това е присъщо на документацията, но ако някой праг се промени в кода, този текст ще трябва да се обнови ръчно — няма автоматична връзка. Струва си да се спомене в PR-описанието като поддръжков дълг.
  • Тъй като е Draft, зависещ от стека: слейте едва след feat(web): dashboard design-system base (MetricInfo, fullscreen, tokens, overrun index) #169feat(web): analyze index — five equal analysis cards #172, за да не сочат котвите/описанията към табла, които още не са на main.

Отлична, честна и точна работа — тонът „ориентир, не заключение" е спазен навсякъде.

Одобрявам — на съществото; сливане след стека #169#172.

chooseXTicks took the lowest 5 stops from the in-range tick ladder, so a
wide log range (e.g. 25%–4800%) left the upper ~40% of the axis without
labels even though points were plotted there. Sample evenly across the
in-range ladder instead, always keeping the first and last stops.
The card's hint claimed avgYoy is "the same value as on /трендs", but
/trends never displays this figure (methodology.tsx documents /trends as
forecast-free) — the metric is analytics-only, computed from the same
underlying monthly series. Reword the hint to state that accurately.
The file's own header states the palette lives in exactly one place
(OKLch); --paper-raised: #ffffff was the one raw-hex exception.
…ly CSRF

postcss 8.5.15 (GHSA-r28c-9q8g-f849) and valibot 1.4.0 (GHSA-5qjj-4xww-7phc)
are patch-level, non-breaking bumps to 8.5.18+ and 1.4.2+ via pnpm-workspace
overrides.

react-router 7.18.0 is flagged by GHSA-qwww-vcr4-c8h2, a CSRF issue confined
to the unstable RSC APIs (verified via repo-wide grep: zero RSC usage). No
fix exists in the 7.x line; the minimum fixed version is 8.3.0, a major
breaking bump out of scope here. Suppressed via osv-scanner.toml, following
the same time-boxed pattern as the existing sharp (GHSA-f88m-g3jw-g9cj)
entry, reviewable by 2026-10-01.
…nflicts

- osv-scanner.toml: kept the react-router RSC suppression (this branch);
  dropped main's sharp suppression since this branch already bumped sharp
  to ^0.35.0 (5f94c94), which resolves GHSA-f88m-g3jw-g9cj outright
- packages/db/migrations: renumbered this branch's overrun-index migration
  from 0002 to 0003 to avoid colliding with main's already-merged
  0002_current_value_currency.sql; updated migrations.test.ts to apply both
  in sequence
- apps/web/app/routes/analytics.tsx, trends.tsx: kept this branch's
  redesigned loaders/queries (analytics hub, obzor three-lens view), applied
  main's getDb() read-only D1 chokepoint (midt-bg#199) instead of raw env.DB
- apps/web/app/routes/overruns.tsx: not itself conflicted, but also routed
  through getDb() to satisfy the chokepoint guard test added by main
  (readonly-db-chokepoint.test.ts)
- pnpm-lock.yaml: regenerated via pnpm install
@nedda76

nedda76 commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

Този клон е в конфликт с main, тъй че към момента не може да се ревюира — дифът, който GitHub показва, вече не отговаря на това, което би влязло. Ще го пребазираш ли върху актуалния main (или merge на main в клона) и да разрешиш конфликтите? След това веднага го поглеждам. Благодаря! 🙏

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщен преглед на PR — „docs(web): методологията описва точно таблата — формули, обхват, изключения"

Какво прави PR-ът

Въпреки заглавието docs(web): …, това е значителна функционална промяна, обхваната в 9 партиди. PR-ът:

  • добавя ново табло /overruns (за „раздуване" на договори) и пренаписва страниците /trends и /analytics с леки „заглавни четива" вместо тежки lens-preview изгледи (edge-кеширани за 1800 s, паралелни заявки);
  • въвежда нова бизнес-логика и заявки (overruns.ts, flows.ts, competition.ts, analytics-stats.ts, cpvGroupSelection, getOverrunsAnalytics, getOpaqueShareByYear и др.) с изчерпателна тестова покривка;
  • добавя нови презентационни компоненти (ComboTrendChart, FullscreenButton, MetricInfo), CSS за таблото (изнесен от inline style= за CSP-чистота) и премахва неизползваните .lens-* стилове;
  • разширява страницата methodology.tsx с нова секция 7 „Аналитичните изгледи" (само проза, коректно преномериране и препратки);
  • обновява зависимости за сигурност (sharp ^0.35.0, postcss ^8.5.18, valibot ^1.4.2) и подменя потискане в osv-scanner.toml.

Сигурност (Фаза 0)

Чисто във всички партиди с достъпен diff. Няма твърдо кодирани тайни, нови външни URL адреси, злонамерени шаблони или необосновани зависимости. Входът е валидиран и ограничен навсякъде (cpvGroupSelection, whitelist на query параметри, JSX escaping — защита срещу CWE-349 и XSS). Обновленията на зависимости са коректно обосновани с CVE/GHSA номера и последователно отразени в lock файла.

Качество

Кодът е с високо качество: чисти, SSR-безопасни функции, споделени SQL фрагменти, които предотвратяват разминаване между изгледите, детерминистични tie-breaker-и, коректно чункиране спрямо D1 лимита от 100 параметъра и осмислена, нетривиална тестова покривка (гранични случаи, NaN/null, YoY през липсваща година).

Основни находки (не блокиращи)

  1. Несъответствие заглавие ↔ обхват. Заглавието е docs(web): …, но PR-ът добавя ~хиляди редове нова логика, компоненти и CSS (напр. overruns.ts +582, pages.css +3080). Препоръчва се типът на commit-а да отрази реалния обхват (напр. feat(web): …) заради changelog/семантичното версиониране, и да се потвърди, че няма scope creep.
  2. Липса на горна граница за CPV групи (cpvGroupsClause в getSpendingTrend/listOverviewContracts). За разлика от getOverrunAnnexes, тук няма чункиране — при ~49+ избрани групи заявката ще надхвърли лимита от 100 SQL променливи на D1 и ще се провали. Желателна е явна серверна граница/чункиране. Аналогично, getCpvGroupMedians прави паралелен fan-out без таван.
  3. estimateYoyGrowth / гранулярност. Гранулярността на getSpendingTrend е сменена от 'year' на 'month'; функцията предполага месечна серия и при друга гранулярност мълчаливо връща „плосък" фактор — струва си потвърждение.
  4. Достъпност на scatter точките в /overruns: <circle onClick> не са фокусируеми от клавиатура и нямат role/tabIndex/onKeyDown. Има достъпна алтернатива (списък с бутони + aria-label), затова е дребна забележка.
  5. Кръстосани зависимости за потвърждение: че sharp наистина е вдигнат до >= 0.35.0 (иначе премахнатото потискане чупи скенера — потвърдено в партида 9); че нито един жив шаблон не реферира премахнатите .lens-* класове.
  6. Дребни/CSS забележки: твърдо кодирано „24" вместо CONTRACT_CARD_LIMIT; непоследователен fallback за --font-serif; възможно z-index наслагване на .metric-info-pop вътре в изгледа на цял екран; повтарящ се font shorthand.

Ограничения на прегледа

Diff-ът на pages.css (+3080/-3) не беше наличен за инспекция — препоръчва се ръчна проверка за дублиране/мъртви стилове и Фаза 0 върху пълния файл преди сливане. Желателно е и потвърждение, че pnpm install --frozen-lockfile минава в CI.

Заключение

COMMENT — няма блокиращи проблеми. Промяната е атомарна, добре документирана и тествана. Преди сливане си струва да се адресират: несъответствието заглавие/обхват, потенциалният отказ при много CPV групи (D1 лимит), потвърждението за гранулярността на estimateYoyGrowth и ръчният преглед на pages.css. Останалите забележки са с нисък приоритет.

Comment thread apps/web/app/lib/analytics-stats.ts Outdated
Comment thread apps/web/app/routes/analytics.tsx
Comment thread apps/web/app/routes/analytics.tsx
Comment thread apps/web/app/routes/trends.tsx
Comment thread packages/config/src/index.ts Outdated
Comment thread packages/db/src/overruns-sql.test.ts Outdated
Comment thread packages/db/src/queries/competition.ts
Comment thread packages/db/src/queries/trend.ts
…d-rolled casts

Removes the last 11 `as unknown as D1Database` casts left behind after the
main-branch merge, so packages/db/src/queries/{competition,flows,overruns,regions,trend}.test.ts
clear the midt-bg#325 fake-D1 gate (scripts/check-fake-d1.mjs) that started failing CI on this
branch after it landed on main.
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Aug 30, 2026
cpvBucket fell through to 'goods' for any catalogued division not in
CPV_BUCKET_WORKS/SERVICES. A future services division added to CPV_SECTORS
but forgotten in CPV_BUCKET_SERVICES would silently misclassify as goods,
with no test able to catch it (the existing partition test only asserted
!== 'other'). CPV_BUCKET_GOODS is now an explicit set; an omitted division
now falls to 'other', and a new test asserts
WORKS ∪ SERVICES ∪ GOODS === CPV_SECTORS exactly.

Same change is required byte-identical on the sibling PRD
feedback-193-methodology branch, which flags the same line — coordinate
before either merges.

Addresses ydimitrof's review on PR midt-bg#171 (also flagged on midt-bg#193).
…only adjacent years in YoY

Addresses ydimitrof's review round on PR midt-bg#193:
- trend.ts: validCpvGroups caps at 40 groups (2 binds each) so cpvGroupsClause
  can never exceed D1's 100-bound-parameter limit; test proves 60 groups stay bindable
- analytics-stats.ts: estimateYoyGrowth skips non-adjacent year pairs so a gap
  year is never read as a one-year ratio; months===12 documented as an invariant
- trends.tsx: interpolate CONTRACT_CARD_LIMIT into the truncation subtitle
- config: goods is now an explicit CPV bucket set; unassigned catalogued
  divisions fall to other and trip the partition tests
- overruns-sql.test.ts: global tab replace in sqlLiteral
- competition.ts: note the GLOB 4-digit invariant guarding the lexicographic year compare
nikimilenkov added a commit to nikimilenkov/sigma that referenced this pull request Aug 31, 2026
… review)

The 14 subject-risk columns were added by editing 0000_init.sql. The served D1
is persistent and `wrangler d1 migrations apply` is filename-tracked, so an
edit to the already-applied 0000_init reaches the work DB (green local CI) but
never prod — precompute's UPDATE would then fail on missing columns (the

Extract the columns into 0006_subject_risk_columns.sql (ALTER TABLE ADD
COLUMN) and remove them from 0000_init (SQLite has no ADD COLUMN IF NOT EXISTS,
so a fresh DB can't carry both). 0006 clears the 0002 claimants
(midt-bg#226/midt-bg#193/midt-bg#172) to avoid a duplicate-version at apply. The precompute
CREATE TABLE IF NOT EXISTS mirror stays a no-op on the existing tables. The
four risk tests now apply 0006 after 0000_init, mirroring the served-D1 chain.

Reword the 0000_init header to distinguish the rebuilt-every-import work DB
from the persistent served D1, so the next person adds objects via a new
migration instead of editing 0000_init. Also document that value_flag='ok' is
the exact complement of the hidden suspect set.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение на прегледа

Какво прави този PR

Въпреки заглавието docs(web), това е значителна функционална промяна, разпределена в 8 партиди. PR-ът обновява методологичната документация (нов раздел 7, пренномериране на разделите, обновен TOC и вътрешни препратки) така, че тя да описва точно таблата — формули, обхват и изключения. Успоредно с това добавя:

  • Нов продукционен код за анализи: помощни функции (analytics-stats, overruns-chart, overruns-inspector, filters.cpvGroupSelection), нови query функции (overruns.ts, regions.ts, trend.ts), CPV дял (cpvBucket/cpvDivision), getOpaqueShareByYear.
  • Пренаписани маршрути: analytics.tsx (начална страница), overruns.tsx, trends.tsx (пълно функционално пренаписване, +619/−108 — нов маршрутен модел с три „ъгъла", нови графики, премахнати филтри за сектор и финансиране).
  • Нова SQL миграция (0011, forward-only, с частичен индекс), нови tie-breakers в ORDER BY за детерминизъм, RESERVED_CACHE_PARAMS дисциплина.
  • CSS: пренасяне на inline style= към класове (CSP-чисто), премахване на неизползвани .lens-* правила, голямо добавяне в pages.css.
  • Изчерпателни тестове за render, loader, миграции и заявки.

Обща оценка

Кодът е с високо качество: презентационен и само за четене, edge-кеширан, с ограничени заявки. Няма проблеми със сигурността в нито една партида — потребителските входове минават през стриктна валидация (/^\d{5}$/ за CPV групи, pick/allow-list за query-параметри, clampLimit), подават се като bound параметри (без SQL инжекция), а изходът се екранира от React (без XSS). Няма нови мрежови извиквания, зависимости, промени в auth/CI, тайни или следи от prompt injection. Целостта на данните е спазена — помощниците не измислят стойности при оскъдни данни (връщат , непотвърден), изключват непълните периоди/години и клампират аномалии. Методологичният текст съответства на реалната семантика на изгледите (праг ≥ 1000 €, медиана vs. средно, дял с една оферта само при ≥20 поръчки, растеж претеглен по €).

Ключови забележки (не блокиращи)

  1. Несъответствие заглавие/обхват (повтарящо се във всички партиди): заглавието е docs(web), но PR-ът съдържа съществен продукционен код — SQL миграция, нови query функции, пренаписани маршрути и масивен CSS. Молба да се потвърди, че етикетът/обхватът са умишлени и че методологичните текстове са синхронизирани с новия изглед на trends.tsx (сектор/финансиране са премахнати).

  2. getCpvGroupMedians няма таван върху броя групи (партида 8) — за разлика от validCpvGroups, който сряза до MAX_CPV_GROUPS, и стартира по една конкурентна D1 заявка на уникална група. Предложение за устойчивост.

  3. Неизползван импорт singleSelectFilters в trends.tsx (партида 4) — може да счупи билда при noUnusedLocals/строг ESLint.

  4. pages.css (+3080/−3) не беше наличен за diff (маркиран като бинарен/твърде голям) — препоръка за ръчен преглед за неочаквани url(...), @import или вградени base64 данни.

  5. Съгласуваност между партидите: изтритите .lens-* CSS правила предполагат, че съответният маркъп също се премахва — да се потвърди. Стабилността на „stretched link" + MetricInfo бутона зависи от CSS stacking context — струва си визуална проверка.

Заключение

COMMENT — няма дефекти, които да блокират сливането или да се проявят в продукция. Основното действие преди merge е сверяване на заглавието/обхвата на PR-а с реалното съдържание и потвърждаване, че документацията е синхронизирана с пренаписаните изгледи. Останалите забележки са незадължителни подобрения.

Comment thread apps/web/app/components/ComboTrendChart.tsx Outdated
Comment thread apps/web/app/lib/analytics-stats.test.ts
Comment thread apps/web/app/routes/overruns.tsx
Comment thread apps/web/app/routes/trends.tsx Outdated
Comment thread packages/db/src/queries/competition.ts
Comment thread packages/config/src/index.ts
Comment thread packages/db/src/queries/trend.ts Outdated
At the minimum 2-point series, width/n overshot point spacing and the
edge bar spilled outside the viewBox. Also clamp each bar's half-width
to the room actually available on either side of its center so edge
bars never overflow regardless of n.
getCpvGroupMedians issues one concurrent D1 query per unique group via
Promise.all with no cap, unlike validCpvGroups which already truncates
to MAX_CPV_GROUPS. Reuse that helper so untrusted input cannot fan out
unbounded concurrent D1 queries.
The new loader reads only cpvGroupSelection; the old single-select
loader call is gone but the import was left behind.
GROWTH_TRAILING_YEARS = 3 keeps only 2022-2024, so the 2020-2021 jump
is excluded by the sliding window itself, not by a median of four
ratios — the window only ever produces two.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обобщение на прегледа

Какво прави PR-ът: Въпреки заглавието „docs(web)…", промяната далеч надхвърля документацията. Тя изгражда цялостно табло за анализи в apps/web: пренаписана начална страница на „Анализи", нов раздел „Методология" с формули/обхват/изключения, нови функционални route-ове (/trends, /overruns) от порядъка на стотици редове, разширения на API-контракта (нови типове и заявки getOpaqueShareByYear, getFlowsHeadline), помощни функции cpvDivision/cpvBucket в @sigma/config, частичен индекс (миграция 0011), нови дизайн-токени, обширни CSS стилове и богато тестово покритие.

Обща оценка: Кодът е чист, добре изолиран, добре документиран и щателно тестван. Логиката е изнесена в pure функции, компонентите са SSR-безопасни, а граничните случаи (деление на нула, non-finite вход, частични периоди, липсваща предходна година при YoY) са коректно обработени с честен em-dash вместо измислени стойности.

Сигурност и интегритет на данните — без блокиращи дефекти:

  • Няма злонамерен код, нови мрежови повиквания, тайни, eval/обфускация, промени по auth/permissions/CI или подозрителни зависимости. Не са открити опити за prompt injection.
  • Входовете са валидирани и whitelist-нати: year през regex, by като union тип, cur='1', а CPV групите през cpvGroupSelection (/^\d{5}$/, дедупликация, запазен ред, таван MAX_CPV_GROUPS) — защита срещу CWE-349 и неограничено разрастване на кеш ключове/SQL обхват.
  • SQL използва константи и параметризирани плейсхолдъри; спазен е лимитът на D1 от 100 свързани параметъра (IN-листата е чънкована). Целият изход минава през екраниран JSX — без XSS.
  • Миграция 0011 е IF NOT EXISTS, forward-only, с частичен предикат, съответстващ на OVERRUN_WHERE. FX нормализацията към EUR (пег за BGN, идентичност за EUR, NULL за непозната валута) и подът от €1000 са покрити с тестове. Добавени tie-breaker-и в ORDER BY премахват недетерминизъм при LIMIT.

Незадължителни забележки (не блокиращи):

  1. Разминаване в обхвата спрямо заглавието. Няколко партиди отбелязват, че commit-ът е обозначен docs(web), но реалното съдържание е предимно нов функционален код, route-ове и голям CSS. Особено внимание заслужава pages.css (+3080/−3), чийто diff не беше достъпен за преглед — препоръчва се ревюиращ с достъп до пълния diff да потвърди, че разрастването е свързано и оправдано, без нежелано включен генериран/минифициран код.
  2. Дублирана логика. Изчислението на yearStart ('-Q1'/'-01'/null) е копирано дословно между TrendChart.tsx и ComboTrendChart.tsx — струва си да се изнесе в общ helper.
  3. Форма на моковете в тестове. В analytics.test.ts някои мокове не съвпадат с типизираните контракти (напр. getOpaqueShareByYear), тъй че деривацията на „opaque" не се упражнява реално — тестът минава, но не пази маппинга в loader-а (loader-ът е в друга партида).
  4. Крос-партидни зависимости за проследяване: RESERVED_CACHE_PARAMS изисква g да е добавен в CANONICAL_QUERY_PARAMSquery-params.ts, друга партида), иначе тестът „keeps RESERVED_CACHE_PARAMS honest" ще падне; премахнатите стилове .analytics-lenses/.lens-* предполагат съответно премахване на JSX разметката в друга партида.
  5. corpus.count в getOverrunsAnalytics идва от единичен проход без JOIN, докато лидербордът/byAuthority ползват INNER JOIN — договор без свързан участник/възложител би влязъл в корпусния брой, но не в разбивката. Документирано е като умишлено; отбелязва се за пълнота.

Заключение: Няма блокиращи концерни по сигурност, коректност или интегритет на данните. Основната препоръка преди сливане е да се потвърди голямата, непрегледана CSS промяна (pages.css) и да се изясни/коригира разминаването между типа docs и реалния функционален обхват на PR-а.

Comment thread apps/web/app/routes/analytics.test.ts Outdated
Comment thread apps/web/app/components/ComboTrendChart.tsx Outdated
Comment thread apps/web/app/routes/analytics.tsx
Comment thread apps/web/app/routes/analytics.tsx
Comment thread apps/web/app/routes/methodology.tsx
Comment thread apps/web/app/routes/trends.tsx

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Прегледах #193 при HEAD f39ab996. Delta-та е review-fix код (не самият doc) — cross-check-нах кода срещу methodology дока, няма drift. estimateYoyGrowth adjacent-year fix (analytics-stats.ts:189): if (curYear - prevYear !== 1) continue коректно спира gap-година (2021→2023 без 2022) да се чете като едногодишен скок — поправя публикуван growth rate, fail-safe към flat, съвпада с „3-year trailing median of complete-year ratios" (analytics.tsx). getCpvGroupMedians fan-out cap (trend.ts) преизползва validCpvGroups (MAX_CPV_GROUPS=40), консистентно с getSpendingTrend. Вердикт: чисто.

StanislavBG and others added 3 commits September 20, 2026 00:06
# Conflicts:
#	apps/web/app/components/TrendChart.tsx
#	apps/web/app/lib/query-params.ts
#	apps/web/app/routes/analytics.tsx
#	apps/web/app/routes/trends.tsx
#	apps/web/app/styles/pages.css
#	apps/web/workers/cache-key.test.ts
#	packages/db/src/queries/competition.test.ts
#	packages/db/src/queries/flows.test.ts
#	packages/db/src/queries/regions.test.ts
#	packages/db/src/queries/trend.test.ts
…cInfo, fullscreen hook, growth stats and sparse cpv stats

Exports CPV_DIVISION_SET (matches pr/overruns) so the bucket fallthrough is testable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…а на opaque мока; общ yearAxisTicks; поправена номерация в методологията

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
StanislavBG added a commit to StanislavBG/sigma-pr that referenced this pull request Sep 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants