From 80b06eb1c6e8dba85432ed5e9d28ae7c0960bae1 Mon Sep 17 00:00:00 2001 From: DiyanaDimitrova Date: Mon, 13 Jul 2026 16:53:33 +0300 Subject: [PATCH 1/2] feat(web): non-accusatory disclaimer next to per-contract risk flags (#219) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-requisite of the risk numbers (#218): the disclaimer must travel with the flags wherever they appear, not only next to the homepage leading number. - RiskIndicators (contract page): add a „сигнал, не присъда" note that links to /methodology#flagged, so the caveat sits directly beside the flags. - Reframe the flag copy to a non-accusatory signal tone: drop „повишен риск според стандартите на ОЛАФ", „Значително оскъпяване" and „подлежи на допълнителна проверка"; state the structural fact instead. Labels now mirror methodology §10 / homepage FLAG_LABELS. - Rename the heading „Индикатори за риск" → „Сигнали за риск" for one vocabulary. - Methodology §10: add an „Ограничения" note — signals rest on public data and structural indicators, source errors are possible, a signal can be a false positive and is not proof of wrongdoing; links to the gaps (§8) and corrections (§11) sections. --- apps/web/app/components/RiskIndicators.tsx | 28 +++++++--- apps/web/app/routes/methodology.tsx | 61 ++++++++++++++++++++-- apps/web/app/styles/pages.css | 9 ++++ 3 files changed, 87 insertions(+), 11 deletions(-) diff --git a/apps/web/app/components/RiskIndicators.tsx b/apps/web/app/components/RiskIndicators.tsx index 59e1568a..ec38e038 100644 --- a/apps/web/app/components/RiskIndicators.tsx +++ b/apps/web/app/components/RiskIndicators.tsx @@ -1,7 +1,13 @@ import type { ContractDetail } from '@sigma/api-contract'; +import { Link } from 'react-router'; import { pct } from '@sigma/shared'; import { evaluateRiskIndicators } from '../lib/riskLogic'; +// The per-contract risk signals. Copy is deliberately non-accusatory (#219): each item states the +// structural fact and frames it as a signal that warrants a look, never as proven wrongdoing. The +// note below repeats the disclaimer next to the flags and links to the full methodology, so the +// caveat travels with the flags wherever they appear (issue #219 co-requisite of the risk numbers). +// Labels mirror methodology §10 and the homepage FLAG_LABELS for a consistent vocabulary. export function RiskIndicators({ contract }: { contract: ContractDetail }) { const flags = evaluateRiskIndicators(contract); @@ -27,22 +33,23 @@ export function RiskIndicators({ contract }: { contract: ContractDetail }) { - Индикатори за риск + Сигнали за риск
    {flags.map((flag, i) => { if (flag.type === 'eu_no_competition') { return (
  • - Риск при Еврофондове: Проектът е финансиран с европейски средства, - но е възложен без реална конкуренция (повишен риск според стандартите на ОЛАФ). + Липса на конкуренция (със средства от ЕС): проектът е финансиран с + европейски средства и е допусната само една оферта. Правилата за конкуренция при + еврофондове са по-строги, затова случаят заслужава преглед.
  • ); } if (flag.type === 'no_competition') { return (
  • - Липса на конкуренция: Този договор е сключен след допускане на само + Липса на конкуренция: договорът е сключен след допускане на само една оферта.
  • ); @@ -50,22 +57,27 @@ export function RiskIndicators({ contract }: { contract: ContractDetail }) { if (flag.type === 'high_markup') { return (
  • - Значително оскъпяване: Стойността на договора е нараснала с{' '} - {pct(flag.deltaPct!)} спрямо първоначално обявената — чрез допълнителни анекси. + Ръст на стойността чрез анекси: текущата стойност е нараснала с{' '} + {pct(flag.deltaPct!)} спрямо стойността при подписване.
  • ); } if (flag.type === 'anomalies') { return (
  • - Аномалии в данните: Стойността на договора (или някои от датите) е - извън обичайния диапазон и подлежи на допълнителна проверка. + Стойностна или времева аномалия: стойността е с непотвърдена + достоверност или договорът е подписан преди датата си на публикуване.
  • ); } return null; })}
+

+ Сигналите са ориентири за преглед, не присъда — отбелязват договор, който заслужава + внимание, а не доказано нарушение.{' '} + Как четем сигналите → +

); } diff --git a/apps/web/app/routes/methodology.tsx b/apps/web/app/routes/methodology.tsx index a05f92e2..e27c1ecb 100644 --- a/apps/web/app/routes/methodology.tsx +++ b/apps/web/app/routes/methodology.tsx @@ -37,6 +37,7 @@ const TOC = [ ['identity', 'Имена, ЕИК, УНП'], ['gaps', 'Известни празнини в полетата'], ['export', 'Сваляне и достъп до данните'], + ['flagged', 'Сигнали за риск'], ['contact', 'Поправки и обратна връзка'], ]; @@ -143,8 +144,10 @@ export default function Methodology({ loaderData }: Route.ComponentProps) {

- СИГМА е изцяло само за четене: не въвежда нови данни, не оценява процедурите и не - маркира фирми като рискови. + СИГМА е изцяло само за четене: не въвежда нови данни и не оценява процедурите. Тя + отбелязва договори със структурни сигнали за преглед (виж{' '} + раздел 10), но не обявява фирми или възложители за нарушители + — сигналът е ориентир, не присъда.

@@ -517,8 +520,60 @@ export default function Methodology({ loaderData }: Route.ComponentProps) {

Засега не предлагаме публично API в реално време.

+
+

10. Сигнали за риск

+

+ На страницата на всеки договор СИГМА отбелязва{' '} + структурни сигнали за риск, когато поне един признак заслужава + преглед. Сигналите са ориентири, не присъда: те не твърдят нарушение, а + насочват към договори, които си струва да се погледнат по-внимателно. +

+

Отбелязваме четири признака:

+
+
Липса на конкуренция
+
+

+ Допусната е една-единствена оферта (приети оферти = 1), без финансиране от ЕС. +

+ → приети оферти = 1 +
+
Липса на конкуренция (със средства от ЕС)
+
+

+ Същият признак, но при финансиране от ЕС, където правилата за конкуренция са + по-строги — затова го отделяме. +

+ → приети оферти = 1 · финансиране от ЕС +
+
Ръст на стойността чрез анекси
+
+

+ Текущата стойност е нараснала с над 20% спрямо стойността при подписване — + индикатор за оскъпяване след сключването. +

+ → текуща стойност > 1.2 × стойност при подписване +
+
Стойностна или времева аномалия
+
+

+ Стойност с непотвърдена достоверност (виж речника) или договор, подписан преди + датата си на публикуване. +

+ → value_flag / date_flag +
+
+

+ Ограничения. Сигналите стъпват на публични данни и структурни + признаци, не на разследване. Възможни са грешки или празнини в източника (виж{' '} + раздел 8), затова един сигнал може да е фалшив — например + законна процедура с една оферта по обективни причини. Сигнал не е констатация за + нарушение и не оценява поведението на конкретно лице. Ако смятате конкретен сигнал + за неточен, пишете ни (раздел 11). +

+
+
-

10. Поправки и обратна връзка

+

11. Поправки и обратна връзка

Грешките поправяме ръчно при сигнал — двойни записи за институция/компания (изпратете двата ЕИК/линка) или сума, която не отговаря на оригиналния документ diff --git a/apps/web/app/styles/pages.css b/apps/web/app/styles/pages.css index fd6341d3..b317a413 100644 --- a/apps/web/app/styles/pages.css +++ b/apps/web/app/styles/pages.css @@ -102,6 +102,15 @@ .risk-list li:last-child { margin-bottom: 0; } +/* Non-accusatory disclaimer that travels with the per-contract flags (#219). */ +.risk-note { + margin: var(--s-4) 0 0; + padding-top: var(--s-3); + border-top: 1px solid var(--warning); +} +.risk-note a { + color: var(--warning-strong); +} /* Value-history strip + current lot row */ .value-history { From 8e48e2b5285aa17c3825b158e758897bbc0b8851 Mon Sep 17 00:00:00 2001 From: DiyanaDimitrova Date: Mon, 13 Jul 2026 23:14:24 +0300 Subject: [PATCH 2/2] fix(web): noindex contract pages of natural-person bidders carrying risk flags (#219 review) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deep GDPR review follow-up. The contract detail page renders the bidder name together with the „Сигнали за риск" box, but — unlike sole-trader company profiles (company.tsx) and the sitemap — it was not noindex'd. That left an identifiable individual (ЕТ) publicly associated with a risk label on an indexable, edge-cached page. - contract.tsx: emit robots=noindex when the bidder is a natural person (isNaturalPersonProfileName), mirroring the company-profile policy. The contract stays fully public on the site; only search-engine amplification is avoided. Verified: ЕТ-bidder contract → noindex, АД-bidder contract → indexable. - RiskIndicators: add a „Смятате сигнал за грешен?" rectification link to /methodology#contact next to the flags, matching the homepage disclaimer. --- apps/web/app/components/RiskIndicators.tsx | 2 ++ apps/web/app/routes/contract.tsx | 22 ++++++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/web/app/components/RiskIndicators.tsx b/apps/web/app/components/RiskIndicators.tsx index ec38e038..0104823e 100644 --- a/apps/web/app/components/RiskIndicators.tsx +++ b/apps/web/app/components/RiskIndicators.tsx @@ -77,6 +77,8 @@ export function RiskIndicators({ contract }: { contract: ContractDetail }) { Сигналите са ориентири за преглед, не присъда — отбелязват договор, който заслужава внимание, а не доказано нарушение.{' '} Как четем сигналите → + {' · '} + Смятате сигнал за грешен? →

); diff --git a/apps/web/app/routes/contract.tsx b/apps/web/app/routes/contract.tsx index 256617de..f1225bfa 100644 --- a/apps/web/app/routes/contract.tsx +++ b/apps/web/app/routes/contract.tsx @@ -1,5 +1,14 @@ import { Link } from 'react-router'; -import { count, longDate, money, moneyBare, plural, signedMoney, signedPct } from '@sigma/shared'; +import { + count, + isNaturalPersonProfileName, + longDate, + money, + moneyBare, + plural, + signedMoney, + signedPct, +} from '@sigma/shared'; import { contractIdFromSlug, contractSlug, getContract } from '@sigma/db'; import type { ContractDetail } from '@sigma/api-contract'; import type { Route } from './+types/contract'; @@ -71,7 +80,7 @@ function AnnexDescription({ text }: { text: string | null }) { export function meta({ data, params, matches }: Route.MetaArgs) { const c = data?.contract; - return seoMeta({ + const tags = seoMeta({ matches, path: `/contracts/${contractSlug(contractIdFromSlug(params.id))}`, title: `${c?.subject ?? 'Договор'} — СИГМА`, @@ -79,6 +88,15 @@ export function meta({ data, params, matches }: Route.MetaArgs) { ? `Договор по УНП ${c.unp} между ${c.authority.name} и ${c.bidder.displayName}.` : '', }); + // GDPR/ЗЗЛД (#219 review): when the bidder is a sole-trader (ЕТ) / natural person, keep this contract + // page — which carries the „Сигнали за риск" box — out of search indexes, mirroring the noindex on + // sole-trader company profiles (company.tsx) and their exclusion from the sitemap. The contract stays + // fully public on the site; only search-engine amplification of a named individual + risk label is + // avoided. + if (c && isNaturalPersonProfileName(c.bidder.displayName)) { + tags.push({ name: 'robots', content: 'noindex' }); + } + return tags; } export function headers() {