takahiro hashitoIntroduction I run a site that collects overseas viewer comments about individual anime...
I run a site that collects overseas viewer comments about individual anime episodes, translates them, and publishes them. It updates automatically every day.
There is one failure mode that matters more than all the others: creating a page for an episode that has not aired yet.
If the page exists before the broadcast, there are no comments to put on it. But the heading "Episode 8 — overseas reactions" is already live. An empty page is recoverable. What is not recoverable is a pipeline that decides an empty page looks bad and fills it with something plausible. At that point invented sentences are wearing the face of real people.
This post is about the check that prevents that, and about the day it actually fired.
The obvious implementation is arithmetic on dates:
That works for producing candidates, but it is not evidence that anything aired. A skipped week makes the real count lower. So does a recap episode. Calendar arithmetic never observes the broadcast; it only restates an assumption.
So I split the pipeline in two:
For the existence check I use the per-episode discussion threads on MyAnimeList (a large anime database; MAL from here on). One thread is created per episode after it airs, and the timestamp of the first post in that thread is readable.
Viewers post after watching. So that timestamp is external evidence that the broadcast happened. Better still, it lives in exactly the same place I fetch the comments from, so verification costs no additional data source.
The check is a subtraction between what I claim and what the outside world recorded.
/**
* @param {string} expectedAired the air date my data claims (YYYY-MM-DD)
* @param {string} firstPostedAt first post in MAL's thread for that episode (ISO 8601, UTC)
*/
function checkAirDate(expectedAired, firstPostedAt, limitDays = 5) {
const expected = Date.parse(expectedAired + 'T00:00:00Z');
const observed = Date.parse(firstPostedAt);
const gapDays = Math.round((observed - expected) / 86400000);
return {
expectedAired,
firstPostedAt,
gapDays,
limitDays,
status: Math.abs(gapDays) <= limitDays ? 'ok' : 'mismatch',
};
}
Running it on real values:
checkAirDate('2026-07-28', '2026-07-28T14:25:51.000Z');
// => { gapDays: 1, status: 'ok', ... }
checkAirDate('2026-08-23', '2026-07-28T14:25:51.000Z');
// => { gapDays: -25, status: 'mismatch', ... }
The arithmetic is trivial. What does the work is the asymmetry: the left operand is my claim, the right operand is an outside observation. Comparing two values from the same side of that line verifies nothing.
The five-day tolerance absorbs time zones and streaming delays. Japanese late-night slots shift the local date, and overseas releases can lag by days. Demanding an exact match would reject correct episodes.
Finally the per-episode results are aggregated:
const conclusive = episodes.every((ep) => ep.airDateCheck.status === 'ok');
if (!conclusive) return; // don't write any fetched comments
Every episode must pass, or nothing is written. Partial acceptance is wrong here: if some episodes disagree, the air-date data was produced incorrectly, which means the ones that appear to agree may only be agreeing by accident.
The gate fired today, on my own code.
Episodes 4 through 7 were missing, so I created four page stubs and went to fetch comments:
"conclusive": false,
"mismatched": [
"ep 4: first post 2026-07-28T14:25:51Z is -25 days from claimed air date 2026-08-23",
"ep 5: first post 2026-08-04T14:26:23Z is -18 days from claimed air date 2026-08-23",
"ep 6: first post 2026-08-11T14:24:02Z is -11 days from claimed air date 2026-08-23"
]
All four stubs claimed the same air date: today. The stub generator was defaulting the air date to the current date instead of leaving it unset.
This is the part worth keeping. That bug is invisible by inspection. 2026-08-23 is a well-formed date and a perfectly possible value; nothing about the record looks wrong. It only becomes wrong next to an outside observation, and then it becomes wrong with a number attached: minus twenty-five days.
After filling in the real air dates and re-running:
"conclusive": true,
"mismatched": [],
"totals": { "titles": 1, "episodes": 4, "reactions": 32 }
Thirty-two comments from the same threads, against eight while the run was failing — a failed run stops fetching early.
One more thing. The same check also prevents under-collection. The weekly arithmetic proposed episode 8 as a candidate, but MAL has no thread for episode 8 yet; the last observable one is episode 7. Arithmetic said 8, observation said 7. Observation wins.
Here is the site this runs on: https://anime.autoarticles.net
Each episode page carries the poster's handle, a per-post URL, and the original English text alongside the translation. A translation on its own gives the reader no way to confirm that the quote is real. Making it confirmable is part of the same design decision.
Automated pipelines don't emit convincing falsehoods because the generator is too clever. They emit them because nothing in the system is able to say that a precondition stopped holding.
The stub generator that defaulted the air date to today raised no error at all. The value was present and well-formed. What stopped it was a single component comparing that value against a record from outside the system.
So put the gate before generation, not after it. Auditing generated prose for plausibility is a losing game, because plausible prose is cheap to produce. Verifying that you were allowed to generate — against a fact you did not author — is cheap and decisive.
And when you can't confirm the precondition, fail loudly rather than quietly shipping zero. "Nothing today" and "I could not check today" are different messages to the person reading the logs tomorrow, and that person is usually you.
This article is about my own side project. It was written with AI assistance.