Skip to content

Commit 998e1c3

Browse files
rubinderclaude
andcommitted
fix: review round 1 -- re-running the agent doubled findings; report 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
1 parent dac474b commit 998e1c3

4 files changed

Lines changed: 116 additions & 9 deletions

File tree

docs/ai-sdlc/decisions/0007-finding-history-in-iceberg-not-in-the-incident-files.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,27 @@ have to agree, or "first seen" describes something different from the file on
9191
disk. The filename also stopped rendering `None` for breaches that carry no
9292
column.
9393

94+
## Review round: two defects the tests did not reach
95+
96+
**1. Running `make agent` twice in one day doubled the findings.**
97+
`ops.finding_log` is append-only, and re-running the agent on the same date is
98+
an entirely ordinary thing for a human to do. The report counted *rows*, not
99+
findings, so a second run turned one open finding into "2 still open". A
100+
day-over-day diff whose counts are wrong is worse than no diff at all, because
101+
the counts are read as a measurement. `_latest_per_key` now collapses to one row
102+
per `finding_key` per run; the same applies to `make monitor`.
103+
104+
**2. The report was not byte-stable across regenerations.**
105+
The diff sections were built from set differences and rendered in set-iteration
106+
order, so simply regenerating a report reshuffled its lines. These are committed
107+
artifacts: an unstable render produces a git diff on every regeneration that has
108+
nothing to do with what changed, and a diff that is usually noise is a diff
109+
nobody reads. Now sorted by finding key, with a test that renders twice and
110+
compares.
111+
112+
Both were found by probing the shipped code, not by re-reading the diff. Neither
113+
would have failed a test that only asked "does the report render?"
114+
94115
## Scope note
95116

96117
`drift-demo --day N` was added because the report needed something to narrate.

