Skip to content

Daily platform report with day-over-day finding diffs - #8

Merged
rubinder merged 2 commits into
mainfrom
feat/daily-report
Aug 31, 2026
Merged

Daily platform report with day-over-day finding diffs#8
rubinder merged 2 commits into
mainfrom
feat/daily-report

Conversation

@rubinder

Copy link
Copy Markdown
Owner

Closes #6.

The problem

The platform was accumulating a narrative and throwing it away.

ops.monitor_results has carried run_at, partitioned by month, since Task 18 — and nothing ever read it back. make monitor wrote; 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 reason docs/incidents/ is committable at all. But it means write_incident overwrites. 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 on sensors.finding_key) and ops.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.py assembles seven sections and a make report target.

The report reads; it cannot measure

build_report(snapshot) -> str is 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 references monitors.evaluate, sensors.detect or validator.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

Situation Naive report This one
No agent run empty table → looks clean NO RUN RECORDED — "not a clean bill of health"
No monitor results empty table → looks clean "an absence of evidence, not a pass"
No previous run everything listed "new" "nothing to diff against… not because it is new"
Monitor with no prior value 0
Unknown finding kind silently dropped routed to Errors

A four-day story

drift-demo --day N applies 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:

Compared with `2026-06-29`: **1 new**, **0 cleared**, **1 still open**.

### New
- `[renaming]` column 'checkNumber' was renamed to 'check_reference' (field id 20)

### Still open
- `[additive]` column 'merchantCategoryCode' present in table but not declared
  in contract (open 1d (since 2026-06-29))

Day four fills every section: two schema changes still open, a volume collapse, bronze_txn_row_count breaching, 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() hashed table | kind | column | change with no monitor name, so every monitor_breach on a table hashed identically:

bronze.yodlee_transactions_raw|monitor_breach|None|

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()'s act node and wrote into the repo's real docs/incidents/. test_agent_graph and test_bronze already guard against exactly this; test_report now uses the same autouse fixture. Verified the suite no longer dirties the committed artifact set.

Verification

  • 275 passed (250 before), ruff clean.
  • The four-day sequence re-run end to end at full scale; every committed report and incident regenerated from that one history.

🤖 Generated with Claude Code

https://claude.ai/code/session_0199ePw5w34FyfGAB41mkv3G

rubinder and others added 2 commits August 31, 2026 16:38
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
@rubinder

Copy link
Copy Markdown
Owner Author

Review round 1 — two defects found, both fixed

Probed the shipped code rather than re-reading the diff. Neither would have failed a test that only asked "does the report render?"

1. Running make agent twice in one day doubled the findings.

ops.finding_log is append-only and re-running the agent on the same date is entirely ordinary. The report counted rows, not findings, so one open finding rendered as 2 still open.

A day-over-day diff whose counts are wrong is worse than no diff, because the counts are read as a measurement. _latest_per_key now collapses to one row per finding_key per run; make monitor gets the same treatment.

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 reports/ after the dedupe fix and seeing 4 insertions / 4 deletions with identical counts. Now sorted by finding key, with a test that renders twice and compares.

Verification

  • 279 passed (275 before this round, 250 on main), ruff clean.
  • reports/daily-2026-07-01.md re-rendered under stable ordering — counts unchanged (6 new, 0 cleared, 2 still open), only line order.
  • Verified byte-stable across two consecutive regenerations.

@rubinder
rubinder merged commit eb34082 into main Aug 31, 2026
1 check passed
@rubinder
rubinder deleted the feat/daily-report branch August 31, 2026 20:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Daily platform report: read ops.monitor_results back as a narrative, with day-over-day diffs

1 participant