Recipe № 1 Impact analysis¶
Problem. You want to deprecate customer_features (or change its schema). What
breaks?
Approach. Models declare their inputs and outputs; connect() builds the edges.
downstream() then returns everything that depends on a node — directly or
transitively.
from model_ledger import Ledger, DataNode
ledger = Ledger()
ledger.add([
DataNode("customer_features", platform="feature-store", outputs=["customer_features"]),
DataNode("fraud_scorer", platform="ml", inputs=["customer_features"], outputs=["risk_scores"]),
DataNode("churn_scorer", platform="ml", inputs=["customer_features"], outputs=["churn_scores"]),
DataNode("review_queue", platform="alerting", inputs=["risk_scores"]),
])
ledger.connect()
# Everything that depends on customer_features, directly or transitively:
blast_radius = ledger.downstream("customer_features")
print(blast_radius)
# ['fraud_scorer', 'churn_scorer', 'review_queue']
Expected output. Three consumers: two models directly (fraud_scorer,
churn_scorer) and one queue transitively (review_queue). Don't deprecate until
those are handled.
graph LR
CF["customer_features"] --> FS["fraud_scorer"] --> RQ["review_queue"]
CF --> CS["churn_scorer"]
classDef hot fill:#7a1a1a,color:#fff,stroke:#5a1010;
classDef dep fill:#efe8da,stroke:#7a1a1a,color:#1c1a17;
class CF hot; class FS,CS,RQ dep;
The same question, from an agent¶
// trace(name="customer_features", direction="downstream")
{ "nodes": [
{"name": "fraud_scorer", "depth": 1},
{"name": "churn_scorer", "depth": 1},
{"name": "review_queue", "depth": 2}
] }
Claude: Deprecating
customer_featuresbreaks 3 things —fraud_scorerandchurn_scorerconsume it directly, andreview_queuedepends on it one hop further.
Variations¶
ledger.upstream("review_queue")— the reverse: everything that feeds a node.ledger.trace("review_queue")— the full path from roots to a node.- Use
DataPortwhen several models write a table with the same name, so the blast radius is precise rather than over-broad.