Головна › Архітектура корпоративної аналітики
Провенанс, лінеаж і комплаєнс
Модель PROV, колонковий лінеаж із логічного плану, детермінований реплей числа для регулятора, стирання за GDPR по всьому графу похідних даних і реальні межі приватних обчислень.
У цьому напрямі
Питання «звідки взялося це число» має інженерну, а не бюрократичну відповідь. Якщо платформа не може назвати кожну трансформацію, версію коду й знімок входу, що дали конкретну клітинку звіту, — число існує лише як твердження. Цей модуль розглядає провенанс як окремий шар архітектури: як фіксувати його з розв'язаного логічного плану, а не з тексту SQL; як зібрати доказовий пакет, що відтворює цифру до копійки; і як виконати стирання, коли персональні дані вже розтеклися в агрегати, ембединги та бекапи. Саме цю задачу вирішує шар лінеажу в онтологічних платформах на кшталт Palantir Foundry.
Модель провенансу і колонковий лінеаж
Три типи вузлів — і більше нічого. Модель W3C PROV зводить провенанс до трьох сутностей: Entity — артефакт (версія таблиці, файл, ваги моделі); Activity — запуск, що спожив одні артефакти й породив інші; Agent — людина або сервіс, відповідальний за цей запуск. Решта — ребра: used, wasGeneratedBy, wasAttributedTo, wasDerivedFrom. Єдина дисципліна, яку тут не можна порушувати: сутність незмінна. Перезапис таблиці не оновлює вузол, а створює новий. Інакше граф починає брехати заднім числом — учорашній звіт посилатиметься на сьогоднішній вміст, і жоден аудит цього не помітить.
Три різні питання, які постійно плутають:
- Where — з якої фізичної комірки джерела прийшло значення. Потрібно, коли розслідуєш «чому тут 12, а не 14».
- Why — які вхідні рядки зумовили появу цього рядка на виході (свідки предикату). Це основа стирання й оцінки впливу.
- How — якою послідовністю операцій це отримано. Це основа відтворення.
Каталог, що показує лише ребра «таблиця → таблиця», не відповідає на жодне з трьох питань, хоча зовні виглядає як лінеаж.
Колонковий лінеаж беруть із плану, а не з тексту запиту. Текст SQL — ненадійне джерело: SELECT * не називає колонок, вкладені в'юхи ховають два рівні перейменувань, USING зливає дві колонки в одну, а CTE перевикористовується під різними псевдонімами. Розв'язаний логічний план уже містить те, чого в тексті немає: зірочку розкрито до переліку атрибутів, кожен атрибут має стабільний ідентифікатор, а вираз кожної вихідної колонки — це дерево над конкретними вхідними атрибутами. Звідси лінеаж витягається механічно й розрізняє два класи ребер: DIRECT (значення справді потрапило у вихід) і INDIRECT (колонка вплинула через WHERE, GROUP BY чи JOIN, але її значень у виході немає). Для комплаєнсу це принципова різниця: непряме ребро не є розголошенням, але воно є залежністю — і при стиранні враховується.
Політики їдуть по тих самих ребрах. Якщо customers.tax_id має тег PII, кожна колонка, у виразі якої він бере участь, успадковує тег автоматично. Зняти тег можна лише явним downgrade із записаним автором і причиною — наприклад, для хешу з сіллю, що зберігається окремо. Ручне маркування вітрин не масштабується й розсинхронізується за тиждень.
Покриття вимірюйте, прогалини позначайте. Python-UDF, виклик зовнішнього сервісу чи foreachPartition непрозорі для аналізу плану. Такий вузол має отримати явний статус opaque, а не бути тихо пропущеним. Робоча метрика — частка колонок вітрин із відомим походженням до кореня; нижче 90% граф не годиться для аналізу впливу, і знати це треба заздалегідь, а не під час інциденту.
{
"eventType": "COMPLETE",
"eventTime": "2026-03-11T02:14:07.318Z",
"run": {
"runId": "b1f0c2de-8a41-4f6b-9d55-0c9a7e2f4411",
"facets": {
"parent": { "job": { "namespace": "risk", "name": "daily_exposure" } },
"codeVersion": { "gitSha": "9f2c1ab", "image": "sha256:4d7e0b31...9c1" }
}
},
"job": { "namespace": "risk", "name": "risk.build_exposure_daily" },
"inputs": [
{ "namespace": "delta://lake", "name": "core.positions",
"facets": { "version": { "datasetVersion": "4217" } } },
{ "namespace": "delta://lake", "name": "ref.fx_rates",
"facets": { "version": { "datasetVersion": "88" } } }
],
"outputs": [
{ "namespace": "delta://lake", "name": "risk.exposure_daily",
"facets": {
"columnLineage": {
"fields": {
"exposure_eur": {
"inputFields": [
{ "name": "core.positions", "field": "notional",
"transformations": [ { "type": "DIRECT", "subtype": "TRANSFORMATION", "masking": false } ] },
{ "name": "ref.fx_rates", "field": "rate",
"transformations": [ { "type": "DIRECT", "subtype": "TRANSFORMATION", "masking": false } ] },
{ "name": "core.positions", "field": "book_id",
"transformations": [ { "type": "INDIRECT", "subtype": "GROUP_BY", "masking": false } ] }
]
}
}
}
} }
]
}
// DIRECT = the value reached the output. INDIRECT = it only shaped the grouping.
// Both are dependencies for erasure; only DIRECT is a disclosure.
На практиці
Роздрібний банк із 14 200 таблицями будував лінеаж регулярними виразами по тексту SQL і мав 61% покриття колонок. Перехід на витяг із розв'язаного логічного плану Spark підняв покриття до 94%, а решту 6% (Python-UDF і виклики зовнішнього скорингу) система стала позначати як opaque замість того, щоб мовчки їх пропускати. Аналіз впливу перед зміною типу однієї колонки скоротився з трьох тижнів ручного трасування до 40-хвилинного запиту до графа.Антипатерн
Будувати лінеаж регулярними виразами по тексту SQL. `SELECT *`, вкладені в'юхи та `USING` дають правдоподібний, але хибний граф — і головна шкода в тому, що він виглядає повним, тож ніхто його не перевіряє. Хибне ребро гірше за відсутнє: відсутнє змушує розслідувати, хибне закриває питання неправильною відповіддю.Доказовий пакет: детермінований реплей числа
Що насправді просить регулятор. Не «покажіть журнал», а «відтворіть клітинку R3.C7 звіту станом на 28 лютого». Це задача ретроспективного провенансу: граф конкретного запуску, а не схема пайплайну. Тут і проходить межа між журналом аудиту й провенансом. Журнал каже, що відбулося: джоба X записала таблицю Y о 03:12 від імені svc_etl. Провенанс дає змогу повторити й отримати те саме число.
Чотири піни, без яких реплей неможливий:
- Версія кожного входу. Не «таблиця positions», а
positions@4217— номер версії Delta, snapshot-id Iceberg або хеш об'єкта. Читання «поточного» стану робить реплей недетермінованим за побудовою. - Дайджест коду. git-sha недостатньо: та сама ревізія в іншому базовому образі дає інший numpy і інші округлення. Пінити треба
sha256образу. - Параметри запуску. Дата обробки, курси, пороги — усе, що джоба читає ззовні, має прийти явним аргументом.
- Детермінізм самої логіки.
current_date(),rand(),first()безORDER BY,collect_set, порядок редукції чисел із рухомою комою — кожен із них тихо руйнує відтворюваність.
Доказовий пакет — це зворотне замикання графа. Від вихідної клітинки йдемо назад по ребрах wasDerivedFrom, збираючи всі проміжні артефакти аж до сирих екстрактів джерельних систем. Кожен вузол на шляху несе свої чотири піни. Правило приймання просте й перевіряється одним запитом: якщо хоч один вузол у замиканні не має версії входу, дайджесту образу або позначений як недетермінований — число недоказове. Це бінарний результат, а не оцінка «загалом непогано».
Форма самого воркфлоу теж перевіряється. Пайплайн як граф задач — це мережа Петрі, і для неї є класична властивість soundness: із будь-якого досяжного стану досяжне завершення, після завершення не лишається завислих токенів, і немає мертвих переходів, які ніколи не спрацьовують. Формальна перевірка ловить те, чого не ловить жоден тест на даних: гілку, яка за певної комбінації прапорців записує вихід двічі, або крок, що ніколи не виконується й тому мовчки лишає вітрину на позаминулому тижні.
Піни зберігають під час запуску, а не відновлюють потім. Спроба реконструювати координати з git-історії й розкладу оркестратора через півроку — це припущення, а не доказ. Колектор має писати подію в момент виконання, синхронно з комітом транзакції, інакше в графі з'являться запуски без результату й результати без запусків.
-- Backward closure: everything a single reporting cell depends on.
-- prov_edge is written by the lineage collector from the resolved logical plan;
-- prov_run holds the four replay pins for the run that produced each edge.
WITH RECURSIVE closure(out_ds, out_col, in_ds, in_col, run_id, depth) AS (
SELECT e.out_ds, e.out_col, e.in_ds, e.in_col, e.run_id, 1
FROM prov_edge e
WHERE e.out_ds = 'risk.exposure_daily'
AND e.out_col = 'exposure_eur'
AND e.business_date = DATE '2026-02-28'
UNION ALL
SELECT e.out_ds, e.out_col, e.in_ds, e.in_col, e.run_id, c.depth + 1
FROM prov_edge e
JOIN closure c ON e.out_ds = c.in_ds AND e.out_col = c.in_col
WHERE c.depth < 32 -- cycle guard: a sound provenance graph is acyclic
)
SELECT c.depth, c.in_ds, c.in_col,
r.dataset_version, r.git_sha, r.image_digest, r.params, r.is_deterministic
FROM closure c
JOIN prov_run r USING (run_id)
ORDER BY c.depth, c.in_ds;
-- Acceptance gate. The figure is provable only when this returns zero rows:
-- SELECT * FROM <closure above>
-- WHERE dataset_version IS NULL
-- OR image_digest IS NULL
-- OR is_deterministic = false;
На практиці
У страховика одна клітинка регуляторного звіту проходила 11 стрибків пайплайну. Реплей на закріпленій версії Delta 4217 і образі sha256:4d7e… відтворив число до копійки. З 40 показників форми три виявилися невідтворюваними: джоба викликала `current_date()` усередині логіки замість того, щоб приймати дату обробки параметром. Виправлення зайняло дві години; знайти це без графа провенансу було неможливо — тести на даних проходили щоразу.Антипатерн
Плутати журнал аудиту з провенансом. Запис «джоба X записала таблицю Y о 03:12, користувач svc_etl» відповідає на питання «що відбулося», але не містить жодного з чотирьох пінів. Регулятор не питає, що відбулося, — він просить повторити й отримати те саме число.Стирання за GDPR і межі приватних обчислень
Стирання — це пряме замикання графа, а не DELETE. За півроку персональний рядок встигає розтектися у вітрини, агрегати, feature store, індекс ембедингів, кеші, ваги моделі та бекапи. Стаття 17 GDPR стосується всіх копій, тож алгоритм починається не з видалення, а з обходу лінеажу вперед від кожного набору, де присутній суб'єкт.
Чотири класи похідних артефактів — і для кожного своя дія:
- Відтворюваний із джерела — видалити й перематеріалізувати. Дешево, якщо пайплайн ідемпотентний.
- Агрегат вище порога — якщо мінімальна група ≥ k і немає унікальних комбінацій квазіідентифікаторів, артефакт можна аргументувати як неперсональний. Аргумент має бути записаний із числами, інакше під час перевірки його доведеться вигадувати.
- Незмінне сховище — WORM, object-lock, стрічка, 90-денні бекапи. Переписати фізично неможливо, тож застосовують крипто-шредінг: дані суб'єкта зашифровані окремим ключем, і знищення ключа робить шифротекст недоступним назавжди.
- Необоротне, але персональне — ембединги, ваги моделі, статистики feature store. Це справжній важкий випадок: ескалація, рішення на рівні DPO, зазвичай перенавчання за розкладом.
Tombstone обов'язковий. Без запису в suppression-list нічний синк із CRM поверне суб'єкта наступного ранку, і система звітуватиме про «виконане» стирання щомісяця. Список зберігає лише хеш ідентифікатора — самого ідентифікатора там бути не може.
Семантика важливіша за механіку. Політика стирання має визначати не «рядок видалено з таблиці T», а «жодне майбутнє обчислення не може залежати від цих даних». Це різні твердження, і перевіряє аудитор саме друге.
Чого приватні обчислення не дають. Псевдонімізація не є анонімізацією: доки існує таблиця відповідності, це й далі персональні дані. k-анонімність ламається зв'язуванням із зовнішнім набором, якщо квазіідентифікатори достатньо вузькі. Диференційна приватність дає справжню гарантію, але її бюджет ε вичерпується й композиційно складається: після N запитів до одного набору бюджет закінчився, і наступний запит треба відхилити — це операційне обмеження, яке має жити в рушії, а не в інструкції для аналітика. І окремо: технічна доступність даних для запиту не означає доречності потоку — той самий рядок, доречний у розслідуванні шахрайства, недоречний у маркетинговій сегментації.
# Forward closure from a data subject's rows to every derived artefact, then
# classify each artefact by the only property that decides the action:
# can it be rebuilt, is it aggregated above k, or is it physically immutable?
REBUILD, AGGREGATE_OK, SHRED, ESCALATE = (
"rebuild", "aggregate_ok", "crypto_shred", "escalate")
def erasure_plan(subject_id, graph, catalog, k_threshold=50):
plan, roots = [], catalog.datasets_holding(subject_id)
for ds in graph.forward_closure(roots): # follows DIRECT *and* INDIRECT edges
meta = catalog.get(ds)
if meta.immutable or meta.object_lock: # WORM, tape, 90-day backups
action = SHRED # destroy the per-subject DEK
elif meta.rebuildable_from_source:
action = REBUILD # delete + re-materialise
elif meta.is_aggregate and meta.min_group_size >= k_threshold:
action = AGGREGATE_OK # record the argument WITH the numbers
else:
action = ESCALATE # embeddings, weights, feature store
plan.append({"dataset": ds, "action": action,
"min_group_size": meta.min_group_size, "sla_days": 30})
# Without this the subject is re-ingested from the CRM on the next nightly sync
# and the same erasure gets "completed" again every month.
plan.append({"dataset": "suppression_list", "action": "insert_tombstone",
"key_hash": catalog.pseudonymise(subject_id)})
return plan
На практиці
У телеком-оператора один запит на стирання торкнувся 47 похідних наборів. 31 із них перебудували за 6 годин, 9 закрили аргументом «агрегат із мінімальною групою ≥ 50», 7 лежали в незмінному Parquet під object-lock — для них знищили персональний ключ шифрування. Після впровадження графа замість ручного пошуку медіана виконання запиту впала з 26 днів (при законному ліміті 30) до 4 днів, а частка запитів, де суб'єкт повертався наступного місяця, — з 18% до нуля завдяки suppression-list.Антипатерн
Виконати `DELETE` у джерельній таблиці й вважати справу закритою. Нічний агрегат справді перерахується без цього рядка, але індекс ембедингів, feature store і 90-денні бекапи його зберігають, а синк із CRM наступного ранку поверне суб'єкта назад. Формально стирання «виконане» — фактично дані на місці, і саме це знаходить перевірка.Джерела, з яких виведено напрям
- A Framework for Policies over Provenance
- Cryptographically Secure Information Flow Control on Key V
- A Role and Attribute Based Access Contro
- A Formal Foundation for Workflow Composi
- A Petri net based supervisory control im
- A High Level Distributed Execution Frame
- A Semantic Hierarchy for Erasure Policies
- Need to Know Contextual Integrity Grounded Query Rewriting
- When PETs misbehave A Contextual Integrity analysis
- A Hybrid Approach to Private Record Link
Пройти інтерактивно
У кожного напряму є питання, картки з інтервальним повторенням і облік прогресу. Для них потрібен акаунт — безкоштовний і на одну хвилину.
Відкрити інтерактивний трек Створити безкоштовний акаунт