← Zurück zum Journal
Tools

Von der These zum Nachweis, hands-on — Cordatas OSS-Projekte

László Hadházy·2. November 2026·19 Min.

OpenlineageMCPDSGVOData Governance

Cordata veröffentlicht vier Open-Source-Pakete für Python, und dieser Beitrag führt durch alle vier — gemeinsam an einem Datensatz ausgeführt, statt README für README beschrieben. Sie sind aus den Beiträgen auf dieser Website entstanden: ein Pipeline-Executor, der seine eigenen Nachweise emittiert, ein kleiner Emitter für Code, der selbst keine Lineage erzeugt, ein Werkzeug, das aus diesen Events die Verzeichnisse macht, die eine Datenschutzbeauftragte verlangt, und ein Katalog-Server, den ein KI-Agent abfragen kann. Zusammen sind sie die praktische Seite des Bogens, den diese Website von den Pillars über die Pipeline-Serie gespannt hat und den Teil 2 schließt — von der These zum Nachweis: Hier wird der Nachweis ausgeführt statt beschrieben. Jedes Paket liegt auf PyPI, und der Durchgang unten beginnt mit pip install, ohne dass etwas geklont wird.

Der Datensatz ist eine Tabelle bewerteter Kartentransaktionen mit einer IBAN-Spalte — der Kontonummer, die nach der DSGVO ein personenbezogenes Datum ist. Die Frage ist die, die eine Datenschutzbeauftragte zu einer solchen Spalte tatsächlich stellt: Wo liegen diese Daten, und wenn jemand ihre Löschung verlangt — hat die Löschung alle erreicht?

Der Katalog kennt die Transaktionstabelle, in der die IBANs ankommen, die bewertete Tabelle, die daraus entsteht, und jede Pipeline, die erklärt, dass sie die Quelle liest — und er liegt bei alledem richtig. Die Lineage findet einen weiteren Ort, an den die IBANs gelangt sind: eine CSV-Datei, die ein Export-Job geschrieben und nirgends registriert hat und deren einzige Spur das Event ist, das der Job beim Lauf emittiert hat. Diese Kopie zu finden braucht beide Arten von Werkzeug, und genau das zeigt der Rest dieses Beitrags.

§ 1 — Die vier Pakete im Überblick

Paket Beantwortet Lizenz Verwendete Version
cordata-pipeline-runtime Was soll diese Pipeline tun, und hat sie es getan? MIT 0.2.9
art30-emit Wie sagt Code ohne Lineage-Integration, warum er verarbeitet? Apache-2.0 0.1.0
qedro Was ist passiert, und kann ich es einem Prüfer nachweisen? Apache-2.0 0.10.1
cordata-catalog-mcp Welche Policy gilt für diesen Datensatz gerade? MIT 0.3.0

Die ersten drei sprechen OpenLineage, einen offenen Standard für die Events, die ein Job beim Lauf emittiert: welcher Job, welche Datensätze er gelesen und geschrieben hat, und beliebig viele Facets — typisierte Anhänge wie ein Schema oder eine Klassifizierung. Das vierte spricht das Model Context Protocol, MCP, das Protokoll, mit dem ein KI-Assistent Tools auf einem Server aufruft, den er nicht selbst gebaut hat.

Wie die vier Pakete zusammenhängenDurchgezogene Pfeile tragen OpenLineage-Events, der gestrichelte nicht. Die Pipeline-Runtime führt drei Beispiel-Pipelines aus und emittiert Events in ein Verzeichnis mit zeilengetrennten JSON-Dateien. Ein separates Export-Skript, umhüllt von art30-emit, emittiert seine Events in dasselbe Verzeichnis. Qedro liest dieses Verzeichnis und erzeugt das Verzeichnis nach Art. 30 und die Löschprüfung. Der Katalog-Server liest entlang des gestrichelten Pfeils das Deskriptor-Modell und den Beispielkatalog der Runtime als Python-Abhängigkeit, nie die Events, und beantwortet die Fragen eines KI-Agenten über MCP. Gemeinsam haben Katalog und Events nur den Namen jedes Datensatzes.

emittiert Events

emittiert Events

liest Events

Modell und Beispielkatalog,
nie die Events

antwortet über MCP

cordata-pipeline-runtime
führt drei Pipelines aus

