One YAML per feed, replacing layer-keyed config and three Python tuples - #9
Conversation
…ython tuples Closes #3. The repo already had declarative config. What it lacked was config organised by FEED. Ten monitors lived in monitors/{bronze,silver,gold}.yaml keyed by layer, and the rest of a feed's operational facts lived in Python: graph.py:WATCHED, arrival.py:ARRIVAL_SLAS, runner.py:TYPED_TABLES. Adding a feed meant editing every layer file plus three modules, and nothing described one feed in one place. feeds/<name>.yaml now declares layers, contracts, monitors, arrival SLAs, which layer the agent watches, and which layers get per-contract column-type checks. Three feeds: yodlee_transactions, stock_prices, and forecast_training_set -- the last a derived feed, because a Gold mart has the same operational needs as an ingested one and belongs to neither upstream feed cleanly. Iceberg schemas stay in Python: they declare field ids and partition transforms, and moving those to YAML would be config theatre. THE MIGRATION WAS GATED ON MEASUREMENT, NOT ON TESTS PASSING. The risk here is not failing loudly, it is succeeding while quietly changing a threshold or dropping a monitor with every test still green -- because the tests were written against the same config that moved. So before any code changed, run_monitors, check_arrival and the agent were captured on TWO warehouses. Two, because a clean one yields 17 ok results and 0 findings, and an all-ok baseline is satisfied by a migration that breaks every check into silence. It caught a real bug on the first comparison. Dropping a now-unused `schemas` import left a NameError in runner, and run_monitors' deliberately broad except turned it into a breach on ALL SEVENTEEN monitors. Seventeen results appeared and the run exited non-zero -- output that reads as a platform correctly reporting everything is on fire. Only the before/after diff showed every metric was null and every detail read "monitor failed: NameError". After the fix both captures match exactly: 17 monitor results, 8 arrival checks, and on the drifted warehouse the same 5 findings with the same severities. The expectations are pinned in tests/test_feeds.py as literals, deliberately not derived from feeds/*.yaml -- a check that reads its expectation from the thing under test checks nothing. The one remaining difference was silver_txn_mean_abs_amount at the 13th decimal. Measured rather than assumed: three consecutive runs of identical code give 171.4137026799999, ...800034, ...800003, because DuckDB sums in parallel. The report was rendering that as a +1.56e-12 change; _delta now returns 0 below a 1e-9 relative threshold, six orders of magnitude below anything a monitor acts on. Presenting float noise as signal churned the committed reports every run. A typo'd `kind` is now rejected at config load, naming the file, the monitor and the valid kinds, instead of surfacing as a breach. A breach means "your data is bad"; a typo means "your config is bad", and conflating them sends someone to look at a table. The runtime breach path is KEPT, not replaced -- a MonitorDef can be built without the loader and one bad monitor must never take down the other sixteen -- and now has its own test. Also fixed, surfaced by feeds loading alphabetically: 0007's render-ordering fix covered the diff sections and missed the verdict, schema, anomaly and incident renderers, which iterated in check-execution order. Reordering the config churned three committed reports with identical content. Every render path now sorts on a key ending in finding_key or monitor. That defect predated this change and was invisible because nothing had reordered the checks yet. Reviewed: 300 tests pass (279 before), ruff clean, both golden captures match, the four-day report sequence re-run end to end with identical finding counts (0, 1, 2, 8), and reports verified byte-stable across two regenerations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G
…tform Probing the shipped loader found the same failure this repo keeps closing, one level up from where it had been closed before. 1. An empty or missing feeds/ directory loaded zero feeds silently. Every monitor, arrival SLA and agent watch is declared there now, so a deleted or mistyped directory made `make monitor` print "0 checks -- 0 breach, 0 warn" and exit 0: a platform reporting perfect health because it was looking at nothing. Centralising the config concentrated this risk -- losing one monitors/<layer>.yaml used to lose a third of the checks; losing feeds/ now loses all of them plus the SLAs plus the agent's watch list. load_feeds raises. 2. Two feeds with `typed: true` on the same table minted colliding monitor names. Column-type monitors are generated as `<table>_type_<field>`, so this is exactly the baseline corruption the duplicate-name check prevents, arriving by a route that check could not see -- generated names never appear in feed.monitors. Same for two feeds watching one table, which would raise every finding on it twice. 3. A `tables:` alias naming an unknown table was unvalidated. It would fail at query time and be swallowed into a breach: visible, but reported as a data problem when it is a config typo. Two feeds merely naming the same table stays legal -- a shared dimension is a real thing. Only the collisions that corrupt something are rejected. Reviewed: 305 tests pass (300 before), ruff clean, and both golden captures re-run after the hardening -- 17 monitors, 8 arrival, 0 findings clean and 5 drifted, all still identical to the pre-migration baseline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G
Review round 1 — four findings, all fixedProbed the shipped loader rather than re-reading the diff. 1. An empty or missing This is the failure this repo keeps closing, one level up from where it had been closed before. Every monitor, arrival SLA and agent watch is declared in …and exit 0. A platform reporting perfect health because it is looking at nothing. Worth naming plainly: centralising the config concentrated this risk. Losing one 2. Two feeds typing the same table minted colliding monitor names. Column-type monitors are generated as 3. A 4. Two feeds merely naming the same table — left legal. A shared dimension is a real thing; only the collisions that actually corrupt something are rejected. Verification
|
Closes #3.
The axis was wrong, not the amount of config
The repo already had declarative config — ten monitors in
monitors/{bronze,silver,gold}.yaml, validated against aKINDSregistry that fails loudly. What it lacked was config organised by feed. A feed was spread across four places, three of them Python:monitors/{bronze,silver,gold}.yamlgraph.py:WATCHEDarrival.py:ARRIVAL_SLASrunner.py:TYPED_TABLESfeeds/<name>.yamlnow holds all of it. Three feeds —yodlee_transactions,stock_prices, andforecast_training_set(a derived feed: a Gold mart has the same operational needs as an ingested one and belongs to neither upstream feed cleanly).Iceberg schemas stay in Python. They declare field IDs and partition transforms — that's code, and moving it to YAML would be config theatre.
The migration was gated on measurement, not on tests passing
The risk here isn't failing loudly. It's succeeding while quietly changing a threshold or dropping a monitor — with every test green, because the tests were written against the same config that moved.
So the gate came first. Before any code changed,
run_monitors,check_arrivaland the agent were captured on two warehouses. Two, because a clean one yields 17okresults and 0 findings, and an all-okbaseline is satisfied by a migration that breaks every check into silence.It caught a real bug on the first comparison. Dropping a now-unused
schemasimport left aNameErrorinrunner, andrun_monitors's deliberately broadexcept— which exists so one malformed monitor can't kill a run — turned it into a breach on all seventeen monitors.Seventeen results appeared. The run exited non-zero. Nothing about that output says "broken" — it reads as a platform correctly reporting that everything is on fire. Only the before/after diff showed every
metricwasnulland everydetailreadmonitor failed: NameError: name 'schemas' is not defined.After the fix, both captures match exactly: 17 monitor results, 8 arrival checks, and on the drifted warehouse the same 5 findings with the same severities.
The one difference turned out not to be the migration
silver_txn_mean_abs_amountdiffered at the 13th decimal. Measured rather than assumed — three consecutive runs of identical code:DuckDB sums in parallel. Relative magnitude ~2.5e-15, irrelevant to the z-score that consumes it — but the daily report was rendering it as a
+1.56e-12change, churning the committed reports on every regeneration._deltanow returns0below a1e-9relative threshold.A typo'd
kindis now a config error, not a breachRejected at load, naming the file, the monitor, and the valid kinds. A breach means your data is bad; a typo means your config is bad, and conflating them sends someone to look at a table.
The runtime breach path is kept, not replaced — a
MonitorDefcan be built without the loader, and one bad monitor must never take down the other sixteen. It has its own test now.A second ordering defect this surfaced
Feeds load alphabetically, so the arrival findings changed order and three committed reports churned with identical content. Cause: #6's ordering fix covered the diff sections and missed the verdict, schema, anomaly and incident renderers, which iterated in check-execution order.
Every render path now sorts on a key ending in
finding_keyormonitor. That defect predated this PR and was invisible because nothing had reordered the checks yet.What the loader refuses
Each with a message naming the offending key: a monitor on an undeclared
layer; a table absent fromALL_TABLES; a missingcontract:file;typed: truewith no contract (would generate zero type checks and read as coverage); unknowncalendar/ingest.mode/feed_type/ symbolic row floor; and a duplicate monitor name across feeds — monitor names are the join key to persisted baseline history, so two feeds sharing one would interleave different measurements into a single baseline series.Verification
300 passed(279 before), ruff clean.tests/test_feeds.pyare literals, not derived fromfeeds/*.yaml.🤖 Generated with Claude Code
https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G