Skip to content

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

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

docs(web): методологията описва точно таблата — формули, обхват, изключения#193
StanislavBG wants to merge 26 commits into
midt-bg:mainfrom
StanislavBG:docs/methodology-dashboards

Conversation

@StanislavBG

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 е единственият променен файл.

Проверки

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

Зависимости

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

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

Проверих 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

Одобрявам

Ревю на 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.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Ре-проверих раздел 7 срещу кода — една точна неточност по стойностната база (иначе HHI/конкуренция и праговете за раздуване са верни):

Формулировките, че договорите с непотвърдена стойност „се изключват от сумите" (увод на раздел 7, ~ред 408) и „не се сумират" („Договори — обзор", ~ред 536), са неверни за сайт-широките суми в евро:

  • normalize-raw.sql:357WHEN value_flag = 'value_suspect' THEN x.proc_est_eur: value_suspect редовете се repair-ват към прогнозната стойност (ненулев amount_eur), не се зануляват.
  • trend.ts:72,146 (WHERE amount_eur IS NOT NULLSUM(amount_eur), без value_flag='ok' филтър), competition.ts и home_totals ги сумират → влизат във всеки евро-тотал при прогнозната си стойност.

Т.е. договор с типо €5 млрд, маркиран value_suspect, НЕ се вади от /trends, /contracts, /competition, /regions — влиза при proc estimate. Читателят е упътен обратното. Коригирайте двете места: value_suspect договорите се включват при прогнозната си стойност (или „суровата стойност се заменя с прогнозна", ако това е замисълът).

NB: за „Раздуване след анекси" (~ред 482) твърдението е вярно — там annex_suspect има NULL current_value_eur и наистина отпада. Само сайт-широките суми са грешно описани.

Both the section-7 intro and the /contracts description said implausible-value contracts are
excluded from site-wide EUR sums. They're actually repaired to the procedure's estimated value and
included (normalize-raw.sql:357) — trend/competition/home totals sum them at that value. Only the
overrun view is correctly described as excluding them (no current value to compare against).
@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Благодаря — §7 вече е точен: „…влизат в сумите по прогнозната стойност на процедурата, не по подадената — с изключение на „Раздуване след анекси"" (ред 409-410). Това затваря основната находка.

Едно остатъчно разминаване: §6 редът „Непотвърдени стойности" (ред 210) още казва „стойността им се изключва от сумите", което сега противоречи на §7 (влизат по прогнозна). Читател само на §6 остава подведен. Изравни §6 — напр. „суровата стойност се заменя с прогнозната, която влиза в сумите" вместо „се изключва".

(Ред 397 — чужда валута без намерен курс → NULL amount_eur — си е коректно „изключени".)

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.

3 participants