Daily platform report with day-over-day finding diffs - #8
Conversation
Closes #6. The platform was accumulating a narrative and discarding it. ops.monitor_results has carried run_at since Task 18 and nothing ever read it back. Worse, the one artifact that looked like a history was destroying one: incident slugs are deliberately stable so a recurring finding rewrites one file rather than spawning many, which means write_incident OVERWRITES and yesterday is gone. Nothing could answer "when did this start?" or "did yesterday's problem clear?" Two append-only Iceberg tables: ops.finding_log every finding on every run, keyed on sensors.finding_key ops.agent_runs one row per run, findings or not The second exists because a clean run writes no finding rows, so without it "ran and found nothing" and "never ran" are the same empty table -- and a report would render the second as a clean bill of health. Same empty-delta blind spot this codebase has now closed four times (quarantine_rate SELECT 0.0, range on an all-NULL column, freshness on no values, an enum watch on an empty column). src/ops/report.py assembles seven sections. build_report() takes a dataclass and returns a string: it is handed no engine, so it CANNOT compute a metric even by accident, which is what makes "the report recomputes nothing" a property rather than a comment. A test asserts the module never references monitors.evaluate, sensors.detect or validator.validate. A report that recomputes can disagree with the monitor that raised the alert, and the report is the one people believe. Absences are reported as absences: no run recorded says so instead of rendering clean; no monitor results is "an absence of evidence, not a pass"; no previous run says "nothing to diff against" instead of calling every standing finding new; a monitor with no prior value prints an em dash, never 0. An unknown finding kind routes to Errors rather than vanishing. drift-demo --day N applies one scheduled change instead of all four, so the agent can run between them and the log accumulates a real timeline. Bare drift-demo is unchanged. reports/ holds a four-day sequence from one continuous history: clean, +column, rename, then the rest plus a volume collapse -- 0, 1, 2, 8 findings. Fixed a latent bug found on the way in: incident_slug() hashed table|kind|column|change with no monitor name, so EVERY monitor_breach on a table hashed identically. With one breach firing that was invisible; with two the second incident file silently overwrote the first, and this feature's diff would have reported a finding as "cleared" while it was still breaching. Extracted sensors.finding_key() as the single definition of finding identity, shared by the incident filename and the log row -- they must agree or "first seen" describes something other than the file on disk. Incident filenames also stopped rendering "None" for breaches carrying no column. The committed incident slugs change accordingly; this is a one-time migration. Also fixed: the new tests reached graph.run()'s act node and wrote into the repo's real docs/incidents/. test_agent_graph and test_bronze already guard against this; test_report now uses the same autouse fixture. The suite no longer dirties the committed artifact set. Reviewed: 275 tests pass (250 before), ruff clean, four-day sequence re-run end to end at full scale and every committed report and incident regenerated from it. Day four runs one day past the end of the data on purpose, which is why three arrival SLAs breach; stated in the README rather than left to look accidental. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G
…render was unstable Two defects found by probing the shipped code rather than re-reading the diff. 1. `make agent` twice on one date -- an ordinary thing to do -- appended the same logical finding to ops.finding_log twice, and the report counted rows rather than findings. One open finding rendered as "2 still open". A diff whose counts are wrong is worse than no diff, because the counts are read as a measurement. `_latest_per_key` collapses to one row per finding_key per run; `make monitor` gets the same treatment. 2. The diff sections were rendered in set-iteration order, so regenerating a report reshuffled its lines with no content change. Reports are committed artifacts: an unstable render puts noise in every git diff, and a diff that is usually noise stops being read. Sorted by finding key, with a test that renders twice and compares. Also added a test that an unescaped pipe in detail text cannot split a Markdown table cell -- it was already handled, but nothing pinned it. Reviewed: 279 tests pass (275 before), ruff clean. reports/daily-2026-07-01.md re-rendered under the stable ordering; counts are unchanged (6 new, 0 cleared, 2 still open), only line order. 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
Review round 1 — two defects found, both fixedProbed the shipped code rather than re-reading the diff. Neither would have failed a test that only asked "does the report render?" 1. Running
A day-over-day diff whose counts are wrong is worse than no diff, because the counts are read as a measurement. 2. The report was not byte-stable across regenerations. The diff sections were built from set differences and rendered in set-iteration order, so simply regenerating a report reshuffled its lines with no content change. These are committed artifacts — an unstable render puts noise in every git diff, and a diff that is usually noise stops being read. Caught by regenerating Verification
|
Closes #6.
The problem
The platform was accumulating a narrative and throwing it away.
ops.monitor_resultshas carriedrun_at, partitioned by month, since Task 18 — and nothing ever read it back.make monitorwrote; no code queried.Worse, the one artifact that looked like a history was destroying one.
incident_slug()is deliberately stable so a recurring finding rewrites a single file instead of spawning one per run — the right call, made deliberately, and the reasondocs/incidents/is committable at all. But it meanswrite_incidentoverwrites. Nothing in the repo could answer "when did this start?" or "did yesterday's problem clear?"What changed
Two append-only Iceberg tables —
ops.finding_log(every finding, every run, keyed onsensors.finding_key) andops.agent_runs(one row per run, findings or not).The second is not redundant. A clean run writes no finding rows, so without it "the agent ran and found nothing" and "the agent never ran" are the same empty table — and a report would render the second as a clean bill of health. That's the same empty-delta blind spot this codebase has now closed four times.
src/ops/report.pyassembles seven sections and amake reporttarget.The report reads; it cannot measure
build_report(snapshot) -> stris handed a dataclass, not an engine. It cannot compute a metric even by accident, which makes "recomputes nothing" a property rather than a comment. A test also asserts the module never referencesmonitors.evaluate,sensors.detectorvalidator.validate.A report that recomputes can disagree with the monitor that raised the alert — and when they disagree, the report is the one people believe.
Absences are reported as absences
NO RUN RECORDED— "not a clean bill of health"0—A four-day story
drift-demo --day Napplies one scheduled change instead of all four, so the agent runs between them.reports/holds four files from one continuous warehouse history — 0 → 1 → 2 → 8 findings:Day four fills every section: two schema changes still open, a volume collapse,
bronze_txn_row_countbreaching, three arrival gaps. It runs one day past the end of the data on purpose — that's why the arrival SLAs fire, and the README says so rather than letting it look accidental.A latent bug found on the way in
incident_slug()hashedtable | kind | column | changewith no monitor name, so everymonitor_breachon a table hashed identically:With one monitor breaching, invisible. With two, the second incident file silently overwrote the first — and this feature's diff would have reported a finding as "cleared" while it was still breaching. That is the worst possible lie for a day-over-day report to tell.
Fixed by extracting
sensors.finding_key()as the single definition of finding identity, shared by the incident filename and the log row. Committed incident slugs change accordingly — a one-time migration.And one in my own tests
The new tests reached
graph.run()'sactnode and wrote into the repo's realdocs/incidents/.test_agent_graphandtest_bronzealready guard against exactly this;test_reportnow uses the same autouse fixture. Verified the suite no longer dirties the committed artifact set.Verification
275 passed(250 before), ruff clean.🤖 Generated with Claude Code
https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G