reports/daily-2026-07-01.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,16 +84,16 @@ _Assembled from `ops.agent_runs`, `ops.finding_log`, `ops.monitor_results` and `
8484
Compared with `2026-06-30`: **6 new**, **0 cleared**, **2 still open**.
8585

8686
### New
87-
- `[breaking]` [silver.transactions_arrival_gap] 1 missing period(s): 2026-07-01
87+
- `[breaking]` [bronze_txn_row_count] 5 is 0% of trailing median 250000
8888
- `[additive]` column 'settlementDays' present in table but not declared in contract
8989
- `[breaking]` latest snapshot added 5 rows, below 50% of trailing median 250,000
90-
- `[breaking]` [silver.stock_prices_arrival_gap] 1 missing period(s): 2026-07-01
91-
- `[breaking]` [bronze_txn_row_count] 5 is 0% of trailing median 250000
9290
- `[breaking]` [gold.forecast_training_set_arrival_gap] 1 missing period(s): 2026-07-01
91+
- `[breaking]` [silver.stock_prices_arrival_gap] 1 missing period(s): 2026-07-01
92+
- `[breaking]` [silver.transactions_arrival_gap] 1 missing period(s): 2026-07-01
9393

9494
### Still open
95-
- `[additive]` column 'merchantCategoryCode' present in table but not declared in contract (open 2d (since 2026-06-29))
9695
- `[renaming]` column 'checkNumber' was renamed to 'check_reference' (field id 20); the contract still declares the old name (open 1d (since 2026-06-30))
96+
- `[additive]` column 'merchantCategoryCode' present in table but not declared in contract (open 2d (since 2026-06-29))
9797

9898
## Open incidents
9999

src/ops/report.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ def _as_date(value) -> date | None:
7171
return value.date() if hasattr(value, "date") else value
7272

7373

74+
def _latest_per_key(rows: list[dict]) -> list[dict]:
75+
"""One row per `finding_key`, last write wins.
76+
77+
`ops.finding_log` is append-only, so running `make agent` twice on the same
78+
date -- an entirely ordinary thing for a human to do -- writes the same
79+
logical finding twice. Counting rows instead of findings then reports "2
80+
still open" where one finding exists, and the diff's whole value is that
81+
its counts mean something.
82+
"""
83+
latest: dict[str, dict] = {}
84+
for row in rows:
85+
latest[row["finding_key"]] = row
86+
return list(latest.values())
87+
88+
7489
def load_snapshot(engine, as_of: date) -> ReportSnapshot:
7590
"""Select everything the report needs for `as_of`. The only engine call."""
7691
runs = [_as_date(r["run_at"]) for r in _rows(
@@ -84,8 +99,9 @@ def load_snapshot(engine, as_of: date) -> ReportSnapshot:
8499
for row in findings:
85100
row["run_at"] = _as_date(row["run_at"])
86101

87-
today = [f for f in findings if f["run_at"] == as_of]
88-
prior = [f for f in findings if f["run_at"] == previous] if previous else []
102+
today = _latest_per_key([f for f in findings if f["run_at"] == as_of])
103+
prior = _latest_per_key(
104+
[f for f in findings if f["run_at"] == previous]) if previous else []
89105

90106
# First time each key was ever seen, across the whole log.
91107
first_seen: dict[str, date] = {}
@@ -105,9 +121,13 @@ def load_snapshot(engine, as_of: date) -> ReportSnapshot:
105121
default=None)
106122
monitors_today = [m for m in monitor_rows if m["run_at"] == newest]
107123

124+
monitors_today = list({m["monitor"]: m for m in monitors_today}.values())
125+
108126
monitor_runs = sorted({m["run_at"] for m in monitor_rows})
109127
current_run = monitors_today[0]["run_at"] if monitors_today else None
110128
prior_runs = [r for r in monitor_runs if current_run and r < current_run]
129+
# Same dedupe reasoning as `_latest_per_key`: a second `make monitor` on
130+
# one date must not turn one metric into two.
111131
previous_monitors = {
112132
m["monitor"]: m["metric"]
113133
for m in monitor_rows if prior_runs and m["run_at"] == prior_runs[-1]
@@ -329,9 +349,16 @@ def _diff_section(snapshot: ReportSnapshot) -> list[str]:
329349

330350
today = {f["finding_key"]: f for f in snapshot.findings}
331351
prior = {f["finding_key"]: f for f in snapshot.previous_findings}
332-
new = [today[k] for k in today.keys() - prior.keys()]
333-
cleared = [prior[k] for k in prior.keys() - today.keys()]
334-
still = [today[k] for k in today.keys() & prior.keys()]
352+
# Sorted, not set-iteration order. These are committed artifacts: a report
353+
# whose lines shuffle between runs produces a git diff on every
354+
# regeneration that has nothing to do with what changed, and a diff that is
355+
# usually noise is a diff nobody reads.
356+
def _ordered(keys, source):
357+
return [source[k] for k in sorted(keys)]
358+
359+
new = _ordered(today.keys() - prior.keys(), today)
360+
cleared = _ordered(prior.keys() - today.keys(), prior)
361+
still = _ordered(today.keys() & prior.keys(), today)
335362

336363
lines.append(f"Compared with `{snapshot.previous_run}`: "
337364
f"**{len(new)} new**, **{len(cleared)} cleared**, "

tests/test_report.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,3 +304,62 @@ def test_findings_written_by_the_agent_are_keyed_consistently(engine):
304304
# and the incident slug is derived from the same key
305305
assert actions.incident_slug(item["finding"]).endswith(
306306
__import__("hashlib").sha256(key.encode()).hexdigest()[:8])
307+
308+
309+
def test_running_the_agent_twice_in_one_day_does_not_double_the_findings(engine):
310+
"""`ops.finding_log` is append-only and re-running `make agent` is
311+
ordinary. Counting rows instead of findings reported "2 still open" for a
312+
single finding -- and a diff whose counts are wrong is worse than no diff,
313+
because it is read as a measurement."""
314+
maintenance.evolve_day(engine, 1)
315+
graph.run(engine=engine, dry_run=True, as_of=D2)
316+
graph.run(engine=engine, dry_run=True, as_of=D2) # a human re-runs it
317+
318+
snapshot = report.load_snapshot(engine, D2)
319+
keys = [f["finding_key"] for f in snapshot.findings]
320+
assert len(keys) == len(set(keys)), f"duplicate findings in one run: {keys}"
321+
322+
graph.run(engine=engine, dry_run=True, as_of=D3)
323+
text = report.build_report(report.load_snapshot(engine, D3))
324+
assert "**0 new**, **0 cleared**, **1 still open**" in text
325+
326+
327+
def test_running_monitors_twice_in_one_day_does_not_double_the_rows(engine):
328+
runner.persist(engine, runner.run_monitors(engine, D3))
329+
runner.persist(engine, runner.run_monitors(engine, D3))
330+
graph.run(engine=engine, dry_run=True, as_of=D3)
331+
332+
snapshot = report.load_snapshot(engine, D3)
333+
names = [m["monitor"] for m in snapshot.monitors]
334+
assert len(names) == len(set(names)), "monitor rows duplicated for one run"
335+
336+
337+
def test_a_pipe_in_a_detail_string_does_not_break_the_table():
338+
"""Detail text is machine-generated but carries column names and values;
339+
an unescaped `|` silently splits a Markdown table cell."""
340+
text = report.build_report(_snapshot(findings=[
341+
_finding("k", D3, kind="staleness", detail="a | b | c")]))
342+
row = next(ln for ln in text.splitlines()
343+
if ln.startswith("| `breaking`") and "b" in ln)
344+
assert "\\|" in row, "the pipe in the detail text was not escaped"
345+
# Escaped pipes still contain a `|` character, so strip them before
346+
# counting the structural ones: four cells means five separators.
347+
assert row.replace("\\|", "").count("|") == 5, (
348+
f"pipe leaked into the table structure: {row}")
349+
350+
351+
def test_the_report_is_byte_stable_across_regeneration(engine):
352+
"""Reports are committed artifacts. Set-iteration order made the diff
353+
sections shuffle between runs, producing a git diff on every regeneration
354+
that had nothing to do with what changed."""
355+
maintenance.evolve_day(engine, 1)
356+
graph.run(engine=engine, dry_run=True, as_of=D2)
357+
maintenance.evolve_day(engine, 4)
358+
graph.run(engine=engine, dry_run=True, as_of=D3)
359+
360+
first = report.build_report(report.load_snapshot(engine, D3))
361+
second = report.build_report(report.load_snapshot(engine, D3))
362+
assert first == second
363+
364+
# and stable across a fresh load, not just a repeated render
365+
assert report.build_report(report.load_snapshot(engine, D3)) == first

0 commit comments

Comments
 (0)