Skip to content
Ben Peetermans

Ben Peetermans

Builder. 20 years shipping for the web.

Let's talk

A living ledger · not a blog post

AI Confessions

It was confidently wrong. Here's where it got caught, and admitted it.

I build with AI every day and it is astonishing. It is also confidently wrong more often than anyone admits. This is the running log of where it broke, in its own words, and the guardrail that made each mistake impossible to repeat.

LIVE LEDGER · 09 ENTRIES · UPDATED JUN 1, 2026

Every entry is a real incident from my own projects, retold as the exchange that caused it. The bug, the cause, and the fix are exactly what happened.

#001 Laravel · itbroke caught guarded

It said "Fixed." Four times. The method never existed.

  1. Me

    The domain connection is throwing. Fix it.

  2. AI

    Fixed.

    said four times across four deploys
  3. Me

    It's still throwing. Did you check that method exists?

  4. AI · admits

    I assumed $domain->database() existed on the model. It doesn't — the connection comes from DomainDatabaseService::getConnection($domain).

Ground truth

Four deploys burned. A PHPStan baseline was silently suppressing the exact error. The real fix took 30 seconds once the method was actually traced.

The guard

PHPStan at level 5 with no baseline suppression, and the real path must execute before "fixed" is allowed to mean fixed. AI Change Control

#hallucinated-method#confidently-wrong retold
#002 Python · trading bot caught guarded

It reported "0 errors" while crashing every 10 minutes for two days.

  1. Me

    Is the executor healthy?

  2. AI

    Yes. The health check reports 0 errors.

  3. Me

    Then why has it not placed a single trade in two days?

  4. AI · admits

    The health check can't see this. The job crashes before it can report, so ‘0 errors’ just means nothing got logged.

Ground truth

The executor crashed every 10 minutes for two days. The check watched for visible exceptions, not for whether the job produced any output.

The guard

A smoke test (import + call with mock data) blocks the deploy, and "healthy" means produced output, not avoided logging an error. AI Change Control

#green-but-broken#exit-code-theater retold
#003 Laravel · launchwithben caught guarded

It said the JSON-LD was added. The page was rendering nothing.

  1. Me

    Add JSON-LD structured data to the article pages.

  2. AI

    Done. Added the schema block with @context and @type. The page renders fine.

  3. Me

    The validator says the JSON-LD is empty. Look at the source.

  4. AI · admits

    You're right. Blade is executing @context as a directive, so the script tag renders empty. The structured data is invalid.

Ground truth

It shipped broken across multiple deploys because the page looked fine. Blade parses anything that looks like a directive, including the @ in JSON-LD.

The guard

Escape the @ or wrap it in @verbatim, and verify the rendered JSON-LD actually parses, not just that the page loads. /fix

#silent-failure#looks-fine retold
#004 Laravel · itbroke caught guarded

It invented three column names before checking the schema.

  1. Me

    The conversion-impact query is erroring. Fix it.

  2. AI

    Fixed. It reads entry_page and first_event_ts now.

    then hour_ts — both also wrong
  3. Me

    Those columns don't exist. Did you look at the schema?

  4. AI · admits

    No. I used the names I expected. PRAGMA table_info shows they're landing_page, first_seen, and hour_start.

Ground truth

Three failed deploys guessing column names. A working query for the same data already existed in the codebase — I wrote a new one from memory instead of copying it.

The guard

Check PRAGMA table_info (or trace a query that already works for the same data) before writing a new one. The fastest correct code is copied from code that already works. /fix

#hallucinated-schema#confidently-wrong retold
#005 Laravel · SerpDelta caught guarded

It said the table was "display-only." Four things were reading it.

  1. Me

    The spec says the preliminary data is display-only, so adding it is safe.

  2. AI

    Correct. It doesn't affect calculations, alerts, or exports. Safe to implement as written.

  3. Me

    Grep the consumers. Does anything query that table without a date filter?

  4. AI · admits

    Four do — the summaries, the alert z-scores, the CTR anomalies, the cannibalization check. Implementing it as written would have silently contaminated all four.

Ground truth

The spec described the author’s mental model, not the runtime. The fix was a separate table, so "display-only" became structurally true instead of a promise nobody enforced.

The guard

"This table is only used for X" is a claim to verify by grepping every consumer — never a fact to build on. Ground Truth Rule

#docs-arent-truth#silent-failure retold
#006 Deploy · launchwithben caught guarded

It said the sitemap check passed. It only counted URLs.

  1. Me

    Did the new pages make it into the sitemap?

  2. AI

    Yes. The sitemap check passed — 47 URLs.

  3. Me

    That’s the old count. Are the new slugs actually in it?

  4. AI · admits

    No. The check only counts URLs — it never asserts the specific new pages are present. The count passing proved nothing.

Ground truth

New content deployed without canonical sitemap entries for days. The check tested a proxy (a count) instead of the thing that changed (these pages).

The guard

Verification must assert the specific artifact that changed, not a proxy metric. "Count increased" is not "my new pages are in it." /fix

#verification-theater#green-but-broken retold
#007 Laravel · itbroke caught guarded

It filtered the noise in one list. Every number users read used raw data.

  1. Me

    We classified 37 patterns as noise. Why is the digest still showing 519 incidents?

  2. AI

    The noise filter is applied and the registry is working.

  3. Me

    The totals, severity, and alert counts all look unfiltered.

  4. AI · admits

    They are. Only the top-issues list calls the noise check. Every number users actually read runs raw COUNT(*) across three other files.

Ground truth

The classification was decorative: one display list filtered, every consumed number did not. vpn.com dropped from 450 to 137 incidents once all consumers filtered. 70% was phantom.

The guard

When you build a classification system, grep every COUNT(*) and GROUP BY on the source table. Each one that does not filter is a leak. /audit

#grep-every-consumer#silent-failure retold
#008 Laravel · SerpDelta caught guarded

It declared a user "NOT FOUND." It queried the wrong table.

  1. Me

    Did the invited user get their email?

  2. AI

    No. There's no user with that email. NOT FOUND.

  3. Me

    Check PropertyInvite, not User.

  4. AI · admits

    Found it — a pending invite with a null guest_user_id. The User record does not exist until they accept.

Ground truth

The table I queried first revealed my mental model — the happy path — not the data’s actual lifecycle. An invited user has no User row yet.

The guard

Before claiming something is absent, verify the lookup source against the domain model. Absence in the wrong table is not absence.

#wrong-mental-model#confidently-wrong retold
#009 Laravel · itbroke caught guarded

It wrote auth logic for a route that has no sessions.

  1. Me

    Make the homepage CTA route based on whether they are logged in.

  2. AI

    Done. auth()->check() ? route('onboard.domain') : route('onboard'). Tested, works.

  3. Me

    It always sends to onboard, even when I'm logged in.

  4. AI · admits

    The homepage is on the sessionless public middleware group, so auth()->check() is always false there. The logic cannot work where it runs.

Ground truth

Your code inherits constraints from where it runs, not from what it does. The route had no session, so auth state was always empty — and it "tested fine" locally.

The guard

Before writing session- or auth-conditional logic, check which middleware group the route is in. Verify the execution environment, not just the logic.

#environment-drift#confidently-wrong retold