Export-Skript
umhüllt von art30-emit

OpenLineage-Events
ein Verzeichnis mit
.ndjson-Dateien

qedro
Verzeichnis nach Art. 30, Löschprüfung

cordata-catalog-mcp
Katalog-Server

KI-Agent

Das Diagramm in Worten: Die durchgezogenen Pfeile tragen OpenLineage-Events, der gestrichelte nicht. Die Runtime und das Export-Skript emittieren beide Events in ein Verzeichnis mit zeilengetrennten JSON-Dateien, und qedro liest dieses Verzeichnis. Der Katalog-Server liest die Events nie. Er installiert die Runtime als Abhängigkeit, liest ihr Deskriptor-Modell und ihren Beispielkatalog und antwortet einem Agenten über MCP. Gemeinsam haben beide Seiten, wie sie einen Datensatz benennen — darauf baut § 5 auf.

Alles Folgende lief am 2. November 2026 in einem leeren Verzeichnis mit Python 3.14 — die Pakete brauchen 3.12 oder neuer:

python -m venv .venv && . .venv/bin/activate
pip install 'cordata-pipeline-runtime[run]==0.2.9' art30-emit==0.1.0 \
    qedro==0.10.1 cordata-catalog-mcp==0.3.0

Alle vier lassen sich ohne Konflikt in eine Umgebung installieren. Die Ausgabeblöcke sind aus diesem Lauf kopiert und nur durch Weglassen von Zeilen gekürzt, was … markiert.

§ 2 — cordata-pipeline-runtime: drei Pipelines, ein Event-Strom

cordata-pipeline-runtime ist der Executor aus Eine Pipeline ist ein Deskriptor, kein Programm und dessen zweitem Teil: Eine Pipeline ist eine YAML-Deklaration ihrer Quelle, ihrer SQL-Schritte, ihrer Qualitätserwartungen und ihrer Policy-Tags, und ein generischer Executor führt jede solche Deklaration aus. pipeline-runtime demo legt in dem Verzeichnis, das man ihm übergibt — hier walk —, ein lokales Beispiel an, einen DuckDB-Katalog anstelle von Glue und Parquet-Dateien als Warehouse, und führt seine drei Pipelines aus, die ihre OpenLineage-Events nach walk/out schreiben:

$ pipeline-runtime demo walk
seed        50,000 transactions at v7, 10,240 CDC claims rows
[7a06] descriptor  transactions-scored-daily (fraud)      apiVersion v1 OK
[7a06] schema      fraud_raw.transactions                 pinned v7, catalog v7 OK
[7a06] read        glue_table                             50,000 rows
[7a06] step        filter_settled        (sql)            35,919 rows
[7a06] step        score                 (sql)            35,919 rows
[7a06] policy      sensitivity=high residency=eu subject_type=customer resolved against ontology
[7a06] expect      transactions_scored                    5 passed, 0 failed
[7a06] write       fraud_curated.transactions_scored      iceberg, 1 partition key
[7a06] emit        COMPLETE → lineage adapter             inputs=1 outputs=1
…
[639b] descriptor  merchant-settlement-daily (finance)    apiVersion v2 OK
[639b] schema      fraud_raw.transactions                 pinned v7, catalog v7 OK
…
[639b] emit        COMPLETE → lineage adapter             inputs=1 outputs=1
 
events      walk/out/lineage.ndjson

Zwei Fachbereiche lesen fraud_raw.transactions: Fraud bewertet die Transaktionen, Finance rechnet daraus mit den Händlern ab. Jedes Event, das ein Lauf emittiert, trägt den deklarierten Zweck und die Rechtsgrundlage der Pipeline als processing-Facet; der geschriebene Datensatz trägt die aufgelösten Policy-Tags als standardisiertes tags-Facet, und der Lauf nennt das Anwendungs-Release, das die gelesenen Daten veröffentlicht hat. Nichts davon wird separat aufgeschrieben, also kann nichts davon vom tatsächlichen Lauf abweichen.

Die Demodaten sind synthetisch: Die Runtime erzeugt ihre IBANs aus einem festen Zufalls-Seed, keine davon ist ein echtes Bankkonto.

§ 3 — Ein Export-Skript mit art30-emit umhüllen

