Already there meant never arrived

8–12 minutes
measured on my own machine
Already there meant never arrived

My publishing chain told me a corrected image was already there. In one sense it was: a file with that name existed. The picture inside it was the old one. A few hours later the same chain told me a corrected post was already standing on the blog. It was the same answer and the same mistake. The corrected version never arrived. Both skips had been written on purpose, to stop duplicates. I found both on the same day, and it took 3 commits to close them.

Some context first. The chain builds a post on its own. After 48 hours with no veto from me, it puts the post live without a click. When this happened, no post had made it all the way through yet. The first one was announced for 14.09. What I wanted was simple. When I fix something, the fix should land where a reader will see it, and not just in a local file or in a report that says done.

I believed that a chain reporting ‘already there’ was describing the current state. Already there meant that what I had just built was on the site. That reading felt so obvious that I never questioned it. When the image script said done and the picture still looked wrong, my first move was to believe the message and blame a cache. A cache was a reasonable suspect. It would explain an old picture. It would also mean the upload had worked. That was the part I had wrong.

The chain compared existence, not content. The upload function looks for a media item with the same slug. If it finds one, it hands that item back unchanged. A file with the same name counted as the same file, so the old one stayed put. One level up, the draft step did the same thing: a post with that slug was already standing, so it skipped. Both checks answered the question ‘is there something called this?’ Neither asked ‘is it what I just built?’

304d9eda · 2026-09-12

Ein korrigiertes Bild erreichte die Site nie — „schon da” hieß: die alte Datei bleibt

A corrected image never reached the site. ‘Already there’ meant: the old file stays.

The image fix took more than a re-upload. The blog platform cannot swap the bytes of an existing attachment. The only way is to delete it and create it again, and that changes the media id. So replacing is opt-in. A caller that does not pass the new id along would leave the featured image pointing at a deleted file. My chain passes it on. Other paths stay untouched, and a test confirms that too.

The new check compares content, not dates. Earlier that day a deploy with rsync had fooled me the same way: the time and permission flags reported a change that was not one. And ‘could not fetch’ is not the same as ‘different’. If the script cannot compare, it deletes nothing. That last line matters most. A delete based on a guess is the one mistake here that cannot be undone.

7829913d · 2026-09-12

„steht schon” hieß: ein korrigierter Artikel erreicht die Site nie

‘Already standing’ meant: a corrected post never reaches the site.

The post fix needed a decision before any code. My rule is this. The chain may overwrite a draft it owns. It may not overwrite a published post, and it may not overwrite a draft I edited by hand, because that work lives in no file. The obvious test was to compare the site against the local file. That fails, because what comes back is never what went in: the platform rewrites text. So the site is compared with itself, against a fingerprint the chain took right after its last write.

No fingerprint means no verdict, and with no verdict nothing gets written. ‘Cannot tell’ is not ‘unchanged’. That exact mix-up nearly let a capacity limit through that same day, when a stock check run from the sandbox returned null. The verdict now lives in one pure module with no network access. It is tested on all six outcomes, including the dangerous direction. First placement and renewal now share one derivation. Without that, the renewal would have lost the image id.

What happened, in order

  1. 2026-09-12The script reported done for a repaired image, but the media library still held the old one. I first trusted the report and suspected a cache. The real cause was the upload step returning any existing file with the same slug. I made replacement opt-in and switched the comparison to content.
  2. 2026-09-12I found the same skip one level higher, in the draft step. I wrote a pure verdict function: it overwrites only drafts the chain owns that still match the fingerprint from its own last write.
  3. 2026-09-12On renewal the script printed a missing-image warning that was not true. I checked it on post #489: title image 507 was there before and after. So I changed the message for that case.
  4. 2026-09-12The handover for that session listed nine bugs that no test had caught, all found in real runs, in an image someone looked at, or in a production measurement. One example: the step strip printed a commit hash matched by list position, with 7 steps on the sheet against 4 in the article.
  5. 2026-09-12The next handover first logged the post skip as open, because the fix depended on my decision about what may be overwritten. A later note in the same file marks it as built and checked on the live system.
Every step in the order it happened, from the first upload to the last handover of the day.
Every step in the order it happened, from the first upload to the last handover of the day.

The record quotes exactly one command: git status –porcelain — loops/ growth/. It comes from a neighbouring session that same day, which was waiting for a clean tree before a deploy. It has nothing to do with this fix. Test files changed in both fixes, so node –test was most likely the runner. That is inferred from the changed files, not quoted. The record does not say how long any of this took.

34ab848c · 2026-09-12

Eine Warnung, die bei gutem Ausgang „kaputt” ruft, wird beim nächsten Mal überlesen

A warning that cries ‘broken’ on a good outcome gets skimmed past the next time.

This one was small, and I almost left it alone. On renewal, the script printed a no-image warning that said the post would not be announced. That is true for a first placement. It is false for a renewal, because the draft writer leaves the featured image alone when no id comes along. On post #489, title image 507 was there before and after. I split the message by case. A warning that fires on a good outcome teaches you to stop reading warnings.

The three commit subjects, in the original German.
The three commit subjects, in the original German.

Solved, for these two paths. Both now check content instead of existence, and neither deletes nor overwrites when it cannot compare. The record does not show a full run since then. At the time, the first post built by the chain was still waiting for 14.09, and the image check had never been seen passing on a live post. The fix leaves published posts and hand-edited drafts alone. That is on purpose, not a gap.

What the record supports, and what it leaves open.
What the record supports, and what it leaves open.

Three commits, all from September the twelfth, 2026, plus the handovers written that day. The cost record for that window covers every run, not just this topic, so read it as an upper bound.

304d9eda · 7829913d · 34ab848c

Georg Singer

Georg Singer

I automate my own projects in public and write down what each one cost. Physics degree, doctorate in computer science, nine systems in production on hardware I own, all of it built in my own time.

More from the log