publish when the live outage state moves, not only at 03:10 - #4
Conversation
The site is static and was rebuilt once a night, so a newly announced outage - or a revised restore estimate - could sit invisible for up to 24h. A visitor said exactly that on 2026-08-31: "Nu este actualizat la data de 31.08.2026". The data was already here; only the publish lagged. The scrape now dispatches Nightly publish when the LIVE state changes. Nightly keeps its 03:10 schedule for the historical rebuild; this only adds intraday publishes, and nightly's own `concurrency: nightly` group serialises overlaps. Gated on the canonical hash over page A's parsed records, not the raw bytes: - a revised `remediere_raw` DOES trigger, which matters because the restore time is the number people are actually asking for; - reordered rows and markup churn do NOT (verified: whitespace and attribute churn yields an identical hash); - the affected-street list is not in the key tuple, so a change only to which streets a PT lists will not publish. Accepted and documented. Page A lists only currently-active outages - records vanish on resolve - so a change to it is by definition a change to the live state. MEASURED, and it corrects the estimate this was designed against: across the last 12 snapshots the live hash changed on 11 of 11 transitions. The filter is therefore barely tighter than "any data change"; it earns its place as protection against cosmetic churn, not as a way to cut build count. Expect roughly 8 publishes/day, bounded by how often GitHub actually runs the schedule - measured at ~8/day despite the */15 cron, because GitHub throttles frequent schedules. Vercel Hobby runs one build at a time at ~6 min, so those serialise comfortably. The detector fails OPEN: any read, git or parse error reports changed=true. A broken detector should degrade to publishing too often, never to going silently stale - which is the failure this whole change exists to fix. Co-Authored-By: Claude Code <noreply@anthropic.com>
There was a problem hiding this comment.
Solid design (fail-open, hash on parsed records not raw bytes, running before the commit step) and the claims in the description check out against pipeline/parse.py — Record.key_tuple() does exclude streets and include remediere_raw, and nightly.yml does have workflow_dispatch + concurrency: group: nightly as described. One correctness bug blocks merge though.
EmptyState isn't distinguished from a real parse failure, so the detector fires on every scrape while the outage list is empty, not just on the empty↔non-empty transition.
pipeline/parse.py:179raisesEmptyStatewhen CMTEB renders the "Nu exista inregistrari" banner — a routine, recurring state, not an error.pipeline/backfill.py:65already special-cases it separately fromParseFailurefor exactly this reason.scripts/live_changed.py:78-84wraps bothlive_hash(previous)andlive_hash(current)in one broadexcept Exception, which also swallowsEmptyState. So whenever page A is empty either before or after (a common, routine state, not just a rare edge case), the call raises before any hash is computed, and the script falls into the "parse failed – failing open" branch and unconditionally emitschanged=true.- Net effect: during any calm period with zero active outages, every 15-minute scrape will dispatch
nightly.yml, not just the one transition into/out of that state. That directly contradicts the PR's own "~8 publishes/day" measurement and risks backing up the Vercel Hobby one-concurrent-build queue — the exact waste this design is meant to avoid. - Fix: catch
EmptyStateexplicitly (inlive_hashor around each call) and treat it as zero records, e.g.content_hash([]), so two empty scrapes hash equal. LeaveParseFailureand anything else on the current fail-open path.
Secondary, non-blocking: subprocess.run(..., check=True) at scripts/live_changed.py:59 only catches CalledProcessError; an unexpected OSError (e.g. git unavailable) would propagate uncaught, failing the "Did the live outage state change?" step itself. Since that step has no continue-on-error, the job would stop there and the Commit if changed step would be skipped entirely — the opposite of "fails open." Wrapping main() in a top-level try/except would make the fail-open guarantee actually total.
|
@claude please address this review:
|
Caught in review, and it defeated the throttle this PR exists to add. pipeline/parse.py:179 raises EmptyState for CMTEB's routine "nu exista inregistrari" banner; backfill.py:65 already handles it separately from ParseFailure for exactly this reason. live_hash() wrapped parsing in a broad `except Exception`, so EmptyState fell into the fail-open branch and the detector reported changed=true on EVERY scrape for as long as the city had no active outages - publishing constantly at precisely the times there was nothing to publish. Reproduced against a synthetic banner page before fixing: two consecutive empty scrapes reported changed=true; they now compare equal, while empty->outage and outage->empty both still trigger. No empty snapshot exists in the last 400 commits, so this had not bitten yet - it would have arrived quietly in summer. Second finding, also real and worse than a missed publish: this step runs BEFORE "Commit if changed", so an uncaught exception would fail the step, skip the commit and LOSE the scraped snapshot. Now belt and braces - main() catches everything, and the step carries continue-on-error so even a hard crash cannot stop the commit. With continue-on-error the step's outputs can be unset, so the publish condition is now `!= 'false'` rather than `== 'true'`: only an explicit "no change" suppresses a publish, keeping the fail-open intent intact when the detector itself dies. 4 regression tests pin all of it. 49 passed, 15 skipped. Co-Authored-By: Claude Code <noreply@anthropic.com>
|
Both findings were real and are fixed in 1. Reproduced before fixing, against a synthetic page carrying the exact banner
2. Uncaught exception in the detect step — confirmed, and worse than a missed publish. The step runs before That change makes the step's outputs possibly unset, so the publish condition moved from Added |
There was a problem hiding this comment.
Verified the fix for the prior blocking bug: live_hash (scripts/live_changed.py:56-70) now explicitly catches EmptyState and maps it to content_hash([]), matching the existing precedent in pipeline/backfill.py:65. Confirmed against pipeline/parse.py: parse_page raises EmptyState only for the routine empty-records banner (line 179), distinct from ParseFailure, so a real parse failure still falls through to the fail-open path as intended. New tests in tests/test_live_changed.py cover the empty/empty, empty/real, and markup-churn cases.
The secondary non-blocking suggestion (make the fail-open guarantee total) is also addressed: main() is now wrapped in a top-level except BaseException (scripts/live_changed.py:104-110) and the workflow step has continue-on-error: true (.github/workflows/scrape.yml:32), so even an OSError from a missing git binary degrades to changed=true rather than skipping the commit step.
Workflow logic checked: actions: write permission is the minimal scope needed for gh workflow run, the publish step is correctly gated on both committed=='true' and changed!='false', and no secrets/injection issues (subprocess uses list args, no shell=True).
Why
The site is static and was rebuilt once a night, so a newly announced outage, or a revised restore estimate, could sit invisible for up to 24h. A visitor said exactly that on 2026-08-31:
The data was already in this repo. Only the publish lagged.
What
The scrape dispatches
Nightly publishwhen the live state changes. Nightly keeps its own 03:10 schedule for the historical rebuild; this only adds intraday publishes, and nightly's existingconcurrency: nightlygroup serialises any overlap.The gate is the canonical hash over page A's parsed records (
pipeline.parse.content_hash), not the raw bytes:remediere_rawdoes trigger, which matters because the restore time is the number people are asking forPage A lists only currently-active outages (records vanish on resolve), so a change to it is by definition a change to the live state.
A measurement that corrects the plan this was built from
I proposed this as the "tighter" option, expecting it to cut publishes to a handful a day. It does not. Across the last 12 snapshots of page A the live hash changed on 11 of 11 transitions. The filter is barely tighter than "any data change".
It still earns its place as protection against cosmetic churn, but not as a way to reduce build count. Expect roughly 8 publishes/day — bounded not by this filter but by how often GitHub actually runs the schedule, measured at ~8/day despite the
*/15cron, because GitHub throttles frequent schedules on public repos.Cost: both repos are public, so Actions minutes are free. Vercel Hobby runs one build at a time at ~6 min each, so ~8/day serialise comfortably.
Failure mode
The detector fails open: any read, git or parse error reports
changed=true. A broken detector should degrade to publishing too often, never to going silently stale — which is the exact failure this change exists to fix.It runs before the commit step, because it diffs the working tree against
HEAD.Checks
45 passed, 15 skipped(the documented expected state without a localdb/termo.db). Script exercised against real history in this repo: correctly reportedchanged=falseon an unchanged tree, and detected all 11 real transitions.🤖 Generated with Claude Code