Pipelines sind der einfache Fall, weil sie ohnehin von etwas ausgeführt werden, dem man das Emittieren beibringen kann. Die Kopien, die verloren gehen, entstehen woanders: in einem Skript, das jemand geschrieben hat, um die hoch bewerteten Transaktionen an die Betrugsermittlung zu schicken, gestartet aus einem Cron-Eintrag, ohne etwas zu emittieren. Dieses hier exportiert jede Transaktion mit einem Score ab 0,5 samt IBAN in eine CSV-Datei:

import os
 
import duckdb
from art30_emit import declare
 
SCORED = "cordata://fraud/fraud_curated.transactions_scored"
EXPORT = "file://localhost/exports/fraud_cases.csv"
 
os.makedirs("exports", exist_ok=True)
 
with declare(
    "cordata.fraud/case-export",
    purpose="fraud-investigation",
    legal_basis="legitimate-interest",
    reads=[SCORED],
    writes=[EXPORT],
    classify={
        EXPORT: {
            "data_category": "financial",
            "subject_type": "customer",
            "residency": "eu",
            "retention": "1y",
        }
    },
):
    duckdb.sql("""
        COPY (
            SELECT tx_id, account_id, iban, amount_eur, fraud_score
            FROM read_parquet(
                'walk/warehouse/cordata-fraud-curated-eu-central-1/transactions_scored/**/*.parquet',
                hive_partitioning = true)
            WHERE fraud_score >= 0.5
        ) TO 'exports/fraud_cases.csv' (HEADER)
    """)

declare(...) aus art30-emit ist die einzige Ergänzung zu einem ansonsten gewöhnlichen Skript. Es emittiert beim Eintritt ein START-Event und beim Austritt ein COMPLETE — oder FAIL, wenn die Arbeit eine Ausnahme wirft — mit demselben processing-Facet, das die Runtime emittiert, und demselben standardisierten tags-Facet. Selbst entscheidet es nichts: Kein Zweck wird aus dem Jobnamen erschlossen, und ein Feld, das der Aufrufer nicht deklariert hat, fehlt im Event, statt leer zu sein — denn niemand hat es gesagt und es gibt keines sind für einen Prüfer zwei verschiedene Antworten.

Der Datensatz, den es liest, ist genau so benannt, wie die Runtime ihn benennt, cordata://fraud plus fraud_curated.transactions_scored, sodass sich die beiden Event-Ströme ohne jedes Mapping verbinden. Der Lauf schreibt seine Events neben die der Runtime:

ART30_EMIT_FILE=walk/out/case-export.ndjson python case_export.py

§ 4 — qedro: das Verzeichnis nach Art. 30 und der Löschpfad

qedro liest OpenLineage-Events und erzeugt die Artefakte, die eine Datenschutzbeauftragte verlangt. Das erste ist das Verzeichnis von Verarbeitungstätigkeiten, das Art. 30 DSGVO einem Verantwortlichen vorschreibt — wer welche Daten zu welchem Zweck und auf welcher Rechtsgrundlage verarbeitet —, meist von Hand in einer Tabellenkalkulation gepflegt, die die Pipelines so beschreibt, wie sich jemand an sie erinnert hat.

