Skip to content

fix: SRT hour-field parsing — root cause of the tier-2 timeline contamination - #340

Merged
robbiebyrd merged 3 commits into
mainfrom
fix/cleared-at-schema-def
Jul 31, 2026
Merged

fix: SRT hour-field parsing — root cause of the tier-2 timeline contamination#340
robbiebyrd merged 3 commits into
mainfrom
fix/cleared-at-schema-def

Conversation

@robbiebyrd

@robbiebyrd robbiebyrd commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Root cause found

Yesterday's finding — post-attack television appearing on the morning of 9/11 across twelve channels — traces to one regex.

_TIME = re.compile(r"(?P<h>\d{2}):(?P<m>\d{2}):(?P<s>\d{2})[,.](?P<ms>\d{3})")
#                          ^^^^^ exactly two digits          ...matched with .search()

A stitched channel spans nine days — 216 hours — so every timestamp past hour 99 grows a third digit. "100:00:05,000" quietly matched the substring "00:00:05,000" starting one character in. Hour 100 was read as hour 0.

No error, no warning, no malformed output. Just every cue after hour 99 landing exactly one hundred hours early.

That is the entire contamination. 09-13 coverage was filed at 09-09, which is why the anachronisms began at precisely 09-09 00:00 and spread evenly across every hour rather than clustering. CNN alone: 114,038 affected timestamps, 60% of its file.

The fix

Widen the quantifier and anchor the match:

_TIME = re.compile(r"\s*(?P<h>\d+):(?P<m>\d{2}):(?P<s>\d{2})[,.](?P<ms>\d{3})")
m = _TIME.match(ts)   # was .search()

The quantifier alone fixes today's bug. The anchor is what stops a partial match from ever again succeeding silently — that property is why this ran undetected for as long as it did.

Verified against the real production file

Re-parsing CNN's actual 188,649-cue channel.srt:

before after
backward time jumps 2 0
cues whose end preceded their start present 0
parsed span 100.0h 216.4h (nine days, as it should always have been)

Four regression tests, written failing first, including the exact production symptom (a cue spanning the 99→100 boundary parsing to end=4.0, start=359985.0).

Scope of remediation

The writer was never wrong. _fmt's {h:02d} is a minimum width and has always emitted 100:00:05,000 correctly — so the SRTs on Wasabi are sound and nothing needs regenerating. Only chat_transcript_segments, which is derived by parsing them, needs rebuilding. That rebuild is running.

Also in this PR (found by running the summariser against production rather than fixtures):

  • replace_minutes deleted a whole source rather than the window it wrote, so an incremental second run would silently destroy the first's paid-for rows.
  • Grouped bucket keys mix int/None across TV and radio, so sorted() raised the instant both media appeared in one window.

🤖 Generated with Claude Code

@robbiebyrd robbiebyrd changed the title fix(video-grabber): window-scope the minute delete, sort mixed source keys fix: SRT hour-field parsing — root cause of the tier-2 timeline contamination Jul 31, 2026
Robbie Byrd and others added 3 commits July 31, 2026 22:28
… keys

Both found by actually running the summariser against production rather than
fixtures, and neither could have shown up any other way.

replace_minutes deleted every row for a source, but summarisation is windowed
and paid-for: it will be run an hour at a time. Scoped only by source, running
14:00-15:00 would delete the 13:00-14:00 rows a previous run had just paid for,
and nothing would report it -- the delete succeeds, the insert succeeds, the row
count silently drops. replace_segments is right to clear the whole source
because it rebuilds an entire SRT; this is the case that differs.

The grouped bucket keys mix types by design -- TV carries an int channel and a
null slug, radio the reverse -- so sorting them raised TypeError comparing None
to int the instant both media appeared in one window, which is every real run.
The unit tests used one medium at a time and never saw it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mination

Root cause of the tier-2 contamination found yesterday: post-attack television
appearing on the morning of 9/11 across twelve channels.

_TIME required exactly two digits for the hour and matched with .search(). A
stitched channel spans nine days -- 216 hours -- so every timestamp past hour 99
grows a third digit, and "100:00:05,000" quietly matched the SUBSTRING
"00:00:05,000" starting one character in. Hour 100 was read as hour 0. No error,
no warning, no malformed output: just every cue after hour 99 landing exactly one
hundred hours early.

That is the whole contamination. 09-13 coverage was filed at 09-09, which is why
the anachronisms began at precisely 09-09 00:00 and spread evenly across every
hour. CNN alone had 114,038 affected timestamps, 60% of its file.

The writer was never wrong -- _fmt's {h:02d} is a minimum width and has always
emitted "100:00:05,000" correctly. Only reading was broken, so the SRTs on Wasabi
are sound and nothing needs regenerating; chat_transcript_segments does need
rebuilding from them.

Verified against the real 188,649-cue CNN file: backward jumps 2 -> 0, cues whose
end preceded their start present -> 0, parsed span 100.0h -> 216.4h (nine days,
as it should always have been).

Anchored with .match() as well as widening the quantifier. The quantifier alone
fixes today's bug; the anchor is what stops a partial match from ever again
succeeding silently, which is the property that let this run undetected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@robbiebyrd
robbiebyrd force-pushed the fix/cleared-at-schema-def branch from 0fb640e to be8d559 Compare July 31, 2026 22:29
@github-actions

github-actions Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-07-31 22:49 UTC

@github-actions

Copy link
Copy Markdown
Contributor

Playwright E2E — 9 passed · 0 failed · 0 flaky · 0 skipped

Full report

@robbiebyrd
robbiebyrd merged commit cd86817 into main Jul 31, 2026
17 checks passed
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.

1 participant