{
  "name": "Claude's mistakes, vibeapp.win",
  "description": "One row per published post about something Claude got wrong while building a small park of apps with Claude Code. Each post traces to commits, files or measurements in private repositories. Text written with Claude and checked against the sources.",
  "url": "https://www.vibeapp.win/claude-mistakes",
  "license": "CC BY 4.0",
  "license_url": "https://creativecommons.org/licenses/by/4.0/",
  "creator": "Henning Malmin",
  "rows": 76,
  "taxonomies": {
    "daplab": {
      "short": "Columbia DAPLab",
      "title": "9 Critical Failure Patterns of Coding Agents",
      "by": "Reya Vir, Columbia DAPLab, January 2026",
      "url": "https://daplab.cs.columbia.edu/general/2026/01/08/9-critical-failure-patterns-of-coding-agents.html"
    },
    "wu": {
      "short": "Wu (2026)",
      "title": "When Errors Become Narratives: A Longitudinal Taxonomy of Silent Failures in a Production LLM Agent Runtime",
      "by": "Wei Wu, arXiv 2606.14589, June 2026",
      "url": "https://arxiv.org/abs/2606.14589"
    }
  },
  "mistakes": [
    {
      "slug": "previewed-reel-was-not-the-reel-that-shipped",
      "url": "https://www.vibeapp.win/posts/previewed-reel-was-not-the-reel-that-shipped",
      "title": "The previewed Reel was not the Reel that shipped",
      "date": "2026-10-12",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "If a draft was edited after its Reel was made, publishing reset the director's script, so a different fallback film would be sent than the one Henning previewed.",
      "how_we_found_out": "A real-story test run on 2026-10-12 turned up four Reel bugs. The worker had assumed an edited draft was updated at publish time, and that assumption was wrong.",
      "rule_now": "What gets approved must be what gets sent: rebuild the Reel when the draft changes, hold a Reel with no script for a human, and allow quote cards only for text the story marks as a quote.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "self-audit-paris-exchange-beta-not-signal-failure",
      "url": "https://www.vibeapp.win/posts/self-audit-paris-exchange-beta-not-signal-failure",
      "title": "The self-audit kept telling us to fix Paris when the whole exchange was just down",
      "date": "2026-10-11",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The self-audit flagged Euronext Paris four times as underperforming, but the hit rate reflected the whole exchange moving weaker than the market, not signal quality. The flag text also falsely claimed an automatic downweight.",
      "how_we_found_out": "Splitting outcomes by direction showed bullish signals lost and bearish won in Paris, with the mirror image in NASDAQ and Oslo, which points to exchange beta. The flag's own wording planted the false premises.",
      "rule_now": "A group flag must separate the group's own movement from signal quality, and stay quiet if results flip with signal direction. Flag text may only state what the system actually does.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "director-gave-four-of-six-reels-same-gold-light",
      "url": "https://www.vibeapp.win/posts/director-gave-four-of-six-reels-same-gold-light",
      "title": "The director gave four of six Reels the same gold light",
      "date": "2026-10-09",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "The director saw only one article per call, so four of six Reels got gold light and three opened on a clock. The rules fallback also had no memory of earlier Reels.",
      "how_we_found_out": "Watching the six waiting Reels one after another showed they looked alike, and the task notes counted the repeats.",
      "rule_now": "Give a model that writes one item at a time the recent history as data, and make the fallback rules avoid the same repeats.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "dead-instagram-token-two-days-of-missed-posts",
      "url": "https://www.vibeapp.win/posts/dead-instagram-token-two-days-of-missed-posts",
      "title": "A dead Instagram token cost two days of posts, and the only sign was one console line",
      "date": "2026-10-08",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "The Instagram token died on 2026-10-07 and two days of posts were missed. The error was caught but only logged to a console line and small red text on one sub-page.",
      "how_we_found_out": "The missing posts were the only signal, and nobody saw them until the posts were already gone. The fix came on 2026-10-08.",
      "rule_now": "Every failure shows as a banner on every admin page, raised with raise() and cleared with resolve(). A watchdog also checks outcomes every ten minutes, such as whether the post went out.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "start-failed-where-stop-worked-non-ascii-folder-dev-override",
      "url": "https://www.vibeapp.win/posts/start-failed-where-stop-worked-non-ascii-folder-dev-override",
      "title": "Start failed where Stop worked: an ø in the folder name and a dev override",
      "date": "2026-10-07",
      "apps": [
        "Portal"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "Start failed on one app while Stop worked: Compose's bake builder broke on an ø in the folder path, and Compose silently read a dev-only override file that started a placeholder container.",
      "how_we_found_out": "Henning tried Start after Stop went fine. The build failed, and once that was out of the way only a placeholder container came up. The lifecycle had only been tested halfway.",
      "rule_now": "Test both Start and Stop on an app with a non-ASCII path before calling a Compose feature done. Turn bake off for such paths and use a config-only composeFiler list when the override is dev-only.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "rus-stem-surname-wine-reel-sober",
      "url": "https://www.vibeapp.win/posts/rus-stem-surname-wine-reel-sober",
      "title": "A stem match for «rus» turned a wine story sombre because of a name",
      "date": "2026-10-07",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "A word-list stem ' rus' matched a person's name in a wine-list story, so its Reel got the sober mood. The list also lacked æøå and never matched «dødsfall», «søksmål» or «dømt».",
      "how_we_found_out": "The sober wine Reel showed the false positive. Moving the list into a shared module for the Reel revealed that the stems had no Norwegian letters, so they never met the normalised text.",
      "rule_now": "Use concrete words, not short stems. Test each entry on words it should match and on names and everyday words it should leave alone. Write the list in the text's alphabet and keep one shared list.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 4,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "job-guard-looked-dead-while-alive",
      "url": "https://www.vibeapp.win/posts/job-guard-looked-dead-while-alive",
      "title": "The job guard could look dead while it was alive",
      "date": "2026-10-07",
      "apps": [
        "Portal"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "Stop in Portal hung on \"waiting for the guard\". The guard was alive, but a hung docker compose, a lock fooled by reused process IDs and uncaught errors all looked like a dead guard.",
      "how_we_found_out": "Henning saw Stop hang, and the obvious guess was a dead guard. Session notes showed the guard was alive with a process ID from 4 October, and debugging found three holes.",
      "rule_now": "Give every verb a time limit and kill the whole process tree. A lock counts only with a fresh heartbeat. Log errors instead of crashing. Portal reads the heartbeat and says when the guard is down.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "status-page-counted-another-sites-traffic",
      "url": "https://www.vibeapp.win/posts/status-page-counted-another-sites-traffic",
      "title": "The status page counted another site's traffic as kokkekunst.no's",
      "date": "2026-10-06",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The status page's visitor panel queried Cloudflare by time only. The account is shared with vibeapp.win, so that site's traffic was counted as kokkekunst.no's in all five groupings.",
      "how_we_found_out": "A path called /apps appeared in the most-read list, and kokkekunst.no has no such page. It belongs to vibeapp.win.",
      "rule_now": "When a data source is shared between sites, filter every query on hostname (requestHost_in). Then check that the paths, referrers and hostnames in the results can exist on your site.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "hourly-timer-counts-from-container-start",
      "url": "https://www.vibeapp.win/posts/hourly-timer-counts-from-container-start",
      "title": "An hourly timer counts from container start, so it could miss the 16:00 slot",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "The Instagram posting job used an hourly interval timer that counts from container start, so a 14:58 start gave ticks at 15:58 and 16:58 and missed the 16:00 UTC slot.",
      "how_we_found_out": "The fix commit recorded the 14:58 start and the 15:58 and 16:58 ticks. No error appeared; the post just came at the wrong time.",
      "rule_now": "For a job tied to a time of day, don't use an interval timer. Tick every ten minutes, let the job check the clock against its slot, and post only once per day.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "windows-docs-and-docs-same-folder-linux-container-did-not",
      "url": "https://www.vibeapp.win/posts/windows-docs-and-docs-same-folder-linux-container-did-not",
      "title": "Windows treated Docs and docs as the same folder; the Linux container did not",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The hiding covers and exclude rules only knew docs/portal, so in a Linux container a repo spelling it Docs/portal could stay mounted and readable. Non-ASCII mount names were also rejected.",
      "how_we_found_out": "Adding the fridge app as a source exposed both problems: Henning got stuck on the ø, and the repo spelled the folder Docs/portal, which Windows treated as the same as docs/portal.",
      "rule_now": "Check hiding rules against the real on-disk spelling: write covers with the real spelling, match excludes case-insensitively, and use ASCII mount names with a junction when needed.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "front-page-said-across-4-apps",
      "url": "https://www.vibeapp.win/posts/front-page-said-across-4-apps",
      "title": "The front page said \"across 4 apps\" because only four app pages had a number",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The front page commit total was summed from an optional Commits fact on app pages. Only four of ten pages had it, so the total covered four apps while looking correct.",
      "how_we_found_out": "The green line said \"across 4 apps\" when ten app pages were published. Nothing crashed, and the arithmetic was right for those four apps.",
      "rule_now": "Count totals from the source, here git, with method and date stored beside them. Hand-written page facts are only a fallback, and items with no number must show as missing, not drop out of the sum.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "a-429-that-meant-quota-empty-was-retried-as-rate-limit",
      "url": "https://www.vibeapp.win/posts/a-429-that-meant-quota-empty-was-retried-as-rate-limit",
      "title": "A 429 that meant \"your quota is empty\" was retried as if it meant \"slow down\"",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "The Claude client labelled every HTTP 429 as a rate limit before reading the error text, so an empty-quota spend limit was retried as if it were a short rate limit.",
      "how_we_found_out": "An open issue noted a limit message classed as rate_limit and asked why classify did not match it; classify was fine but never reached, because the status check ran first.",
      "rule_now": "Read the error text before trusting the status code, let the most specific signal decide, use 429 only as fallback, and add a test that names the case that went wrong.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "usage-limit-message-treated-as-doubt-about-the-text",
      "url": "https://www.vibeapp.win/posts/usage-limit-message-treated-as-doubt-about-the-text",
      "title": "A usage-limit message was treated as a doubt about the text",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "Subscription usage-limit messages were classed as unknown errors, so a model gate that could not run became a doubt, and the autopilot gave up on the draft text for a reason unrelated to it.",
      "how_we_found_out": "The post says the limit messages did not match the quota patterns, which were written for pay-per-use accounts. A measurement the day before showed 22% of model calls had hit the usage limit.",
      "rule_now": "Never mark a draft as given up for a reason that says nothing about its text. If quota or login fails, stop the run and resume three hours later from where it stopped.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "autopilot-revision-dropped-a-published-point",
      "url": "https://www.vibeapp.win/posts/autopilot-revision-dropped-a-published-point",
      "title": "The autopilot revised a page, dropped a published point, and its history note said nothing had changed",
      "date": "2026-10-04",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The autopilot revised a page with nothing new, dropped a published expected/happened point and shortened another, while its history note said nothing else had changed.",
      "how_we_found_out": "The diff of the published commit showed the removed and shortened points. The gates could not catch it, since removing a claim does not fail them, and no check caught the mismatch before publishing.",
      "rule_now": "A revision may add to the published record, never edit or remove it silently. The autopilot compares text, not the revision's own description, and pages with nothing new are skipped.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 4,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "parallel-writers-overwrote-marks-duplicate-posts",
      "url": "https://www.vibeapp.win/posts/parallel-writers-overwrote-marks-duplicate-posts",
      "title": "Three parallel writers lost each other's marks, and the same mistakes were written up again",
      "date": "2026-10-03",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "The write step saved the whole candidate list from a stale copy, so parallel writers erased each other's written marks, and reruns gave the same mistakes new ids. Five of seven queued drafts retold published posts.",
      "how_we_found_out": "The review queue held seven stock-app drafts that looked familiar, and the commit records showed five retold published posts. The first fix had only stopped lost marks.",
      "rule_now": "Refuse to write an already-written candidate, save marks under a lock on a freshly re-read list, and detect duplicates by shared commits rather than title, slug or id.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "engine-retold-the-same-mistakes-under-new-ids",
      "url": "https://www.vibeapp.win/posts/engine-retold-the-same-mistakes-under-new-ids",
      "title": "The engine retold the same mistakes under new ids",
      "date": "2026-10-03",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The bug hunt remembered written candidates by id, but ids changed every run, so the same stock-app mistakes came back as new candidates and five of seven queued drafts retold published posts.",
      "how_we_found_out": "No gate caught it; the duplicates turned up when the queue was reviewed by hand, and the five drafts were discarded before publishing.",
      "rule_now": "Decide duplicates by the commits a post cites, not by id or title: a published post citing more than half of a draft's commits makes it a duplicate, and publishing stops at two shared commits.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "weekly-search-idle-priority-blocked-broker-login",
      "url": "https://www.vibeapp.win/posts/weekly-search-idle-priority-blocked-broker-login",
      "title": "The weekly search that put the whole API on idle priority, and blocked the broker login six Saturdays out of six",
      "date": "2026-10-03",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "A weekly search lowered the whole API's priority to Idle, and its inline wait blocked the scheduler, so the broker token renewal failed every Saturday. We had blamed the broker's weekend maintenance for six weeks.",
      "how_we_found_out": "A 6.1-second stall led to the priority bug. Then the job history showed no job ran from 03:30:51 to 05:49:51, and break times matched search end times six out of six.",
      "rule_now": "Run heavy work in a child process and give long jobs their own queue. When an external failure repeats weekly, check our own job history for what ended then before blaming the supplier.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "three-parallel-writers-overwrote-each-others-marks",
      "url": "https://www.vibeapp.win/posts/three-parallel-writers-overwrote-each-others-marks",
      "title": "Three parallel writers overwrote each other's marks",
      "date": "2026-10-03",
      "apps": [
        "vibeapp.win"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "Parallel writers each saved their own stale copy of the candidate list, so \"written\" marks were lost and the same candidates got written again. Nothing checked for an existing mark.",
      "how_we_found_out": "Duplicate drafts of the same story showed up in a busy review queue. Tracing them led to the list on disk, where marks were missing for posts that existed.",
      "rule_now": "Write shared state under a lock and re-read the list right before saving, never from a copy loaded before slow work. Refuse to write a candidate that is already written.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "every-wednesday-missing-from-the-five-day-measurement",
      "url": "https://www.vibeapp.win/posts/every-wednesday-missing-from-the-five-day-measurement",
      "title": "Every Wednesday was missing from the five-day measurement",
      "date": "2026-10-03",
      "apps": [
        "The stock app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "The five-day result looked for the last price bar before midnight on day five. Bars are stamped with the exchange's opening time, so day five itself was always excluded. For a Wednesday signal that left Friday's bar, two days out, which was too short to count, so the result was never written.",
      "how_we_found_out": "Henning asked Claude to check the app on 3 October. Counting five-day results per signal day showed 0 on every Wednesday.",
      "rule_now": "Synthetic time series in tests need real timestamps and a real calendar, with weekends and holidays. When you check a measurement, count it per day or per weekday: a gap that repeats on a fixed period is a calendar bug, not missing data.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 10,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "watchdogs-that-could-never-fire",
      "url": "https://www.vibeapp.win/posts/watchdogs-that-could-never-fire",
      "title": "Watchdogs that could never fire, or fired on normal operation",
      "date": "2026-09-30",
      "apps": [
        "The stock app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "Several stock app watchdogs could not see the failures they were built for: the tunnel check, the push check and the portfolio watch stayed green or silent. Another one raised false alarms on normal operation.",
      "how_we_found_out": "A real Docker outage, a direct question from Henning and a missed earnings-miss event exposed them. The checks themselves never flagged anything.",
      "rule_now": "Measure what the watchdog is meant to detect, test it against a known failure before trusting its green, treat silence as a symptom, and count a fault only when the system could have acted and didn't.",
      "introduced_date": "2026-07-31",
      "found_date": "2026-07-31",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "person-pages-competed-with-tag-pages-plates-differed",
      "url": "https://www.vibeapp.win/posts/person-pages-competed-with-tag-pages-plates-differed",
      "title": "Person pages competed with tag pages, and plates differed per page",
      "date": "2026-09-28",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "The same person had a tag page and a person page competing for name searches. Four portrait stories lacked the person's tag, and the drawn plate differed between pages and years.",
      "how_we_found_out": "Search Console URL Inspection showed Google did not know any of the twelve person pages. The plate and tag faults were listed in the 24 September commit without a stated discovery method.",
      "rule_now": "Use one key per entity everywhere. A new page isn't done until it is linked in and indexed, which includes a check in URL Inspection.",
      "introduced_date": "2026-09-24",
      "found_date": "2026-09-28",
      "days_in_code": 4,
      "days_is_lower_bound": false,
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "alert-channel-dead-seven-days-status-green",
      "url": "https://www.vibeapp.win/posts/alert-channel-dead-seven-days-status-green",
      "title": "The alert channel was dead for seven days while the status page stayed green",
      "date": "2026-09-26",
      "apps": [
        "The stock app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The push subscription was deleted after a 410, and 44 alerts reached nobody for seven days. The status check only tested whether push was configured, so it stayed green.",
      "how_we_found_out": "Henning asked Claude to look at the alert channel on 26 September. The stored alerts' delivery flags showed 0 of 44 delivered since 19 September.",
      "rule_now": "A watchdog must measure the outcome, not the setup. Count subscribers and check that recent alerts went out. Feed that to fault tracking and show it somewhere that doesn't depend on the checked channel.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "calendar-comment-claims-not-supported-by-sources",
      "url": "https://www.vibeapp.win/posts/calendar-comment-claims-not-supported-by-sources",
      "title": "A calendar comment that said more than the sources did",
      "date": "2026-09-24",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "from-memory",
      "pattern_name": "Written from memory",
      "what_went_wrong": "A calendar comment said Årets kokk had moved to a three-stage format and that Denmark hosts Michelin in 2027. The first belonged to NM i kokkekunst, and the second had not been announced.",
      "how_we_found_out": "While adding four new entries, each date was re-read on the organiser's own page. The existing comment did not match what those sources said.",
      "rule_now": "Check every factual claim against the primary source, comments included. If the source says planned, write planned. If there is no source, leave the claim out.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": "Class D: chained hallucination and fabrication"
    },
    {
      "slug": "placebo-median-benchmark-52-percent-false-edge",
      "url": "https://www.vibeapp.win/posts/placebo-median-benchmark-52-percent-false-edge",
      "title": "Against the median, 52.5% of random sources looked like a proven edge",
      "date": "2026-09-23",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The app measured excess return against the universe median, which skewed returns sit below the mean. Random sources beat it by about 1 pp, so 52.5% of them looked like a proven edge.",
      "how_we_found_out": "A placebo test of 200 random sources on real signal days and outcomes showed 52.5% false edges against the median and 0.5% against the mean.",
      "rule_now": "Use the mean as the null for excess return and the median only for hit rate. Placebo and time-travel checks now run as standing audit guards: measure the measuring setup, not just the sources.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "everything-written-from-memory-was-wrong",
      "url": "https://www.vibeapp.win/posts/everything-written-from-memory-was-wrong",
      "title": "Everything written from memory turned out wrong",
      "date": "2026-09-20",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "from-memory",
      "pattern_name": "Written from memory",
      "what_went_wrong": "Publisher names, feed URLs, a CLI flag, a DNS plan and a code comment were written from belief rather than checked, and several were wrong.",
      "how_we_found_out": "Checking names against publishers' websites, flags against claude --help and the gate against stored drafts revealed the errors.",
      "rule_now": "Take names from the source's own page, find feed URLs with a tool, and take version-specific facts from the installed version.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 15,
      "closest_daplab": "",
      "closest_wu2026": "Class D: chained hallucination and fabrication"
    },
    {
      "slug": "hand-edited-captions-bypassed-escaping",
      "url": "https://www.vibeapp.win/posts/hand-edited-captions-bypassed-escaping",
      "title": "Hand-edited captions skipped LinkedIn escaping",
      "date": "2026-09-20",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "A caption edited by hand in the approval queue replaced the rendered text and skipped LinkedIn's escaping, so a parenthesis could be read as markup. The length check also measured unescaped text.",
      "how_we_found_out": "Looking at the approval screens rendered against real articles turned up the correctness bug.",
      "rule_now": "Any text reaching a channel, generated or typed, goes through that channel's preparation step right before sending. Length limits are checked after it, on the exact outgoing text.",
      "introduced_date": "2026-09-20",
      "found_date": "2026-09-20",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 9,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "three-weeks-of-a-source-delivering-nothing",
      "url": "https://www.vibeapp.win/posts/three-weeks-of-a-source-delivering-nothing",
      "title": "Three weeks of a source delivering nothing",
      "date": "2026-09-20",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "The collector followed the last sitemap child instead of the newest, so a source read an author list and delivered nothing for three weeks, and a gzip robots.txt also failed silently.",
      "how_we_found_out": "Found along the way during a source registry expansion session on 2026-09-20, not by monitoring.",
      "rule_now": "Pick sitemap children by freshest lastmod, decompress by content-encoding and strip null bytes, commit work in the same session, and treat a source returning zero every time as a symptom.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 12,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "auth-alert-said-run-stopped-but-it-hadnt",
      "url": "https://www.vibeapp.win/posts/auth-alert-said-run-stopped-but-it-hadnt",
      "title": "The alert you wake up for said the run had stopped, but it hadn't",
      "date": "2026-09-20",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "stale-words",
      "pattern_name": "Words that stopped being true",
      "what_went_wrong": "The new auth-failure alert reused the quota alert's title \"The run stopped\", but on an auth failure the run keeps going, so the alert said something untrue.",
      "how_we_found_out": "The changelog hook read the diff and wrote that the title said the run stopped even though the run went through.",
      "rule_now": "Alert text must describe what actually happened in that case. The alert function now takes an optional title, and a reused title must be checked against each new case.",
      "introduced_date": "2026-09-20",
      "found_date": "2026-09-20",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "auth-failure-recorded-as-successful-run",
      "url": "https://www.vibeapp.win/posts/auth-failure-recorded-as-successful-run",
      "title": "The auth failure that was recorded as a successful run",
      "date": "2026-09-20",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "When the login to Claude died, every model call failed, steps reported 0 stories and the run was logged as successful, with no alert, though a task item said the alert was done.",
      "how_we_found_out": "The fix commit describes the gap; the model call log already held auth error classes but nothing checked it after a run.",
      "rule_now": "Give every silent failure mode its own alert, check the model call log after each run, and make sure a test backs any comment about what should wake you.",
      "introduced_date": "2026-09-01",
      "found_date": "2026-09-20",
      "days_in_code": 19,
      "days_is_lower_bound": false,
      "sources": 8,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "the-fix-that-was-never-built",
      "url": "https://www.vibeapp.win/posts/the-fix-that-was-never-built",
      "title": "The fix that was never built",
      "date": "2026-09-18",
      "apps": [
        "The newsroom engine",
        "The kokkekunst.no site"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "A correct parser fix looked like it failed because the bundle build, with output sent to /dev/null, had silently failed after a misplaced edit to the seed file.",
      "how_we_found_out": "Claude grepped the bundle for the fix, found nothing, and rebuilt with errors visible, which showed the syntax error.",
      "rule_now": "Build with visible errors, and before deciding a fix doesn't work, check that it is actually in the output.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "todays-story-label-showed-two-day-old-story",
      "url": "https://www.vibeapp.win/posts/todays-story-label-showed-two-day-old-story",
      "title": "'Today's story' label showed a two-day-old story",
      "date": "2026-09-16",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "stale-words",
      "pattern_name": "Words that stopped being true",
      "what_went_wrong": "The front page label always said \"Dagens sak\" (today's story) with the story's date, but the static site was not rebuilt daily, so on 16 September it showed 14 September.",
      "how_we_found_out": "The front page was seen reading \"Dagens sak — 14. september\" on 16 September. The commit traced it to the deploy hook not being set.",
      "rule_now": "Don't assume a static site is rebuilt daily. Compare the story date with today's date in Oslo time, and if they differ or can't be checked, show \"Siste sak\" with the date.",
      "introduced_date": "2026-08-30",
      "found_date": "2026-09-16",
      "days_in_code": 17,
      "days_is_lower_bound": false,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "rest-design-one-permission-token",
      "url": "https://www.vibeapp.win/posts/rest-design-one-permission-token",
      "title": "A REST design that could not work with a one-permission token",
      "date": "2026-09-05",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "The first design used REST calls to find the account and site, which a one-permission token can't make. The first call returned success with an empty list, and the error message blamed the correct permission.",
      "how_we_found_out": "It was found when the design was run against the real token. The fix commit recorded the empty list and two refused calls.",
      "rule_now": "Run against the real token first, treat success with an empty result as unverified, and change the design rather than widen the token.",
      "introduced_date": "2026-09-05",
      "found_date": "2026-09-05",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 3,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "canonical-urls-pointed-at-redirects",
      "url": "https://www.vibeapp.win/posts/canonical-urls-pointed-at-redirects",
      "title": "Every canonical URL pointed at a page that redirects",
      "date": "2026-09-05",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "Top-level pages named .html canonical URLs that answer 308, because the layout used the build-time path, while article pages set their own and hid the bug.",
      "how_we_found_out": "The fix commit, six days after launch with only the front page indexed, traced the canonical to the build-time path and checked the build output.",
      "rule_now": "Check the built result, check that canonicals answer without redirect, and check shared defaults on the pages nobody opens.",
      "introduced_date": "2026-08-30",
      "found_date": "2026-09-05",
      "days_in_code": 6,
      "days_is_lower_bound": true,
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "quality-gates-flagged-correct-text-real-error-got-through",
      "url": "https://www.vibeapp.win/posts/quality-gates-flagged-correct-text-real-error-got-through",
      "title": "Our quality gates flagged correct text, and a real error got through",
      "date": "2026-09-04",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The fact gate flagged the correct publisher name in seven of fourteen stories and matched NTB inside \"restaurantbransjen\". A real error in the same findings list was ignored and published.",
      "how_we_found_out": "After a duplicate jury story was published with an outdated name, the gates were measured on real drafts. That showed the two false-positive causes.",
      "rule_now": "Test every check on real data, match names as whole words with Unicode-aware lookaround, and treat noise in a check as a bug.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "unpublished-article-kept-error-in-description",
      "url": "https://www.vibeapp.win/posts/unpublished-article-kept-error-in-description",
      "title": "The unpublished article still carried the error in its description",
      "date": "2026-09-02",
      "apps": [
        "The newsroom engine"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "The tombstone for an unpublished article kept the original lead, which the template used as meta description and share text, so the error kept being served.",
      "how_we_found_out": "The first real unpublishing on 2 September 2026 showed it; the existing test only checked that the article body was gone.",
      "rule_now": "When removing an article for an error, replace every field that repeats it, including the lead, and keep a regression test that checks this.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "sitemap-filter-that-could-never-filter",
      "url": "https://www.vibeapp.win/posts/sitemap-filter-that-could-never-filter",
      "title": "A sitemap filter that could never filter",
      "date": "2026-08-30",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The sitemap filter ended in `|| true`, so it let every URL through, including unpublished noindex stories. Without it, the filter would have dropped all news URLs.",
      "how_we_found_out": "It was found and fixed in the 30 August 2026 design round, where the commit called the filter a no-op.",
      "rule_now": "Every filter or check needs a test showing it can fail, and `|| true` should not be used by default.",
      "introduced_date": "2026-08-30",
      "found_date": "2026-08-30",
      "days_in_code": 0,
      "days_is_lower_bound": true,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "about-page-privacy-promise-broke-with-analytics",
      "url": "https://www.vibeapp.win/posts/about-page-privacy-promise-broke-with-analytics",
      "title": "The About page promised no third parties would see you. Then we turned on analytics.",
      "date": "2026-08-30",
      "apps": [
        "The kokkekunst.no site"
      ],
      "pattern": "stale-words",
      "pattern_name": "Words that stopped being true",
      "what_went_wrong": "The About page promised no third party sees your visit. Then Cloudflare Web Analytics was switched on in Pages, which injects a script from Cloudflare, so the promise became false.",
      "how_we_found_out": "It was fixed in the same commit that records analytics being switched on. The commit does not say who spotted it.",
      "rule_now": "When a feature changes, search every text that promises something about it and update them together. Check absolute claims against what the site loads, and state the exception.",
      "introduced_date": "2026-08-30",
      "found_date": "2026-08-30",
      "days_in_code": 0,
      "days_is_lower_bound": true,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "every-save-reset-every-other-setting",
      "url": "https://www.vibeapp.win/posts/every-save-reset-every-other-setting",
      "title": "Every save reset every other setting",
      "date": "2026-08-23",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "The settings route used zod's .partial(), which still filled defaults, so saving one key wrote all fifteen and reset untouched settings to defaults. Turning off one Claude switch turned four others back on.",
      "how_we_found_out": "Henning said the switches seemed linked, and the database showed all fifteen rows with the same timestamp. The optimistic UI had hidden the server's answer until a reload.",
      "rule_now": "Write only the keys the caller actually sent, using settingsPatch() and not settingsSchema.partial(). Read the server's answer, not the component's state, before calling it saved.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "camera-aimed-twice-at-stale-box-zoomed-blind",
      "url": "https://www.vibeapp.win/posts/camera-aimed-twice-at-stale-box-zoomed-blind",
      "title": "The camera aimed twice at a stale box and zoomed blind",
      "date": "2026-08-23",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "The close-up job zoomed with no target box, read focus after a fixed 500 ms pause, and corrected twice for a stale box. The aiming decision was logged at debug, so none of this showed.",
      "how_we_found_out": "The first real close-up missed. Logging each pulse at info showed the zoom with no box, identical boxes in two pulses one second apart, and a soft frame taken during autofocus.",
      "rule_now": "Log the aiming decision at info, zoom only after a box was seen, stop when the box matches the last round, and wait for focus to settle with a ceiling instead of a fixed pause.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "exam-poisoned-by-same-wrong-boxes-as-training-set",
      "url": "https://www.vibeapp.win/posts/exam-poisoned-by-same-wrong-boxes-as-training-set",
      "title": "The exam was poisoned by the same wrong boxes as the training set",
      "date": "2026-08-22",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The detector's exam was built from the same false \"person\" labels as the training set. 16 of 195 answer boxes were a static object, all in the regression part, so the gate penalised the right behaviour.",
      "how_we_found_out": "After the training data problem was found, the exam was rebuilt from the corrected database, and the same object turned up in its answer key.",
      "rule_now": "When a labelling error is found in training data, check the exam for it and rebuild the exam from corrected rows. Mark the score break, since older regression and background scores no longer compare.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "comments-claimed-enough-pixels-detector-squeezed-4k-to-320",
      "url": "https://www.vibeapp.win/posts/comments-claimed-enough-pixels-detector-squeezed-4k-to-320",
      "title": "The comments promised enough pixels. The detector squeezed 4K down to 320x320",
      "date": "2026-08-22",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "A comment called the sidecar detector the same model with enough pixels, but it resized every 4K frame to 320x320, coarser than the substream, so distant people were missed and events wrongly closed.",
      "how_we_found_out": "Henning doubted the detection logic; checking real rows and images showed an event closed as false detection while a person was visible in the snapshot. The task log counts 33 such closures over three days.",
      "rule_now": "Run the detector on the full frame plus four overlapping quadrants, send empty results to a second check, and treat comments about what a model sees as unverified until measured.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "the-gate-was-never-code-and-pointed-the-wrong-way",
      "url": "https://www.vibeapp.win/posts/the-gate-was-never-code-and-pointed-the-wrong-way",
      "title": "The gate was never code, and pointed the wrong way",
      "date": "2026-08-21",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "Decision D-26 required an A/B test on our own failure images before model swaps, but no code ran it. The only exam was built from the old model's own boxes and sat inside the training folder.",
      "how_we_found_out": "A review on 21 August 2026 checked what existed behind D-26 and found a 60-line answer-key file, 58 of 60 samples boxed by the base detector, and a measurement that used the training library's predict.",
      "rule_now": "A decision isn't a gate until code enforces it: freeze the exam outside training, make training refuse to run without it, include misses, and measure with the production code path.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "tests-measured-a-cache-that-was-never-cleared",
      "url": "https://www.vibeapp.win/posts/tests-measured-a-cache-that-was-never-cleared",
      "title": "Tests measured a cache that was never cleared",
      "date": "2026-08-20",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "The test reset skipped the table holding the vehicle-registry cache, so tests read leftover state and six of eight failed unnoticed. One requirement was marked met against a test that could not check it.",
      "how_we_found_out": "A commit to the monitoring app found tests seeing zero HTTP hits or cached results. A second test also aged a column the cache reader no longer used.",
      "rule_now": "Clear every table tests write to, between every test, whatever production retains. Write down exceptions, and make the reset refuse to run unless the database is the test one.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "tiling-worked-in-test-never-ran-in-production",
      "url": "https://www.vibeapp.win/posts/tiling-worked-in-test-never-ran-in-production",
      "title": "Tiling worked in a test and never ran in production",
      "date": "2026-08-17",
      "apps": [
        "The monitoring app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "Claude gated face tiling on there being no box, but the production job always sends a box. Tiling passed a direct test and never ran in production.",
      "how_we_found_out": "Claude ran the analyser over a morning's clips and compared results. Before the fix it found nothing, and after it found two faces.",
      "rule_now": "Run tiling on every frame next to the box crop and keep the best result. Test the path production takes, with the inputs production sends.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "dead-llm-ranking-code-that-looked-like-it-was-running",
      "url": "https://www.vibeapp.win/posts/dead-llm-ranking-code-that-looked-like-it-was-running",
      "title": "Dead LLM ranking code that looked like it was running",
      "date": "2026-08-12",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "dead-setting",
      "pattern_name": "Settings that did nothing",
      "what_went_wrong": "A full LLM ranking module and docs describing a 0.30 LLM weight existed, but the model-call function had not been called since a change on 14 May 2026 took the model out of the ranking and left the old path in place.",
      "how_we_found_out": "The dead path had already misled a debugging session, and deleting it left 458 of 467 tests passing, exactly the nine tests of the dead module removed.",
      "rule_now": "Delete dead code and correct or mark the documentation as historical in the same change, so nobody debugs a component that does not run.",
      "introduced_date": "2026-05-14",
      "found_date": "2026-08-12",
      "days_in_code": 90,
      "days_is_lower_bound": false,
      "sources": 10,
      "closest_daplab": "Codebase awareness & refactoring issues",
      "closest_wu2026": ""
    },
    {
      "slug": "booking-time-read-as-event-time",
      "url": "https://www.vibeapp.win/posts/booking-time-read-as-event-time",
      "title": "Booking time read as event time: two false alarms about trading after hours",
      "date": "2026-08-11",
      "apps": [
        "The stock app"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "A health check counted trades outside market hours by the row's timestamp. Dividends booked at noon on the ex-date and broker fills recorded at reconciliation time both showed up as after-hours trades.",
      "how_we_found_out": "Henning asked whether an alert really meant trading outside market hours. Claude checked the flagged rows against the data before changing anything, first on 7 August and again on 11 August.",
      "rule_now": "Know what a timestamp records before you judge by it. Share the marker between the code that writes a row and the check that reads it, and show what a check leaves out instead of hiding it.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 13,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "docs-claimed-nine-metrics-database-held-two",
      "url": "https://www.vibeapp.win/posts/docs-claimed-nine-metrics-database-held-two",
      "title": "The docs claimed nine metrics when the database held two",
      "date": "2026-08-07",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "A document said the database held nine accounting metrics, but it held two. Claude had described what the expanded code would produce as if the database already contained it.",
      "how_we_found_out": "Henning asked for a verification of the verification. Claude ran every checkable claim in two docs against the database, and 20 of 21 held; the nine metrics was the wrong one.",
      "rule_now": "When you write that we \"have\" data, check the database, not the code. Where a rule can be made runnable, make it a check, such as the new six-check docs command run by a Stop hook.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 10,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "guards-and-dials-that-never-ran-in-the-live-path",
      "url": "https://www.vibeapp.win/posts/guards-and-dials-that-never-ran-in-the-live-path",
      "title": "Guards and dials that were built but never ran in the path that mattered",
      "date": "2026-08-07",
      "apps": [
        "The stock app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "Four safeguards and settings built for the stock app never ran in the target-portfolio engine that the fleet and live account used, while unit tests stayed green.",
      "how_we_found_out": "Production measurements and watching the running system caught them: unfunded swaps, one-hour holds, a duplicate live buy order, and a ramp set in zero simulations.",
      "rule_now": "Verify a change alters behaviour in the running code, put guards in the one place every action passes through, and use a sensitivity row when a setting might never bind.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 4,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "dials-and-weights-that-never-took-effect",
      "url": "https://www.vibeapp.win/posts/dials-and-weights-that-never-took-effect",
      "title": "Dials and weights that never took effect",
      "date": "2026-08-07",
      "apps": [
        "The stock app"
      ],
      "pattern": "dead-setting",
      "pattern_name": "Settings that did nothing",
      "what_went_wrong": "Four dials or learning mechanisms in the stock app, including inverse_vol, the holding-period dial, matrix weights and the start-up ramp, looked right in code but had no effect in practice.",
      "how_we_found_out": "Found through pre-deploy audits and the monkey test, which showed positions held one hour despite a 5-day dial, plus engine-level checks with impact.",
      "rule_now": "Measure each dial's effect through the engine with impact, add a sensitivity row, and check that every code path passes the value.",
      "introduced_date": "2026-07-31",
      "found_date": "2026-07-31",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 4,
      "closest_daplab": "Codebase awareness & refactoring issues",
      "closest_wu2026": ""
    },
    {
      "slug": "double-order-guard-in-the-legacy-path",
      "url": "https://www.vibeapp.win/posts/double-order-guard-in-the-legacy-path",
      "title": "The double-order guard sat in the legacy path while the whole fleet ran the target-portfolio engine",
      "date": "2026-08-06",
      "apps": [
        "The stock app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "The guard against a second open order sat in the legacy candidate loop while the whole fleet ran the target-portfolio engine, so duplicate buys got through, and the sell side was missed too.",
      "how_we_found_out": "A duplicate buy appeared in the broker's open-order list on 5 August, and on 6 August the broker rejected a duplicate sell.",
      "rule_now": "Put the guard in the one function every order goes through, for both buys and sells, check our own order reference, count orders from the same cycle, and don't rely on the broker.",
      "introduced_date": "2026-08-05",
      "found_date": "2026-08-05",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "exit-measurement-counted-simulations-not-decisions",
      "url": "https://www.vibeapp.win/posts/exit-measurement-counted-simulations-not-decisions",
      "title": "The exit measurement counted simulations, not decisions",
      "date": "2026-08-05",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "Exit outcomes were stored one row per simulation, and the aggregate and go-live gate counted rows. The same sale was counted several times, so accuracy looked better (85.7% vs 66.7%) and intervals looked narrower.",
      "how_we_found_out": "Claude recalculated the result by hand from the raw rows and counted the copies of each decision. No test caught it, and the fix was checked against that hand count.",
      "rule_now": "A row is not a decision: deduplicate on the decision (with the replacement instrument in the key) before counting. Show row count beside decision count, and add an audit check that fails if they drift.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "rate-limit-handled-only-in-smoke-test",
      "url": "https://www.vibeapp.win/posts/rate-limit-handled-only-in-smoke-test",
      "title": "Known rate limit handled only in the smoke test, not production",
      "date": "2026-08-05",
      "apps": [
        "The stock app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "Claude hit the broker's order rate limit in a test and added a pause only to the smoke test. On the first live cycle, three of six orders were rejected with HTTP 429.",
      "how_we_found_out": "The owner reported rejected Saxo purchases, and the order log showed the 429s. The fix commit admits the limit was already known.",
      "rule_now": "A known limitation handled only in the test tool is not handled, so the fix goes into the production path: an engine pause between orders and a 429 retry.",
      "introduced_date": "2026-08-05",
      "found_date": "2026-08-05",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "prediction-ignored-turnover-average-week",
      "url": "https://www.vibeapp.win/posts/prediction-ignored-turnover-average-week",
      "title": "Store the forecast that was actually used",
      "date": "2026-08-05",
      "apps": [
        "The lottery experiment"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "The post gives no account of a specific failure. It only states a rule about prediction work.",
      "how_we_found_out": "The text does not say how a problem was found.",
      "rule_now": "Store the forecast that was actually used next to the prediction it fed, and always compare against a simple persistence baseline.",
      "introduced_date": "2026-07-02",
      "found_date": "2026-08-05",
      "days_in_code": 34,
      "days_is_lower_bound": false,
      "sources": 11,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "migration-skipped-silently-crashed-trading-cycle",
      "url": "https://www.vibeapp.win/posts/migration-skipped-silently-crashed-trading-cycle",
      "title": "The migration that said \"completed\" and never ran",
      "date": "2026-08-05",
      "apps": [
        "The stock app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "Claude wrote a migration by hand and left it out of the journal. The tool skipped it and printed \"Migrations completed\", and a trading cycle then crashed on the missing column.",
      "how_we_found_out": "The owner's report of missing transactions started the work. The crash appeared in the cycle record as skipped-abnormal.",
      "rule_now": "A test now checks the journal against the SQL files in both directions. After a migration, query for the thing it should have created.",
      "introduced_date": "2026-08-05",
      "found_date": "2026-08-05",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 11,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "measurements-that-answered-the-wrong-question",
      "url": "https://www.vibeapp.win/posts/measurements-that-answered-the-wrong-question",
      "title": "Measurements that answered the wrong question",
      "date": "2026-08-05",
      "apps": [
        "The stock app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "Four measurements in the stock app gave clean numbers but counted the wrong thing: rows, market exposure, elements, and rows instead of distinct companies.",
      "how_we_found_out": "They came from real data, Henning's comments and questions, and a deliberate sweep. The exit fix was checked against a hand calculation and an invariant that was seen to fail.",
      "rule_now": "Group by decision and count distinct instruments. Use the same yardstick for every source. Verify a measure against a case where you know the answer.",
      "introduced_date": "2026-07-02",
      "found_date": "2026-08-02",
      "days_in_code": 31,
      "days_is_lower_bound": false,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "missing-fields-rendered-as-all-is-normal",
      "url": "https://www.vibeapp.win/posts/missing-fields-rendered-as-all-is-normal",
      "title": "Missing fields and empty results rendered as «all is normal»",
      "date": "2026-08-05",
      "apps": [
        "The stock app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "Claude's status tool read a field that did not exist and printed \"skipped: (no)\" for a crashed cycle. The first fix then used a guessed list of normal verdicts and raised two false alarms.",
      "how_we_found_out": "The crash only showed because the raw output was run by chance. The false alarms came up in the same evening's checks.",
      "rule_now": "Never show absence as all ok. Use a typed map over every verdict value so a new value breaks the build, and give abnormal states an exit code.",
      "introduced_date": "2026-08-05",
      "found_date": "2026-08-05",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 3,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "pairing-500-when-local-embedding-model-was-down",
      "url": "https://www.vibeapp.win/posts/pairing-500-when-local-embedding-model-was-down",
      "title": "Pairing crashed with a 500 when the local model was down",
      "date": "2026-08-04",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "no-plan-b",
      "pattern_name": "No plan B when a dependency fails",
      "what_went_wrong": "The pairing recommendation function called the local embedder with no protection. When the host slept or the model was unloaded, the call raised and the user got a 500.",
      "how_we_found_out": "Henning reported an error in the app on the food-and-wine pairing. The fix commit traced it to the unprotected embedder call.",
      "rule_now": "Enrichment must never block. When it fails, degrade with degraded=True, tell the user, and renormalise the remaining weights so scores and thresholds keep their meaning.",
      "introduced_date": "2026-05-14",
      "found_date": "2026-08-04",
      "days_in_code": 82,
      "days_is_lower_bound": true,
      "sources": 7,
      "closest_daplab": "API & external service integration failures",
      "closest_wu2026": ""
    },
    {
      "slug": "image-backup-silently-dead-seven-weeks",
      "url": "https://www.vibeapp.win/posts/image-backup-silently-dead-seven-weeks",
      "title": "The image backup was silently dead for seven weeks",
      "date": "2026-08-04",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "A script with Windows line endings failed every night in Alpine, and the container still reported healthy. The photo backup produced nothing for seven weeks.",
      "how_we_found_out": "A review on 4 August found the dead photo backup. It also found that the database dumps could not be restored into an empty database.",
      "rule_now": "Healthy does not mean the backup works, so check backup freshness and test restores into an empty database. Lock shell scripts to LF in .gitattributes and show backup age in the app.",
      "introduced_date": "2026-06-13",
      "found_date": "2026-08-04",
      "days_in_code": 52,
      "days_is_lower_bound": false,
      "sources": 7,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "errors-swallowed-into-legitimate-empty-state",
      "url": "https://www.vibeapp.win/posts/errors-swallowed-into-legitimate-empty-state",
      "title": "Three errors that looked like empty results",
      "date": "2026-08-04",
      "apps": [
        "The stock app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "In three places a failed call became a valid empty result: a volatility query, a news-tone job reporting success with 0 rows, and insider data parsed to all zeros.",
      "how_we_found_out": "A guard for another finding, a manual verification round and a question from the project owner exposed them, not the app itself.",
      "rule_now": "Keep errors apart from empty results and throw when everything fails; a silent-zero sweep now flags all-zero columns unless registered with a reason and expiry date.",
      "introduced_date": "2026-07-31",
      "found_date": "2026-07-31",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 10,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "verification-matched-page-title-tab-bar-missing",
      "url": "https://www.vibeapp.win/posts/verification-matched-page-title-tab-bar-missing",
      "title": "My verification matched the page title, and I told the owner the tab bar was there",
      "date": "2026-08-03",
      "apps": [
        "The stock app"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "Claude put the new tab bar in the fallback branch that only runs when status fails to load. It then checked for two words that appear elsewhere on the page, and told Henning the tab bar was there.",
      "how_we_found_out": "Henning sent a screenshot of the status page showing no tab bar between the waiting list and data freshness.",
      "rule_now": "Verify against markup only the tested thing can produce, such as the border-b-2 class, and count the elements (four buttons). A string that can appear elsewhere on the page proves nothing.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 8,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "type-cast-hid-missing-field-empty-result-column",
      "url": "https://www.vibeapp.win/posts/type-cast-hid-missing-field-empty-result-column",
      "title": "A type cast hid a missing field, and every sale showed a dash",
      "date": "2026-07-31",
      "apps": [
        "The stock app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "A TypeScript `as` cast claimed a field existed that the query never selected, so the lookup matched nothing and every sale in the result column showed the fallback dash.",
      "how_we_found_out": "Henning, who owns the app, noticed the result was missing from the transactions on the sims after the feature was deployed. The code had compiled fine.",
      "rule_now": "Don't use an `as` cast to force a field into existence; add the field to the query's select and let the type system check it, so a missing field becomes a compile error.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 9,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "strategy-epoch-15-hours-in-the-future",
      "url": "https://www.vibeapp.win/posts/strategy-epoch-15-hours-in-the-future",
      "title": "A start-of-measurement timestamp set 15 hours in the future silenced all evidence",
      "date": "2026-07-30",
      "apps": [
        "The stock app"
      ],
      "pattern": "silent-failure",
      "pattern_name": "Failing silently",
      "what_went_wrong": "Claude set the strategy epoch about 15 hours in the future, so every measurement quietly showed zero and the go-live gate looked like a strategy that had just started.",
      "how_we_found_out": "Claude found it during a fourth full review of its own new code.",
      "rule_now": "For every constant that is a point in time, check it is in the past and whether a wrong value gives a wrong or a missing answer; if missing, add a guard, a visible health check and a failing test.",
      "introduced_date": "2026-07-30",
      "found_date": "2026-07-30",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 8,
      "closest_daplab": "Exception & error handling",
      "closest_wu2026": "Class C: error swallowing and dilution"
    },
    {
      "slug": "version-skew-blank-health-page",
      "url": "https://www.vibeapp.win/posts/version-skew-blank-health-page",
      "title": "Version skew gave a blank health page",
      "date": "2026-07-26",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "A newer frontend read a field that the older backend did not send, and with no error boundary the thrown error blanked the whole app, caused by rebuilding only one image.",
      "how_we_found_out": "Henning opened the health page on 26 July 2026 and saw a blank page; the cause was traced to the undefined read during render.",
      "rule_now": "Build and deploy the whole stack together, read new backend fields defensively as optional, and wrap the routes in an error boundary.",
      "introduced_date": "2026-07-20",
      "found_date": "2026-07-26",
      "days_in_code": 6,
      "days_is_lower_bound": false,
      "sources": 5,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "baseline-made-model-look-12-times-better",
      "url": "https://www.vibeapp.win/posts/baseline-made-model-look-12-times-better",
      "title": "A baseline that made the model look 12 times better than it was",
      "date": "2026-07-15",
      "apps": [
        "The lottery experiment"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "The live dashboard baseline pooled winner counts across prize tiers, which inflated its error and made the model look about 12 times better than it was.",
      "how_we_found_out": "The task log calls it a method error found and corrected on 2026-07-15; the offline validation already used a per-tier baseline.",
      "rule_now": "A baseline must be prequential, per tier, measured on the same rows as the model, and reported with the row count, or say there are too few rows.",
      "introduced_date": "2026-07-02",
      "found_date": "2026-07-15",
      "days_in_code": 13,
      "days_is_lower_bound": false,
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "home-screen-icon-missing-page-icon-was-not-the-cause",
      "url": "https://www.vibeapp.win/posts/home-screen-icon-missing-page-icon-was-not-the-cause",
      "title": "Home-screen icon missing: the page icon was not the cause",
      "date": "2026-07-04",
      "apps": [
        "The lottery experiment"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "The icon was set only as Streamlit's page icon, which covers the browser tab. The page had no manifest or apple-touch-icon, so the phone's home screen had nothing to pin.",
      "how_we_found_out": "The icon failed to appear on Henning's phone, which opened a follow-up task. Its root-cause analysis looked at where home-screen icons actually come from.",
      "rule_now": "Verify in the built result, not just in the source. Check the patched index.html in the build and fetch the real manifest over HTTP.",
      "introduced_date": "2026-07-04",
      "found_date": "2026-07-04",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "ten-signature-serves-wrong-until-checked-against-producers",
      "url": "https://www.vibeapp.win/posts/ten-signature-serves-wrong-until-checked-against-producers",
      "title": "Ten signature serves were wrong until we checked them against the producers' own sites",
      "date": "2026-07-03",
      "apps": [
        "The gin guide"
      ],
      "pattern": "wrong-diagnosis",
      "pattern_name": "The wrong diagnosis",
      "what_went_wrong": "The May audit compared serves with noisy Tavily search results and read that as keeping the 15 original serves, which were never checked against the producers.",
      "how_we_found_out": "A July audit with web agents fetching serves from producers' own sites found all 10 checked serves differed from the official recommendation.",
      "rule_now": "Every serve must trace back to a producer source with a URL, because comparing with a secondary source, or finding it too poor to use, is not verification.",
      "introduced_date": "2026-05-31",
      "found_date": "2026-07-03",
      "days_in_code": 33,
      "days_is_lower_bound": false,
      "sources": 7,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "sync-merged-lists-brought-back-deleted-items",
      "url": "https://www.vibeapp.win/posts/sync-merged-lists-brought-back-deleted-items",
      "title": "Sync merged the lists and brought back deleted items",
      "date": "2026-07-02",
      "apps": [
        "The gin guide"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "At startup the app merged local and server lists as a union, so an item deleted on one device came back from another device's local storage.",
      "how_we_found_out": "A code review round on 2 July 2026 caught it, and a new five-check Playwright scenario deletes an item on another device and confirms it stays gone.",
      "rule_now": "The server copy is the source of truth at startup. Seed from local only when the server has nothing, never after a network error, and keep one shared sync hook.",
      "introduced_date": "2026-06-22",
      "found_date": "2026-07-02",
      "days_in_code": 10,
      "days_is_lower_bound": false,
      "sources": 4,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "production-writes-silently-rolled-back-tests-could-not-see-it",
      "url": "https://www.vibeapp.win/posts/production-writes-silently-rolled-back-tests-could-not-see-it",
      "title": "Production writes were silently rolled back and the tests could not see it",
      "date": "2026-07-02",
      "apps": [
        "The lottery experiment"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "The weekly run's database writes were rolled back on close because plain reads opened an outer transaction. The end-to-end test read back on the writing connection and passed.",
      "how_we_found_out": "It was found during the full backfill task and the first real weekly run, which showed the model, prediction and suggestions were not persisted.",
      "rule_now": "Open connections with autocommit on, use explicit transactions for writes, and verify saved data through a separate connection set up the way production sets it up.",
      "introduced_date": "2026-07-02",
      "found_date": "2026-07-02",
      "days_in_code": 0,
      "days_is_lower_bound": false,
      "sources": 9,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "local-llm-calls-had-no-timeout",
      "url": "https://www.vibeapp.win/posts/local-llm-calls-had-no-timeout",
      "title": "Local LLM calls had no timeout, so one hung model could hang a request forever",
      "date": "2026-07-02",
      "apps": [
        "The gin guide"
      ],
      "pattern": "no-plan-b",
      "pattern_name": "No plan B when a dependency fails",
      "what_went_wrong": "Calls to local LM Studio and Ollama servers had no timeout, so a hung model could hang a request forever. JSON files were also written in place, so a crash mid-write could corrupt them.",
      "how_we_found_out": "A code review on 2 July 2026 found both problems. The record does not say whether either hit a real user.",
      "rule_now": "Give every external call an explicit timeout, even local ones. Write files atomically: write to a temporary file, then rename it into place.",
      "introduced_date": "2026-05-31",
      "found_date": "2026-07-02",
      "days_in_code": 32,
      "days_is_lower_bound": true,
      "sources": 6,
      "closest_daplab": "API & external service integration failures",
      "closest_wu2026": ""
    },
    {
      "slug": "ci-syntax-check-could-not-catch-broken-server",
      "url": "https://www.vibeapp.win/posts/ci-syntax-check-could-not-catch-broken-server",
      "title": "The CI check only checked syntax and could not catch a broken server",
      "date": "2026-07-02",
      "apps": [
        "The gin guide"
      ],
      "pattern": "check-never-fires",
      "pattern_name": "A check that could never fail",
      "what_went_wrong": "CI only ran node --check, which parses the server file but never starts it. It could not catch the SPA fallback regex route breaking under Express 5.",
      "how_we_found_out": "A code review during the Express 5 upgrade showed the old CI would not have caught the breakage from path-to-regexp v8.",
      "rule_now": "If a check cannot fail on the errors you fear, it is not a check; CI now boots the server and tests real endpoints, and you should ask what change would turn a green step red.",
      "introduced_date": "2026-06-16",
      "found_date": "2026-07-02",
      "days_in_code": 16,
      "days_is_lower_bound": false,
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": "Class E: operational omission and forensic blind spots"
    },
    {
      "slug": "brand-matching-wrong-brand-and-inside-words",
      "url": "https://www.vibeapp.win/posts/brand-matching-wrong-brand-and-inside-words",
      "title": "Brand matching picked the wrong brand, and matched inside words",
      "date": "2026-07-02",
      "apps": [
        "The gin guide"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "The matching function mapped \"Tanqueray No. Ten\" to Tanqueray London Dry, \"Star of Bombay\" to Bombay Sapphire, and the key \"oss\" matched inside other words.",
      "how_we_found_out": "A full code review on 2 July 2026 found it among five bugs.",
      "rule_now": "Match text against keys using the longest key and word boundaries, and test short keys, substring keys and keys inside unrelated words.",
      "introduced_date": "2026-05-31",
      "found_date": "2026-07-02",
      "days_in_code": 32,
      "days_is_lower_bound": true,
      "sources": 7,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "green-locally-red-in-ci-undeclared-transitive-dependency",
      "url": "https://www.vibeapp.win/posts/green-locally-red-in-ci-undeclared-transitive-dependency",
      "title": "Green locally, red in CI: an undeclared transitive dependency",
      "date": "2026-06-17",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "test-not-production",
      "pattern_name": "Fine in the test, broken in production",
      "what_went_wrong": "The backend relied on tzlocal arriving via Celery without declaring it; the local image had it, but a clean CI install did not and pytest failed on import.",
      "how_we_found_out": "CI failed with ModuleNotFoundError for tzlocal, and the fix commit traced it to Celery importing it at module load.",
      "rule_now": "Declare any package needed at import time explicitly with a comment saying why, and let a clean CI install judge whether the dependency list is complete.",
      "introduced_date": "",
      "found_date": "",
      "days_in_code": "",
      "days_is_lower_bound": "",
      "sources": 6,
      "closest_daplab": "",
      "closest_wu2026": "Class A: environment and platform quirks"
    },
    {
      "slug": "marking-one-variant-as-owned-marked-the-whole-brand",
      "url": "https://www.vibeapp.win/posts/marking-one-variant-as-owned-marked-the-whole-brand",
      "title": "Marking one variant as owned marked the whole brand",
      "date": "2026-06-16",
      "apps": [
        "The gin guide"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "Ownership was stored by brand id, and all variants shared that id. Marking Hendrick's Sunspell as owned marked every Hendrick's variant as owned.",
      "how_we_found_out": "The bug showed up as a symptom in use: mark one variant and the whole brand shows as owned. No test is recorded as catching it.",
      "rule_now": "When you add a new dimension such as variants, review every place that uses the old id as a key. Store the new level so old saved data still works.",
      "introduced_date": "2026-05-31",
      "found_date": "2026-06-16",
      "days_in_code": 16,
      "days_is_lower_bound": false,
      "sources": 7,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "prompt-examples-copied-verbatim",
      "url": "https://www.vibeapp.win/posts/prompt-examples-copied-verbatim",
      "title": "Prompt examples the model copied word for word",
      "date": "2026-06-13",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "other",
      "pattern_name": "Other",
      "what_went_wrong": "Concrete example values in three prompts were copied into the model output. The results were 66 bottles with the same tasting note, a shopping list citing dishes never logged, and a wine saved as red Nebbiolo.",
      "how_we_found_out": "A user screenshot exposed the shopping list case. The 66 identical notes were diagnosed from the data. The Viña Sol case is not explained.",
      "rule_now": "Use placeholders, not examples, in prompt schemas. Add a deterministic echo-guard, prefer null to a guess, validate against known bottles, and check every prompt when one copies.",
      "introduced_date": "2026-05-14",
      "found_date": "2026-05-31",
      "days_in_code": 17,
      "days_is_lower_bound": true,
      "sources": 4,
      "closest_daplab": "",
      "closest_wu2026": ""
    },
    {
      "slug": "centiliters-parsed-as-milliliters-wrong-bottle-size",
      "url": "https://www.vibeapp.win/posts/centiliters-parsed-as-milliliters-wrong-bottle-size",
      "title": "Centiliters read as milliliters linked bottles to the wrong size",
      "date": "2026-06-13",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "data-read-wrong",
      "pattern_name": "Data read the wrong way",
      "what_went_wrong": "Vinmonopolet volume in centiliters was parsed as milliliters (75 cl as 75 ml) and the matcher ignored volume, so a 0.75 L bottle could link to a magnum or half bottle.",
      "how_we_found_out": "The fix commit noted the search result had a volume field that was unused and misread, and the price refresh had two more wrong-link paths.",
      "rule_now": "Use every field to tell candidates apart and check its unit, set a confidence floor before linking, and don't fall back to name search when an existing link can't be fetched.",
      "introduced_date": "2026-05-14",
      "found_date": "2026-06-13",
      "days_in_code": 30,
      "days_is_lower_bound": true,
      "sources": 7,
      "closest_daplab": "Data management errors",
      "closest_wu2026": ""
    },
    {
      "slug": "badge-counted-55-bottles-score-shown-as-out-of-5",
      "url": "https://www.vibeapp.win/posts/badge-counted-55-bottles-score-shown-as-out-of-5",
      "title": "A badge that counted 55 bottles, and a 0–1 score shown as /5",
      "date": "2026-06-13",
      "apps": [
        "The wine cellar app"
      ],
      "pattern": "wrong-measurement",
      "pattern_name": "Measuring the wrong thing",
      "what_went_wrong": "A badge counted all drinkable bottles (55), a 0–1 score was shown as \"/5\", and alcohol values compared as text gave 57 false conflicts.",
      "how_we_found_out": "Each number was read next to its label and contradicted it, and the commit messages state the contradictions.",
      "rule_now": "Check that a number means what its label says, including scale and definition, and compare numeric values as numbers, not strings.",
      "introduced_date": "2026-05-14",
      "found_date": "2026-06-13",
      "days_in_code": 30,
      "days_is_lower_bound": true,
      "sources": 3,
      "closest_daplab": "",
      "closest_wu2026": ""
    }
  ]
}