Dieses Verzeichnis wiegt schwerer, als sein Format vermuten lässt. Art. 30 Abs. 4 verpflichtet den Verantwortlichen, es der Aufsichtsbehörde auf Anfrage vorzulegen, und Art. 5 Abs. 2 macht ihn dafür verantwortlich, die Einhaltung nachweisen zu können, nicht bloß sie einzuhalten. In der Praxis wird aus dieser Pflicht eine Handvoll wiederkehrender Probleme, und jedes davon ist ein qedro-Befehl:

  • Ein Verzeichnis, das veraltet. Von Hand gepflegt, stimmt es an dem Tag, an dem es zuletzt jemand aktualisiert hat, und weicht mit jedem Deployment danach weiter ab — unsichtbar, denn ein veraltetes Verzeichnis liest sich genau wie ein aktuelles. qedro ropa erzeugt es neu aus dem, was die Pipelines tatsächlich getan haben, und jede Zeile sagt, ob sie auf einem emittierten Fakt, auf einem von jemandem geschriebenen Mapping oder auf einer Deklaration beruht.
  • Prüfungsvorbereitung von Hand. Wenn eine Aufsichtsbehörde, ein Prüfer oder der Datenschutzfragebogen eines Kunden wissen will, wie personenbezogene Daten verarbeitet werden, wird die Antwort oft in der Woche vor der Frist aus einem Wiki und dem Gedächtnis einzelner zusammengesetzt. Hier ist sie eine Datei, die auf Abruf entsteht — als Text, Markdown, JSON oder als die Tabelle, die ein Prüfer verlangt —, mit einer eigenen Aussage darüber, was sie abdeckt und was nicht.
  • Änderungen ohne Dokumentation. qedro diff vergleicht zwei Verzeichnisse, sodass ein monatlicher Lauf zeigt, was sich geändert hat — auch einen Zweck, der noch aufgeführt ist, den aber nichts mehr emittiert. Bekommt es auf einer Seite das eigene Register des KI-Governance-Teams, meldet es, wo Register und Pipelines sich widersprechen und wem jede widersprechende Zeile gehört.
  • Löschanfragen mit Frist. Art. 12 Abs. 3 gibt dem Verantwortlichen einen Monat, um auf eine Anfrage zu reagieren, verlängerbar um zwei weitere nur bei komplexen oder zahlreichen Anfragen, und das Schwierige ist jede abgeleitete Kopie, die jemand vergessen hat. qedro erasure folgt der Lineage vorwärts von der gelöschten Tabelle und listet jeden Nachfahren auf — und ob ein Job ihn seither neu geschrieben hat.
  • KI-Verordnung im Anmarsch. Für eine Organisation, die ein Hochrisiko-KI-System betreibt, liest qedro ropa --view deployer die Fragen an den Betreiber aus demselben Verzeichnis ab: Zweck und Rechtsgrundlage, die Modellversionen, die jeder Lauf gemeldet hat, und die Eingaben, die er gelesen hat.

Nichts davon braucht Cordatas andere Pakete. qedro liest OpenLineage aus dbt, Airflow, Spark, Flink oder allem anderen, das es emittiert, und je reicher die Facets eines Emitters, desto mehr vom Verzeichnis steht auf Nachweisen statt auf der Mapping-Datei.

Auf den Events der Demo prüft der erste Befehl, ob sie überhaupt lesbar sind:

$ qedro events walk/out
  8 events · 7 jobs · 6 datasets · 2 files  ∎
  complete 4, other 3, start 1

Das ∎ ist das Beweisende-Zeichen, und qedro druckt es nur, wenn die Ausgabe auf ihren eigenen Nachweisen steht. Für das Verzeichnis muss der Verantwortliche benannt sein — eine Aussage über eine Organisation, die keine Lineage tragen kann —, also steht er in einer qedro.yaml von vier Zeilen:

controller:
  name: Example Finanz GmbH
  contact: [email protected]
 
domains: [fraud, policy, finance]

Alles andere kommt aus den Events:

$ qedro ropa walk/out --config qedro.yaml
Record of processing activities — Example Finanz GmbH
  contact: [email protected]
…
  cordata.fraud/case-export
    purpose       fraud-investigation
    legal basis   legitimate-interest
    categories    financial
    subjects      customer
    residency     eu
    retention     1y
    reads         cordata://fraud/fraud_curated.transactions_scored
    writes        file://localhost/exports/fraud_cases.csv
    runs          1 in window, last 2026-11-02T07:41:19.615221+00:00
 
  cordata.fraud/transactions-scored-daily
    purpose       fraud-detection
    legal basis   legitimate-interest
    subjects      customer
    residency     eu
    unclassified  1 of 2 datasets carries no classification:
                  cordata://fraud/fraud_raw.transactions
    reads         cordata://fraud/fraud_raw.transactions
    writes        cordata://fraud/fraud_curated.transactions_scored
    runs          1 in window, last 2026-11-02T07:41:18.132260+00:00
…
  Scope of this record
    source        walk/out
…
    provenance    7 evidenced, 0 from the mapping file, 0 undeclared
…
  every activity stands on emitted evidence   ∎

Sieben Tätigkeiten, jeder Zweck und jede Rechtsgrundlage aus einem emittierten Facet, keine aus der Mapping-Datei, auf die qedro sonst zurückgreifen würde. Das Export-Skript steht im Verzeichnis, weil es emittiert hat; ein Skript, das nichts emittiert, erschiene nur, wenn jemand es deklariert — als Behauptung gekennzeichnet.

