Shipping & Infra5 min read

The App I Added by Hand Disappeared — And the Count Stayed the Same

I hand-filled a missing entry into a canonical list of 46 apps. Two days later I regenerated that file and the entry was gone, replaced by a different app. Still 46. A check that counts will pass a swap like this forever.

#shipping-infra#automation-pipeline#gotchas#reality-check#seo
Left: an entry hand-added to a generated file is silently replaced at the next regeneration. Right: the count is 46 before and after the swap, so a count check cannot catch it
One left, one arrived. The total never moved.

I run more than forty small web apps. They link to each other through recommendation cards, and those cards read one canonical list file. The hub site, the portal, and every app's recommendation widget all read that single file.

Two days ago I noticed an app missing from it. Live on the web, absent from the list. So I typed the entry in and committed:

chore: track the canonical app list + add birthday (45 -> 46 apps)

Today, while wiring up a newer app, I regenerated that file. Then I diffed the result against the old one:

$ diff <(keys old) <(keys new)
8d7
< birthday
25a25
> gyeonjeok

One left, one arrived. Still 46.

That file was never mine to edit

The README says it on line three:

Regenerate: python3 ootssu-apps.manifest.build.py (the hub cards are the canonical data source; rerun after updating the hub to resync)

The file is an artifact, not a source. The real source is the card HTML on the hub site; a script scrapes it and emits this JSON. What I edited two days ago was the output end.

Which means that fix only ever had a lifespan of one regeneration. The commit message said "add," but nothing was added. It was version-controlled, it passed review, and it was gone two days later.

One question for you. That file you just fixed — who writes it?

The scary part isn't the deletion, it's the silence

I only caught it by accident: I happened to diff before and after while doing something else.

Suppose I hadn't. The hub, the portal, and forty apps' recommendation cards simply never show that app again. The app is alive, the site returns 200, no bot fails. It just becomes an app that appears on no list. That's the same flavor of quiet as the review monitor that watched 21 of 44 apps.

And a count check is no help here:

before   46 apps
after    46 apps

A -1 and a +1 don't move a total. A guard like "tell me if apps go down" will pass this swap forever. Don't compare the size; compare the set.

The fix: put it in the source, not the artifact

Which would you pick?

  1. Teach the generator to preserve hand-added entries. Keep an exceptions list and merge it in on every rebuild.
  2. Add the app to the hub cards — the real source — and just regenerate the artifact.

Option 1 is tempting. Ten lines, and the entries I already typed survive. But at that moment the file gains two sources: hub cards and an exceptions list. The next person won't know which one to edit. I have already paid for a version that lived in three places.

I took option 2: one card each into the ko/en/ja hub homes and the matching category pages, then regenerate. 47 apps, both present.

Opening the source turned up three more

Working from the canonical end showed things the artifact could never show.

One. An app had a card in the en, ja and zh locales but was missing from the structured data (ItemList) in all three. 34 cards, 33 ListItems. I have no idea how long it was visible to humans and absent from the list search engines read.

Two. The generator keeps a per-app locale table, and apps missing from the table were quietly falling back to ["ko"]. It did print a warning:

locale unspecified (defaulting to ko): ['birthday', 'gyeonjeok', 'bungeo', 'hwaksin', 'plotta']

Two of those five really do support several locales. I checked all five by hand — HTTP status plus <html lang>. Defaults are dangerous precisely because they're sometimes right: on the days they're right the warning looks like noise, and on the day they're wrong nobody is reading it.

Three. The artifact carries a generated field, hardcoded as a string:

"generated": "2026-08-21",

Regenerate as often as you like; that date never moves. A field whose only job is to answer "when was this built" had been giving the same answer for three weeks. And my hand-edit two days ago had bumped that value manually — which is why the copies disagreed on their dates and nobody found it odd.

Three things to check on your own code

  • Does the file you just edited have a "generated by" comment at the top? If not — are you sure it doesn't?
  • Does the guard protecting that list watch the count, or the set?
  • Does your pipeline print any "fell back to a default" line? When did you last verify that default was right?

Conclusion

The commit two days ago wasn't wrong code. It was the wrong file. Edit a derived artifact and your fix lives until the next build — and that death is quiet. The build passes, the tests pass, the count is unchanged.

The real lesson: if you want to stop things from vanishing off a list, don't guard the total. Guard the names.

Do one thing right now: pick a file you knowingly treat as generated, run its generator once, and diff the result against what's on disk. If nothing comes back, that cost you three seconds.

Related