Skip to content

docs(etl): target architecture and phased plan (RFC) - #174

Merged
todorkolev merged 3 commits into
midt-bg:mainfrom
nedda76:docs/etl-architecture
Jul 3, 2026
Merged

docs(etl): target architecture and phased plan (RFC)#174
todorkolev merged 3 commits into
midt-bg:mainfrom
nedda76:docs/etl-architecture

Conversation

@nedda76

@nedda76 nedda76 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Добавя docs/etl-architecture.md — RFC за целевата ETL архитектура и реда на изпълнение.

Обобщава дискусията observe-vs-gate (#139 / #154) и включва коментарите за raw зона, идемпотентност и retry стратегия. Описва:

Само документация — без промяна по код. Документ за обсъждане преди имплементацията.

Свързано: #103 (архитектурна диаграма / data-flow), docs/etl-pipeline-state.md.


Текущо състояние

  • CI зелено (Lint / typecheck / test), merge state CLEAN — готов за merge.
  • Освен RFC документа, PR-ът носи и commit 75418b2 style(web): apply prettier formatting to risk indicators. Това форматира apps/web/app/components/RiskIndicators.tsx и apps/web/app/lib/riskLogic.test.ts — двата файла, които 2d93cd5 ("clear repo-wide prettier debt and make CI lint blocking") остави неформатирани. Тъй като pnpm lint вече е блокиращ и тече за целия repo, тези два файла чупеха CI на всеки отворен PR.
  • Този fix замества style(web): apply prettier formatting to risk indicators #175 (затворен като излишен) — съдържанието е същото.

@cefothe

cefothe commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Преглед на документацията — ETL целева архитектура (RFC)

Прегледът е направен с екип от 4 специализирани агента (двама data-engineer, един за SERVED зоната, един архитектурен), всеки от които стъпва на реалния код (apps/etl/src/index.ts, packages/ingest/, scripts/refresh-slice.sql, scripts/integrity-checks.mjs), а не само на текста. И четиримата независимо стигнаха до едни и същи носещи проблеми — силен сигнал.

Какво е PR-ът: само документация (docs/etl-architecture.md, +107) + prettier reformat на два несвързани web файла. CI зелено; сканиране за сигурност CHISTO (без тайни, без нови зависимости, единственият URL е вече съществуващият източник storage.eop.bg).

Обща оценка: APPROVE-WITH-COMMENTS. Като RFC за обсъждане документът е точен, последователен и в правилната посока. Като спецификация за имплементация има два проблема с висок приоритет, които трябва да се решат, преди да започне работа по #163/#158 — като и двата са места, където текстът противоречи на вече съществуващия код.


Преглед на данните — трите зони

RFC-ът коректно поставя истинския проблем: деплойнатият sigma-etl cron е единственият derive път без assertIntegrity gate и без FX зареждане — потвърдено, apps/etl/src/index.ts няма нито едно извикване на двете; те живеят само в scripts/import.mjs (CLI пътя). Автономният, ненадзираван път е този без предпазна мрежа. Трите зони го решават чисто.

🟫 RAW зона — ingest буфер (нова D1)

Плюсове: коректно изолира преходния мрежов retry от детерминистичния derive; дневната партиция по източник (ocds:<date>) е естествена единица за идемпотентност; correctness-first е правилният принцип за проект за прозрачност.

Минуси / отворени въпроси:

  • „Upsert по уникален ключ" никъде не е дефиниран и кодът не го прави. packages/ingest/src/staging.ts:34-50 е DELETE WHERE source = ? + INSERTняма UNIQUE constraint на нито една raw таблица (work-staging-schema.sql); идемпотентността идва от изтриването по партиция, не от row-level ключ. За персистентен RAW store това има значение: прекъсване по средата на chunk цикъла оставя деня липсващ, не остарял.
  • OCDS releases-vs-records е пренебрегнат. ocid идентифицира процес, не публикация на договор; по-късен bucket може да преиздаде коригиран release. RFC-ът трябва да дефинира правилото за предимство — (source_day, ocid, release.id) срещу (ocid, contract.id).
  • Амендментите не са адресирани. releaseToAmendments пази само .slice(-1)[0]; derive-amendments.sql/promote-amendments.sql изобщо не са в cron пътя.
  • Конкурентност: scheduled() извиква REFRESH.create() безусловно на всеки 6 часа без singleton lock — припокриващи се runs + CLI записи се състезават за същата source партиция. „Single writer" е твърдение, не гаранция.
  • Няма retention политика / анализ на разходите за втора персистентна D1; и „RAW" всъщност е 60-колонна нормализирана схема, не raw-JSON архив (така че „преизчисли от RAW" реално значи „пресвали от EOP").

🟨 STAGING / integrity gate

Плюсове: глобалната композитна проверка (не slice-local) е правилното прозрение — slice-local проверка структурно не може да види остарелия rollup от старата страна на пре-атрибутиран entity. assertIntegrity SELECT-ът вече е написан да върви евтино на D1. Фазирането v1 (reject цяла партида) → v2 (entity карантина) е дисциплинирано.

Минуси — класификацията на инвариантите във Фаза 0 има конкретни грешки:

  • Inv 3 (EIK validity) е грешно класифициран като row-attributable — той е structural. Той валидира гаранцията на самата Sigma при нормализация (eik_valid=1 ⇒ eik_normalized числов), зададена в bidder upsert на refresh-slice.sql. Провал значи, че парсерът е счупен за цялата партида → reject на партидата, не per-entity карантина.
  • Inv 2 е със смесена природа, не row-attributable. value_flag='ok' AND amount_eur<0 е structural (sign-flip в derive — коментарът в кода го казва); само не-ok клонът е row-attributable, и той е само WARN.
  • Inv 4 (date sanity) винаги връща ok: true (integrity-checks.mjs:265). Тригерът на RFC-а „невъзможна дата → карантина" е мъртъв код спрямо текущата проверка — routing логика върху него тихо не прави нищо.
  • Inv 5 се self-skip-ва, когато pipeline_stats е остарял/липсва — нуждае се от преразглеждане при изпълнение pre-promotion срещу staging.
  • Entity „маркерът за непълнота" рискува да се сблъска със съществуващия suspect брояч (CompanyDetail.suspect брои value_suspect договори) — два различни UI сигнала; нужно е отделно поле.

🟩 SERVED / mart зона — публикуваният read слой

Плюсове: gate-before-publish е реално подобрение спрямо днешния post-publish „ship-and-alert"; „reject пази served на последните валидни данни" е издържано, защото derive-ът е scoped/идемпотентен.

Минуси — централното противоречие на целия RFC:

  • „Приложи цялата мутация като една атомарна batch()" противоречи на деплойнатия код. apps/etl/src/index.ts:112-126 нарочно разделя refresh-а на ~22 отделни batch() групи именно защото една batch надхвърля Workers CPU/size бюджета (packages/ingest/src/refresh.ts:55-58). Пълен refresh от ~194k договора не може да е една batch. Ако промоцията е multi-batch, тя не е атомарна — провал между batch N и N+1 оставя SERVED полу-промотирана.
  • Cross-database атомарност е невъзможна. Ако RAW/STAGING е „нова D1 база" (ред 24), не може да се направи атомарен batch() между D1 инстанции. Изводът „не е нужен blue-green" трябва да се преразгледа — shadow-table RENAME swap е сам по себе си евтин blue-green и вероятно е правилният примитив.
  • Няма инвалидиране на edge cache. Web чете същата D1, в която cron-ът пише (apps/web/wrangler.jsonc и apps/etl/wrangler.toml свързват sigma), а apps/web/app/lib/cache.ts сервира s-maxage до 1ч + 24ч stale-while-revalidate без purge hook. Гаранцията „SERVED никога не показва грешни числа" изтича на cache слоя, който RFC-ът не споменава — и лоша партида, ако някога изтече, остава на edge до 24ч.
  • Четците могат да наблюдават полу-приложено състояние по средата на промоцията; rollback по средата е недефиниран.

Между зоните

Scope хигиена: обединяването на prettier fix-а е приемливо — PR описанието обяснява, че тези два файла чупеха CI за целия repo след като 2d93cd5 направи lint блокиращ; промяната е само whitespace, нула логика.


Заключение

RFC-ът е мержим като RFC (което е целта му). Ядрото на архитектурата е коректно и вярно стъпило на кода. Двете находки за дискусията по #163/#158 преди да се пише код:

  1. „Една атомарна batch()" още не е реализуема — противоречи на разделянето на 22 batch-а, което кодът вече налага, и не може да обхване две D1. Решете реалния примитив за промоция.
  2. FX трябва да предхожда (или да е включен в) gate-а, иначе gate-ът минава зелено върху тихо непълни валутни данни.

Плюс поправка само по документацията, която си струва да се направи сега: коригирайте таблицата с инварианти във Фаза 0 (Inv 3 → structural; разделете Inv 2; отбележете, че Inv 4 в момента не може да задейства карантина).


Преглед, изготвен от екип специализирани агенти; находките са проверени спрямо текущия код в main.

@nedda76

nedda76 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Благодаря за изчерпателния преглед, Stefan — всичко се потвърди спрямо кода. Обнових docs/etl-architecture.md (commit fbf024e) с поправките:

За дискусия при имплементацията остават точният примитив за промоция и дефиницията на RAW уникалния ключ.

@nedda76

nedda76 commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator Author

Този ПР може да се мърджне as-is - както се разбрахме да се сложи нисък приоритет и да се имплементира спрямо зададен роудмеп, когато се стигне до тази част.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

#157, #174 и #178 правят идентичния prettier reflow на RiskIndicators.tsxriskLogic.test.ts). #178 е специалният форматиращ PR, който владее тези редове. Предложение: първо #178, после #157 и #174 rebase-ват и махат промените по RiskIndicators.tsx/riskLogic.test.ts — извън обхвата им са (AGENTS.md). Иначе двата от трите ще конфликтват на точно тези редове.

@todorkolev
todorkolev merged commit a896d69 into midt-bg:main Jul 3, 2026
1 check passed
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