Das zweite Artefakt beantwortet die Löschhälfte der Frage. Eine Anfrage nach Art. 17 DSGVO bedeutet, die Daten einer Person aus der Quelle und aus allem zu löschen, was daraus abgeleitet ist, und jedes Scheitern daran ist eine abgeleitete Kopie, die jemand vergessen hat. qedro erasure folgt der Lineage vorwärts vom gelöschten Datensatz:

$ qedro erasure walk/out --dataset fraud_raw.transactions --since 2026-11-02T00:00:00+00:00
Erasure of cordata://fraud/fraud_raw.transactions
 
    tombstone     2026-11-02T00:00:00+00:00 — from --since, not emitted
 
    cordata://finance/finance_curated.merchant_settlement_daily
      derived by    cordata.finance/merchant-settlement-daily
      state         rewritten 2026-11-02T07:41:18.689888+00:00 — 1 run since the tombstone
 
    cordata://fraud/fraud_curated.transactions_scored
      derived by    cordata.fraud/transactions-scored-daily
      state         rewritten 2026-11-02T07:41:18.132260+00:00 — 1 run since the tombstone
 
      file://localhost/exports/fraud_cases.csv
        derived by    cordata.fraud/case-export
        state         rewritten 2026-11-02T07:41:19.615221+00:00 — 1 run since the tombstone
…
    propagation   3 of 3 rewritten since the tombstone, 0 not
…
  this record does not claim to be a proof:
    ! the erasure instant came from --since rather than from an emitted lifecycle event, so every rewrite below is measured against a date somebody typed

