Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0fd246f
docs(etl): investigation + fix plan for #305 amendment value double-c…
cefothe Aug 13, 2026
6304730
feat(etl): flag single-annex value double-counts as annex_total_suspe…
cefothe Aug 13, 2026
b5837ce
feat(etl): text-confirmed value correction for amendment double-count…
cefothe Aug 13, 2026
aaab972
fix(ingest): use .ts extension for the amendment-total import so the …
cefothe Aug 13, 2026
6f3641a
feat(ingest): restate exact-2x amendment double-counts to the before-…
cefothe Aug 13, 2026
4be0ad2
fix(ingest): stop restating 'в размер на <N>' increments as totals (#…
cefothe Aug 13, 2026
cb44e75
feat(etl): mark flag-only amendment double-counts per-row and suppres…
cefothe Aug 13, 2026
6709c57
feat(etl): flag multi-annex value double-counts via a prior-annex anc…
cefothe Aug 13, 2026
9004829
fix(etl): scope the multi-annex anchor to legit prior totals; green t…
cefothe Aug 13, 2026
8ffe622
fix(ingest): skip text-free exact-2× restatement on outside-ЗОП contr…
cefothe Aug 13, 2026
883c016
fix(etl): align slice current_value tie-break to natural_key (#305)
cefothe Aug 13, 2026
2afd512
feat(web): mark text-corrected amendment rows in the timeline (#305)
cefothe Aug 13, 2026
8652d69
test(db): drop tautological full-vs-slice cross-equality assertions (…
cefothe Aug 13, 2026
ed87e51
fix(ingest): gate amendment value restatement on a monetary/text anch…
cefothe Aug 13, 2026
6693b16
ci(deploy): probe amendment restated/suspect columns before serving (…
cefothe Aug 13, 2026
9e1c49c
fix(web): blank the known-doubled current value on the contract page …
cefothe Aug 13, 2026
34d93c8
fix(etl): flag chain-contaminated annexes and align slice reconciliat…
cefothe Aug 13, 2026
46fcb07
docs(db): register annex_total_suspect in the value_flag enumerations…
cefothe Aug 13, 2026
03b8384
fix(etl): flag orphan exact-2× annexes (the 84818 class) (#305)
cefothe Aug 13, 2026
c199202
fix(etl): tighten amendment restatement guards against day-counts and…
cefothe Aug 13, 2026
5d1fcfd
style(etl): wrap NON_MONEY_UNIT_AFTER regex to satisfy prettier (#305)
cefothe Aug 13, 2026
36dd21e
fix(etl): veto qualified day/quantity units after amendment figures (…
cefothe Aug 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 59 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,65 @@ jobs:
;;
esac

# #305 additive columns (migrations 0006/0007) must exist BEFORE the Worker serves: the contract-page
# query reads amendments.value_restated/value_suspect, and promote-amendments/refresh-slice also INSERT
# value_treatment — so all three must be present or the read AND the ETL write fail. Same rationale as
# the currency step above: the migration ledger is empty, so `d1 migrations apply` would collide on
# 0000. Probe the actual table and ALTER only the missing columns (SQLite has no ADD COLUMN IF NOT
# EXISTS). These are pure additions with safe defaults (INTEGER NOT NULL DEFAULT 0 / nullable TEXT), so
# no backfill or completion marker is needed — an ALTER populates every existing row. Malformed
# responses are fatal. This also makes 0006/0007 replay-safe when they were applied out-of-ledger.
- name: Apply amendment restated/suspect columns
if: steps.guard.outputs.ok == 'true'
run: |
node scripts/wrangler-render.mjs apps/web/wrangler.jsonc
schema_json="$(pnpm --filter @sigma/web exec wrangler d1 execute "${SIGMA_D1_NAME:-sigma}" \
--config wrangler.deploy.jsonc --remote --yes --json \
--command "SELECT
(SELECT COUNT(*) FROM pragma_table_info('amendments') WHERE name = 'value_restated') AS has_restated,
(SELECT COUNT(*) FROM pragma_table_info('amendments') WHERE name = 'value_treatment') AS has_treatment,
(SELECT COUNT(*) FROM pragma_table_info('amendments') WHERE name = 'value_suspect') AS has_suspect")"

read_flag() {
printf '%s' "$schema_json" | node -e '
const fs = require("fs");
let payload;
try {
payload = JSON.parse(fs.readFileSync(0, "utf8"));
} catch {
process.exit(2);
}
const result = Array.isArray(payload) ? payload[0] : payload;
const row = result && Array.isArray(result.results) ? result.results[0] : null;
const key = process.argv[1];
if (!row || !Object.hasOwn(row, key)) process.exit(2);
process.exit(Number(row[key]) === 1 ? 0 : 1);
' "$1"
}

add_column() {
echo "amendments.$1 missing; adding it."
pnpm --filter @sigma/web exec wrangler d1 execute "${SIGMA_D1_NAME:-sigma}" \
--config wrangler.deploy.jsonc --remote --yes \
--command "ALTER TABLE amendments ADD COLUMN $2"
}

ensure_column() {
set +e
read_flag "$1"
status="$?"
set -e
case "$status" in
0) echo "amendments.$1 already exists." ;;
1) add_column "$1" "$2" ;;
*) echo "::error::Could not determine whether amendments.$1 exists."; exit 1 ;;
esac
}

ensure_column value_restated "value_restated INTEGER NOT NULL DEFAULT 0"
ensure_column value_treatment "value_treatment TEXT"
ensure_column value_suspect "value_suspect INTEGER NOT NULL DEFAULT 0"

# `run deploy`, not `deploy` — bare `pnpm deploy` is a pnpm built-in, not our package script.
- name: Deploy explorer (sigma)
if: steps.guard.outputs.ok == 'true'
Expand Down
8 changes: 5 additions & 3 deletions apps/web/app/lib/assistant/describe-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ export const DATA_TRAPS: string[] = [
'Парични агрегати: СУМИРАЙ САМО `contracts.amount_eur` (каноничен EUR, безопасен за сумиране). ' +
'НИКОГА не сумирай `contracts.amount` — то е „както е записано" в смесена валута (`currency`), само за показване.',
'Канонична база за всяка парична сума: `contracts.amount_eur IS NOT NULL`. НЕ филтрирай по ' +
'`value_flag`: включи `ok`, `review`, `annex_suspect`, `value_low` и поправените `value_suspect` редове.',
'`value_flag`: включи `ok`, `review`, `annex_suspect`, `annex_total_suspect`, `value_low` и ' +
'поправените `value_suspect` редове.',
'`amount_eur IS NULL` означава, че няма използваема EUR стойност (например `value_suspect` без ' +
'прогноза за поправка или чужда валута без FX курс); само тези редове се изключват от парични суми.',
'`value_flag` ∈ {ok, review, annex_suspect, value_suspect, value_low} мени значението на стойността ' +
'на реда, но не и каноничната база; `date_flag` ∈ {ok, signed_after_publication} е вердикт за датата.',
'`value_flag` ∈ {ok, review, annex_suspect, annex_total_suspect, value_suspect, value_low} мени ' +
'значението на стойността на реда, но не и каноничната база; `date_flag` ∈ {ok, ' +
'signed_after_publication} е вердикт за датата.',
"`tenders.procedure_type = 'неизвестна'` маркира СИНТЕТИЧНИ (само-договорни) преписки — " +
'изключи ги при анализ на разпределението по процедура, освен ако нарочно ги искаш.',
'`lots` са на grain по обособена позиция — не ги брой едно към едно срещу `contracts`.',
Expand Down
39 changes: 34 additions & 5 deletions apps/web/app/routes/contract.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -222,16 +222,24 @@ export default function Contract({ loaderData }: Route.ComponentProps) {
<div className="vh now">
<div className="step">Текуща стойност</div>
<strong className="num">{v.currentEur != null ? money(v.currentEur) : '—'}</strong>
{v.suspect && <div className="sub suspect">{UNVERIFIED_VALUE_LABEL}</div>}
{v.currentValueDoubled ? (
<div className="sub suspect">
стойността изглежда двойно отчетена и не се показва
</div>
) : (
v.suspect && <div className="sub suspect">{UNVERIFIED_VALUE_LABEL}</div>
)}
{v.deltaPct != null && (
<div className="delta">{signedPct(v.deltaPct)} спрямо сключване</div>
)}
</div>
</div>
{v.suspect && (
<p className="small muted">
Показана е публикуваната стойност от източника, без СИГМА да я коригира. Виж{' '}
<Link to="/methodology">методология</Link>.
{v.currentValueDoubled
? 'Текущата стойност изглежда двойно отчетена в източника и затова не се показва. '
: 'Показана е публикуваната стойност от източника, без СИГМА да я коригира. '}
Виж <Link to="/methodology">методология</Link>.
</p>
)}
{c.frameworkAwards != null && (
Expand Down Expand Up @@ -267,11 +275,32 @@ export default function Contract({ loaderData }: Route.ComponentProps) {
{c.amendments.map((a, i) => (
<tr key={`${a.documentNumber ?? 'amd'}-${i}`}>
<td>{a.date ? longDate(a.date) : '—'}</td>
{/* #305 residual: an uncorrectable double-count — the source's value_after is the
untrusted doubled figure, so show „—" and mark the row rather than a number we
can't stand behind. A `restated` row is the opposite: СИГМА corrected the doubled
total from the основание text, so we show the corrected number and flag that we
rewrote it. */}
<td className="money">
{a.valueAfterEur != null ? moneyBare(a.valueAfterEur) : '—'}
{a.suspect ? (
<>
— <Chip>непотвърден тотал</Chip>
</>
) : a.valueAfterEur != null ? (
<>
{moneyBare(a.valueAfterEur)}
{a.restated && (
<>
{' '}
<Chip>коригиран тотал</Chip>
</>
)}
</>
) : (
'—'
)}
</td>
<td className="money">
{a.deltaEur != null ? signedMoney(a.deltaEur) : '—'}
{!a.suspect && a.deltaEur != null ? signedMoney(a.deltaEur) : '—'}
</td>
<td className="annex-desc-cell">
<AnnexDescription text={a.description} />
Expand Down
1 change: 1 addition & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [`etl-architecture.md`](etl-architecture.md) — целевата ETL архитектура (RFC): предложение за състоянието и реда на изпълнение.
- [`v1-implementation-plan.md`](v1-implementation-plan.md) — precompute слоят и пагинацията (защо rollup-и и keyset вместо per-request GROUP BY / OFFSET).
- [`implementation-plans/286-ocds-amendment-unp.md`](implementation-plans/286-ocds-amendment-unp.md) — защо OCDS анексите не се свързват с договор (OCID вместо УНП) и планът за поправка през bridge-а `tender.id → УНП` + prefer-EOP dedup (#286).
- [`implementation-plans/305-amendment-value-double-count.md`](implementation-plans/305-amendment-value-double-count.md) — защо стойността на анекс се удвоява (ЦАИС ЕОП слага новия **тотал** в полето за промяна) и планът за откриване/поправка: `annex_total_suspect` флаг + текстова хеуристика за възстановяване на истинския тотал (#305).
- [`integrity-gate.md`](integrity-gate.md) — reconciliation gate-ът: hard asserts върху тоталите при import/CI.
- [`anomaly-report.md`](anomaly-report.md) — cross-row аномалии при опресняване: какво `value_flag` не хваща на ниво отделен договор.
- [`deploy.md`](deploy.md) — деплой към Cloudflare: двата Worker-а (`sigma`, `sigma-etl`) и споделеният D1 per environment.
Expand Down
Loading