Drei Nachfahren, auf einem Zweig zwei Stufen tief, und die CSV-Datei ist die zweite Stufe. Der Pfad erreicht sie nur, weil das Export-Skript ein Event emittiert hat, das seine Eingabe so benennt, wie die Runtime sie benennt. Der Finance-Zweig ist da, weil die Runtime die Tabelle, die Finance liest, nach dem Fachbereich benennt, dem sie gehört, und nicht nach dem, der sie liest — bis Version 0.2.2 tat sie das nicht, und der Pfad verfehlte die Tabelle von Finance ganz (pipeline-runtime#7, beim Bau dieser Demo gefunden).

Das Zeichen bleibt hier aus, und das zu Recht. Die Runtime emittiert kein Event, das die Löschung selbst markiert, also ist der Zeitpunkt der hinter --since eingetippte, und qedro sagt das, statt Neuschreibungen an einem Datum zu messen, als hätte es jemand nachgewiesen. Außerdem sagt es in jedem Lauf, dass es Datensätze meldet und keine Zeilen: Ein neu geschriebener Nachfahre ist ein Nachweis dafür, dass die Löschung weitergegeben wurde, nicht dafür, dass die Zeilen einer bestimmten Person verschwunden sind.

§ 5 — cordata-catalog-mcp fragen, wo die IBANs liegen

cordata-catalog-mcp ist der Server, der in Der Katalog ist die API entworfen wurde: sechs lesende Tools über den Katalog — Ownership, Schema, LF-Tags (die Schlüssel-Wert-Klassifizierungen von Lake Formation), Reichweite von Grants und deklarierte Leser — für einen KI-Agenten, der über MCP verbunden ist. Er liest das Deskriptor-Modell der Runtime und nie eine Datenzeile. Ein kurzer Client stellt ihm dieselben Fragen:

import asyncio
import json
import sys
 
from mcp import Client
from mcp.client.stdio import StdioServerParameters
 
ANALYST = {
    "domain": ["fraud"],
    "sensitivity": ["public", "internal", "confidential"],
    "residency": ["eu"],
}
 
 
async def main():
    # The same server cordata-catalog-mcp starts, launched from this venv.
    server = StdioServerParameters(command=sys.executable, args=["-m", "catalog_mcp"])
    async with Client(server) as catalog:
        found = await catalog.call_tool("find_datasets", {"tags": {"subject_type": "customer"}})
        for match in found.structured_content["datasets"]:
            print("customer data in", f"{match['domain']}/{match['table']}")
 
        scored = await catalog.call_tool(
            "get_dataset", {"domain": "fraud", "table": "transactions_scored"}
        )
        dataset = scored.structured_content
        iban = next(c for c in dataset["columns"] if c["name"] == "iban")
        print("get_dataset owner", dataset["metadata"]["owner"])
        print("get_dataset iban", json.dumps(iban))
        print("get_dataset lineage", json.dumps(dataset["lineage"]))
 
        grant = await catalog.call_tool(
            "check_grant_expression",
            {"domain": "fraud", "table": "transactions_scored", "expression": ANALYST},
        )
        print("check_grant_expression", json.dumps(grant.structured_content))
 
        readers = await catalog.call_tool(
            "get_consumers", {"domain": "fraud", "table": "transactions"}
        )
        for consumer in readers.structured_content["consumers"]:
            print("declared reader of fraud/transactions:", consumer["pipeline"],
                  f"({consumer['domain']}, pins v{consumer['pins']})")
 
 
asyncio.run(main())
customer data in fraud/transactions_scored
customer data in policy/claims_deduped
get_dataset owner [email protected]
get_dataset iban {"name": "iban", "type": "VARCHAR", "lf_tags": {"domain": "fraud", "sensitivity": "high", "residency": "eu", "subject_type": "customer"}}
get_dataset lineage {"namespace": "cordata://fraud", "name": "fraud_curated.transactions_scored"}
check_grant_expression {"reaches": false, "columns": []}
declared reader of fraud/transactions: merchant-settlement-daily (finance, pins v7)
declared reader of fraud/transactions: transactions-scored-daily (fraud, pins v7)

Der Katalog weiß, wem die bewertete Tabelle gehört, dass ihre IBAN-Spalte als high klassifiziert ist und dass die Analysten-Rolle — berechtigt für jede Sensitivität außer high — nichts davon erreicht. Dafür ist ein Katalog da, und nichts im Event-Strom könnte das beantworten: Lineage sagt, was passiert ist, und ein Grant sagt, wer darf.

Gefragt, wer die Transaktionstabelle liest, in der die IBANs ankommen, nennt er die Bewertungs-Pipeline von Fraud und die Abrechnungs-Pipeline von Finance, jeweils mit der Schemaversion, die sie pinnen. Diese Liste ist die der Runtime selbst: get_consumers ruft dieselbe Funktion auf, mit der die Prüfung auf Produzentenseite den Build einer Anwendung scheitern lässt, wenn eine Schemaänderung einen gepinnten Leser brechen würde — Katalog und Prüfung können sich also nicht darüber uneinig sein, wer von der Tabelle abhängt.

Diese Prüfung ist das Argument des Data-Contracts-Pillars in einem Befehl: Jeder Konsument deklariert die Version, die er liest, die Liste der Konsumenten entsteht aus diesen Deklarationen, statt von Hand gepflegt zu werden, und der Build des Produzenten scheitert, wenn eine vorgeschlagene Änderung einen davon brechen würde. Die Runtime liefert mit ihrem Beispiel zwei vorgeschlagene Releases der Anwendung hinter der Transaktionstabelle mit. Auf den Katalog der Demo gerichtet, liest die Prüfung den Vorschlag, der eine Spalte umbenennt, die beide Pipelines verwenden:

EXAMPLE=$(python -c 'import os, pipeline_runtime.example as e; print(os.path.relpath(e.__path__[0]))')
CORDATA_CATALOG=walk/catalog.duckdb python -m pipeline_runtime.consumers \
    "$EXAMPLE/proposals/card-ledger-v5.0.0.yml" "$EXAMPLE/domains"
card-ledger release v5.0.0 proposes fraud_raw.transactions v8; the catalog is at v7
2 descriptors under .venv/lib/python3.14/site-packages/pipeline_runtime/example/domains read fraud_raw.transactions
 
BREAKS      merchant-settlement-daily (finance)  pins v7  owner [email protected]
            .venv/lib/python3.14/site-packages/pipeline_runtime/example/domains/finance/pipelines/merchant_settlement.yml
            + merchant_ref (VARCHAR, not null)
            - merchant_id (VARCHAR, not null)   <- breaks
 
BREAKS      transactions-scored-daily (fraud)  pins v7  owner [email protected]
            .venv/lib/python3.14/site-packages/pipeline_runtime/example/domains/fraud/pipelines/transactions_scored.yml
            + merchant_ref (VARCHAR, not null)
            - merchant_id (VARCHAR, not null)   <- breaks
 
2 of 2 consumers fail this check

Sie endet mit Exit-Code 1, und genau das macht daraus in der CI der Anwendung einen gescheiterten Build — bevor das Release ausgeliefert wird, nicht nachdem eine Pipeline gebrochen ist. Der andere Vorschlag, v4.12.0, fügt nur eine nullable Spalte hinzu: Dieselbe Prüfung endet mit 0 und markiert beide Pipelines als BUMP — nichts, was sie lesen, ändert sich, aber jede hält bei ihrem nächsten Lauf an, bis ihr Pin auf v8 wandert, sodass keine Pipeline je eine Version liest, gegen die sie nicht geschrieben wurde.

Das Export-Skript steht auf keiner der beiden Listen. Es deklariert dem Katalog nichts — keinen Deskriptor, keine registrierte Ausgabe —, also hat der Katalog weder vom Job noch von der CSV-Datei, die er schreibt, eine Spur, und ein Agent, der fragt, wo diese Daten liegen, bekommt eine vollständige Antwort über alles, was ihm gemeldet wurde. Eine weitere Folge davon, wo Klassifizierung liegt: Die Quelltabelle selbst gilt als unklassifiziert, weil die Klassifizierung der Tabelle einer Anwendung deren Eigentümer obliegt, und deshalb liefert find_datasets mit subject_type=customer sie nicht, obwohl sie die IBANs enthält.

Der Katalog liegt bei jedem Datensatz richtig, der bei ihm registriert wurde. Die Kopie, auf die es bei einer Löschung ankommt, ist eine, die nie registriert wurde, und ihre einzige Spur ist das Event, das der Job beim Lauf emittiert hat.

Das lineage-Feld, das get_dataset zurückgegeben hat, ist die Stelle, an der sich beide Antworten treffen. Der Katalog nennt den Datensatz fraud/transactions_scored; die Events nennen ihn cordata://fraud plus fraud_curated.transactions_scored, weil OpenLineage einen Datensatz über Namespace und Namen identifiziert. Seit Version 0.2.0 liefert der Katalog die Lineage-Form neben seiner eigenen, sodass ein Agent, der mit beiden verbunden ist, von einem Datensatz, den der Katalog kennt, nachschlagen kann, was daraus abgeleitet wurde — so taucht die CSV-Datei auf —, ohne dass einer der beiden Server den anderen liest.

§ 6 — Gemeinsames Vokabular und eine bewusste Abhängigkeit

Nur eines der vier Pakete importiert ein anderes. qedro und art30-emit lassen sich ohne die Runtime installieren, und qedro darf insbesondere von nichts AWS-Förmigem abhängen: Es liest Klassifizierungen aus dem standardisierten tags-Facet, gleich, was sie erzeugt hat, sodass LF-Tags eine mögliche Quelle neben Purview-Klassifizierungen oder Policy-Tags von Google Cloud sind und keine davon vorausgesetzt wird.

Der Katalog-Server ist das eine, das es tut, und zwar bewusst. Er installiert das Basispaket der Runtime — die Deskriptor-Modelle, die Policy-Prüfung gegen die Ontologie und seit 0.3.0 das Modul, das die Pins der Pipelines liest — und nichts von ihrem Ausführungs-Stack. Ein Vokabular und ein Benennungsschema lassen sich zwischen Paketen kopieren und auf Abweichungen prüfen; ein Pin-Parser nicht, denn zwei Kopien würden auseinanderlaufen, und ein anders gelesener Pin ist eine Liste von Lesern, die der eigenen Prüfung des Produzenten widerspricht.

Was alle vier teilen, ist Vokabular, und dieser Anteil ist auf eine Weise partiell, die sich genau benennen lässt:

Ontologie der Runtime Vokabular von qedro art30-emit
processing-Facet: purpose, legal_basis emittiert liest emittiert
subject_type, residency, retention ja ja, und residency erlaubt zusätzlich eea gibt weiter, was der Aufrufer deklariert
sensitivity, domain ja kein Begriff —
data_category kein Begriff ja gibt weiter, was der Aufrufer deklariert
Datensatznamen cordata://<domain> + database.table liest sie, wie sie kommen wie vom Aufrufer angegeben

Das processing-Facet ist die stärkste Verbindung. Sein JSON-Schema wird im Repository von qedro aus dem Processing-Modell der Runtime erzeugt, mit einem Test, der fehlschlägt, sobald beide auseinanderlaufen, und art30-emit implementiert dasselbe veröffentlichte Schema — zwei unabhängige Emitter eines Facets. Die Metadata- und Contract-Modelle der Runtime sind zugleich das, was der Katalog-Server zurückgibt, sodass sein Output-Schema aus dem Modell abgeleitet und nicht nachgeschrieben ist.

Die vierte Zeile der Tabelle ist die Lücke. Die Ontologie der Runtime klassifiziert nach Sensitivität, und qedro meldet die Kategorien personenbezogener Daten, nach denen Art. 30 Abs. 1 lit. c fragt, aus data_category, das die Runtime nie emittiert. Deshalb meldet die Tätigkeit des Export-Skripts categories financial und die der Pipelines keine.

Den übrigen Code getrennt zu halten, hat eine Nebenwirkung, die sich lohnt: Jedes Paket wird von den anderen auf eine Weise geprüft, wie es seine eigenen Tests nicht tun. Drei Korrekturen der Runtime im Oktober wurden gefunden, indem qedro ihre Events gelesen hat, nicht durch einen Test in der Runtime — Validierungsjobs, die Zweck und Rechtsgrundlage verloren (pipeline-runtime#9), ein Datensatz, der in einem Event klassifiziert war und im nächsten nicht (#10), und ein Signaturbefund zu einem Commit, den nichts benannt hatte (#11).

§ 7 — Bekannte Grenzen dieser Versionen

Jede der folgenden Grenzen gilt für die oben genannten Versionen:

  • Kategorien personenbezogener Daten bleiben für die Datensätze der Pipelines leer, aus dem Grund in § 6. data_category in die Ontologie der Runtime aufzunehmen ist eine Entscheidung über die Ontologie, keine Korrektur.
  • Der Löschzeitpunkt wird eingetippt, nicht emittiert. Nichts in der Runtime emittiert das lifecycleStateChange-Facet von OpenLineage für eine Löschung, also braucht die Löschprüfung --since und hält ihr Zeichen zurück.
  • Ein Lauf aus einer installierten Kopie nennt keinen Commit. Ein installiertes Paket liegt in keinem Arbeitsverzeichnis eines Repositorys, also melden seine Läufe keinen Commit, keinen Signaturbefund und kein standardisiertes sourceCodeLocation-Facet. Das ist so gewollt: Bis 0.2.5 meldete ein solcher Lauf descriptor_git_commit_signed: false, ohne einen Commit zu nennen, was sich wie ein als unsigniert gemeldeter Commit liest, und pipeline-runtime#11 hat stattdessen beide Felder weggelassen, weil fehlende Provenienz keine schlechte Provenienz ist. descriptor_sha256 ist davon unberührt, sodass das Verzeichnis weiterhin nachweist, welche Deskriptor-Bytes gelaufen sind, und nur einen Schritt vor dem Commit endet, aus dem sie stammen. Aus einem Checkout — git clone --branch v0.2.9 und pip install -e — emittiert dieselbe Demo den Commit und das standardisierte Facet: das Facet seit 0.2.8, und die Demo läuft in einem Klon überhaupt erst seit 0.2.7.
  • Die Spaltenlisten des Katalogs sind ein Platzhalter. Ein Deployment liest sie aus dem Glue-Katalog jedes Fachbereichs; hier übernimmt eine JSON-Datei diese Rolle, und die CI prüft sie gegen das Parquet, das die Pipelines schreiben.

Wenn Sie einen Katalog betreiben und abwägen, ob sich Lineage lohnt — oder diesem Argument eine Lücke nachweisen können —, freue ich mich über Ihre Sicht unter cordata.tech/contact. Weiterführend: Der Katalog ist die API zum Entwurf hinter dem Katalog-Server, Teil 2 der Pipeline-Serie zu den Events, die die Runtime emittiert, und der AI-Readiness-Pillar dazu, warum aus diesen Events abgeleitete Verzeichnisse über die DSGVO hinaus zählen.