<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Doby Baxter</title>
    <description>The latest articles on DEV Community by Doby Baxter (@dobybaxter127).</description>
    <link>https://dev.to/dobybaxter127</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4027698%2F76f09f21-843d-463d-bfc3-ee4aa035c479.png</url>
      <title>DEV Community: Doby Baxter</title>
      <link>https://dev.to/dobybaxter127</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dobybaxter127"/>
    <language>en</language>
    <item>
      <title>I Am Not an Archivist: What Careless Automation Asks of Disabled People</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Mon, 12 Oct 2026 21:39:22 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/i-am-not-an-archivist-what-careless-automation-asks-of-disabled-people-hom</link>
      <guid>https://dev.to/dobybaxter127/i-am-not-an-archivist-what-careless-automation-asks-of-disabled-people-hom</guid>
      <description>&lt;p&gt;I'm writing this in a studio that has had no heating since early July.&lt;/p&gt;

&lt;p&gt;I have reported it by email, by phone, through a formal complaint form, and to the building's reception. This week, a phone call finally explained why nothing happened: my emails and calls had been going to a team in another city the whole time.&lt;/p&gt;

&lt;p&gt;Nobody noticed. Nobody told me. I only found out because I kept chasing.&lt;/p&gt;

&lt;p&gt;This piece isn't really about one cold studio. It's about the job that lands on you when a system fails and nobody owns the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The job nobody hired me for
&lt;/h2&gt;

&lt;p&gt;When an organization's software gets something wrong, the work of putting it right rarely stays with the organization. It moves to the person.&lt;/p&gt;

&lt;p&gt;You become the &lt;strong&gt;archivist&lt;/strong&gt;, keeping every email, screenshot, reference number and date, because one day you may need to prove what happened.&lt;/p&gt;

&lt;p&gt;You become the &lt;strong&gt;advocate&lt;/strong&gt;, writing the complaint, finding the escalation route, and quoting the organization's own policies back to it.&lt;/p&gt;

&lt;p&gt;You become the &lt;strong&gt;detective&lt;/strong&gt;, connecting the dots its systems never connect: why this charge, why this name, why this rejection.&lt;/p&gt;

&lt;p&gt;And you become the &lt;strong&gt;project manager&lt;/strong&gt;, tracking deadlines across a dozen institutions that never talk to each other.&lt;/p&gt;

&lt;p&gt;I have ADHD and autism, and I live with memory difficulties and other health conditions. I cannot do this job reliably on my own. Most people couldn't keep it up for long, and nobody should have to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four failures, one shape
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The heating that was "logged on the system"
&lt;/h3&gt;

&lt;p&gt;Before that phone call, the most anyone had told me was that my issue was "logged on the system." No reference number, no name, no timeline.&lt;/p&gt;

&lt;p&gt;When I tried the out-of-hours number on the wall, it belonged to a separate estates company that looks after the building but not the flats, so they sent me back. The building's own receptionist told me she gets no replies from the company either. The direct number I was finally given shows as invalid on my phone.&lt;/p&gt;

&lt;p&gt;Every route pointed somewhere else. The only person who ever owned the problem was me.&lt;/p&gt;

&lt;h3&gt;
  
  
  The person who doesn't exist
&lt;/h3&gt;

&lt;p&gt;When I moved in, about three years ago, I had already changed my name by deed poll. The company managing the building at the time would not accept it and registered me under my old name.&lt;/p&gt;

&lt;p&gt;I complained in writing and in person. Staff at their local office told me to ignore the messages or block them.&lt;/p&gt;

&lt;p&gt;Then the building changed hands, and the new company inherited the old record. One day, two property inspectors walked into my studio while I was at my computer in my pajamas. Their records said nobody lived there.&lt;/p&gt;

&lt;p&gt;Once, when I was locked out of my flat, the agent on the phone wouldn't help me, because someone else was registered at my address under a different name.&lt;/p&gt;

&lt;p&gt;A deposit replacement scheme was about to take its once-a-year fee from me twice. After I canceled the Direct Debit, an automated email warned that I might have to pay twice, because more than one person was on my tenancy.&lt;/p&gt;

&lt;p&gt;I live alone. The "other person" is a record nobody corrected.&lt;/p&gt;

&lt;h3&gt;
  
  
  The emails I had to silence
&lt;/h3&gt;

&lt;p&gt;For about a year and a half, the new company's system sent me automated emails asking me to set up a Direct Debit, often several times a week. I asked many times for them to stop. There was no unsubscribe link.&lt;/p&gt;

&lt;p&gt;In the end, I blocked the sender so my main inbox could stay usable for the things that matter. Later, I learned that the same system also sends maintenance updates. To protect myself from the noise, I may have lost the signal.&lt;/p&gt;

&lt;p&gt;That trade should never be the user's to make.&lt;/p&gt;

&lt;h3&gt;
  
  
  The rejection that arrived in a minute
&lt;/h3&gt;

&lt;p&gt;My passport is due for renewal, so I applied online through my home country's service. I was rejected five times. Each rejection arrived about a minute after I submitted, with identical wording and no reason.&lt;/p&gt;

&lt;p&gt;Staff on the phone said my biometric data needed updating. I made the long, expensive journey to the consulate in London, and the officer at the counter said my biometric data was valid. Nobody could explain the rejections.&lt;/p&gt;

&lt;p&gt;My best guess is that the first rejection left a flag on my record, and that flag rejects every new attempt automatically until a human clears it. But that is only a guess, because no one has looked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern
&lt;/h2&gt;

&lt;p&gt;Put those four stories side by side, and the same shape appears:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A system holds a record or makes a decision.&lt;/li&gt;
&lt;li&gt;The record is wrong, or the decision comes with no reason.&lt;/li&gt;
&lt;li&gt;Nobody inside the organization owns the error.&lt;/li&gt;
&lt;li&gt;The person is left to notice it, prove it and chase it, often while being treated as the problem.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;None of these failures is complicated to fix. Route a report to the right team. Update a name when a deed poll arrives. Add an unsubscribe link. Attach a reason to a rejection.&lt;/p&gt;

&lt;p&gt;Each fix is small. What makes the failures heavy is that they never end, and they never stay inside one institution.&lt;/p&gt;

&lt;h2&gt;
  
  
  There's a name for this
&lt;/h2&gt;

&lt;p&gt;Researchers call it &lt;strong&gt;administrative burden&lt;/strong&gt;. Donald Moynihan, Pamela Herd and Hope Harvey describe it as three kinds of cost that people carry when they deal with institutions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learning costs:&lt;/strong&gt; working out what is required of you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance costs:&lt;/strong&gt; the forms, documents, calls and waiting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Psychological costs:&lt;/strong&gt; the stress, frustration and stigma.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Their work makes two points that match my life closely. These costs fall unevenly, and they land hardest on people with the least time, money and capacity to absorb them. And how much burden a person carries is often a matter of choice, not accident, even when nobody sits down and decides it.&lt;/p&gt;

&lt;p&gt;Disability multiplies all three costs. If your memory is unreliable, keeping records is harder. If phone calls are difficult, every escalation costs more. If you are already exhausted, each new failure arrives when you have the least to give.&lt;/p&gt;

&lt;h2&gt;
  
  
  It isn't just me
&lt;/h2&gt;

&lt;p&gt;At national scale, the same shape has caused enormous harm.&lt;/p&gt;

&lt;p&gt;In the UK, the Post Office prosecuted more than 700 subpostmasters between 1999 and 2015, using figures from its Horizon accounting system that showed money missing. In 2019, the High Court found that Horizon contained bugs, errors and defects capable of causing those shortfalls. In 2024, new laws quashed the convictions. For years, people were told the computer was right and they were wrong.&lt;/p&gt;

&lt;p&gt;In Australia, the Robodebt scheme ran from 2015 to 2019. It averaged people's annual income to calculate debts, and it raised about 1.73 billion Australian dollars in unlawful debts against more than 400,000 people. The job of proving whether a debt existed moved onto the people receiving the letters, often with income evidence from years before. The Royal Commission called it "a crude and cruel mechanism, neither fair nor legal."&lt;/p&gt;

&lt;p&gt;In the Netherlands, the tax authority wrongly accused tens of thousands of parents of childcare benefit fraud and demanded repayments that drove families into hardship. Dual nationality was used as one of its risk indicators. The government resigned over it in January 2021, and more than 33,000 people have since been officially recognized as victims.&lt;/p&gt;

&lt;p&gt;Each case has the same core. The system's record is treated as the truth, and the person has to prove otherwise, with documents nobody told them to keep.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;The cost I feel most is not the hours. It's the vigilance.&lt;/p&gt;

&lt;p&gt;I find myself trying to predict where the next failure will come from, so I can keep the right records before I need them. When a failure does arrive, I often don't have the capacity to act on it, or the document I need is long gone.&lt;/p&gt;

&lt;p&gt;It is humiliating to explain the same thing again and again to people who have the records in front of them. It is heartbreaking to be treated as an inconvenience for asking for something basic, like heat in October.&lt;/p&gt;

&lt;p&gt;And it accumulates. Housing, banking, benefits, government, healthcare: every system I am registered with is another place where a wrong record can sit for years, waiting for me to find it.&lt;/p&gt;

&lt;p&gt;The only reason I can keep up at all is that I use AI tools to hold the threads for me: drafting the complaint, tracking the deadlines, remembering what I told whom. That is an accessibility tool, the same as glasses. But it shouldn't be the price of being treated fairly.&lt;/p&gt;

&lt;h2&gt;
  
  
  I am not an archivist
&lt;/h2&gt;

&lt;p&gt;I am not an archivist. I'm not a lawyer or a detective, either. I'm a person who needs heat, a correct name on a record, and an inbox I can use.&lt;/p&gt;

&lt;p&gt;I need to be able to live my life without preparing a defense for every ordinary interaction.&lt;/p&gt;

&lt;p&gt;When software fails, someone carries the cost. Right now, it is usually the person with the least capacity to carry it. The responsibility belongs with the people who build, buy and run these systems.&lt;/p&gt;

&lt;p&gt;Software should never fail a person in silence. And it should never leave that person to clean up after it alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Moynihan, D., Herd, P. and Harvey, H. (2015). &lt;a href="https://doi.org/10.1093/jopart/muu009" rel="noopener noreferrer"&gt;Administrative Burden: Learning, Psychological, and Compliance Costs in Citizen-State Interactions&lt;/a&gt;. &lt;em&gt;Journal of Public Administration Research and Theory&lt;/em&gt;, 25(1).&lt;/li&gt;
&lt;li&gt;Herd, P. and Moynihan, D. (2018). &lt;em&gt;Administrative Burden: Policymaking by Other Means&lt;/em&gt;. Russell Sage Foundation.&lt;/li&gt;
&lt;li&gt;Bates v Post Office Ltd (No 6: Horizon Issues) [2019] EWHC 3408 (QB).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.legislation.gov.uk/ukpga/2024/14/contents" rel="noopener noreferrer"&gt;Post Office (Horizon System) Offences Act 2024&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Royal_Commission_into_the_Robodebt_Scheme" rel="noopener noreferrer"&gt;Royal Commission into the Robodebt Scheme&lt;/a&gt; (final report, July 2023).&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://en.wikipedia.org/wiki/Dutch_childcare_benefits_scandal" rel="noopener noreferrer"&gt;Dutch childcare benefits scandal&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;DutchNews (2024). &lt;a href="https://dutchnews.nl/2024/01/over-33000-families-acknowledged-as-benefit-scandal-victims" rel="noopener noreferrer"&gt;Over 33,000 families acknowledged as benefit scandal victims&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>automation</category>
      <category>ethics</category>
      <category>discuss</category>
    </item>
    <item>
      <title>A Label Is Not a Review: Following One Closed Merge Request</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Tue, 06 Oct 2026 19:44:18 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/a-label-is-not-a-review-following-one-closed-merge-request-25a2</link>
      <guid>https://dev.to/dobybaxter127/a-label-is-not-a-review-following-one-closed-merge-request-25a2</guid>
      <description>&lt;p&gt;This is a follow-up to two earlier pieces: &lt;a href="https://dev.to/dobybaxter127/why-this-time-is-different-following-the-chain-of-consequences-266d"&gt;Why This Time Is Different: Following the Chain of Consequences&lt;/a&gt; and &lt;a href="https://dev.to/dobybaxter127/learning-to-build-without-a-ladder-4bmm"&gt;Learning to Build Without a Ladder&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the first one, I listed the assumptions that let the tech industry recover from past downturns. The last one was the easiest to miss: &lt;strong&gt;there was a way in.&lt;/strong&gt; Someone outside the industry could still reach a person who would let them in, and open source was one of the places where that still worked. I wrote about how automated gates on both sides are closing that door.&lt;/p&gt;

&lt;p&gt;This week I found an example that shows the door can close without any automation at all. A person closed it, with a quick look and one word. Human judgment is still one of the biggest gates in front of people who already have the skills, and a quick human check can fail in exactly the same way an automated filter does.&lt;/p&gt;

&lt;p&gt;So I want to follow this one the same way I followed the chain in that article: one link at a time, looking at what each step actually cost.&lt;/p&gt;

&lt;p&gt;I'm not naming the project or the people involved. Its maintainers do real, careful work, much of it for the public good, and nothing here is meant as a pile-on. I'll call the project SkyRelay. It's an open-source network of volunteer satellite ground stations, the kind of project I love, and one I was seriously considering contributing to.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happened
&lt;/h2&gt;

&lt;p&gt;At the end of February 2026, a contributor opened a merge request to speed up one of SkyRelay's busiest pages. The page was slow to load, and the change went after the reasons why.&lt;/p&gt;

&lt;p&gt;A maintainer marked it as a draft. The comment said that a quick check showed signs the merge request was AI-produced, and that it was being set aside "in order to avoid spending review time for AI nonsense."&lt;/p&gt;

&lt;p&gt;To be fair to the maintainer, the comment didn't stop there. It apologized in case the guess was wrong, and asked for three things: describe the planned optimization on the related issue, make sure nothing that already works stops working, and run the pipeline checks locally first.&lt;/p&gt;

&lt;p&gt;The next day, the contributor closed the merge request. They also closed a second, much smaller fix they had opened the same day, which nobody had raised any concern about. They haven't opened another merge request in the project since.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually in the change
&lt;/h2&gt;

&lt;p&gt;I wanted to know whether the label fit, so I went through the diff file by file with my AI assistant.&lt;/p&gt;

&lt;p&gt;It was a real fix for a real problem. The page got its data from a list of satellites that made a separate database query for every single satellite, just to check one flag. The change replaced those thousands of queries with one. It also changed a large table so it loaded its rows on demand, instead of building every row into the page up front.&lt;/p&gt;

&lt;p&gt;It also had noise around it, the kind that needs tidying before anything is merged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A code style rule was switched off, instead of the new code being changed to follow it.&lt;/li&gt;
&lt;li&gt;A few package lock entries appeared that had nothing to do with the change.&lt;/li&gt;
&lt;li&gt;The pipeline failed.&lt;/li&gt;
&lt;li&gt;It changed how the page loads without that approach being agreed on the issue first.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All of that deserves a review comment. None of it is unusual. It's what a merge request looks like when someone is working fast, on their own machine, without asking first. A reviewer could reasonably have said "this needs work before it can be merged." That is a very different sentence from "this is nonsense."&lt;/p&gt;

&lt;p&gt;Then I found something I hadn't expected. This wasn't a newcomer at all. The same person had more than twenty merged contributions across the project's organization a few years earlier, including an earlier speed-up of this very page. A quick check didn't show that. Only a closer look did.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mine had signs too
&lt;/h2&gt;

&lt;p&gt;I recognized that merge request, because I've sent one like it.&lt;/p&gt;

&lt;p&gt;That same week, I was finishing a documentation contribution to GitLab. I was teaching myself Git from the command line, and I was working on three issues at the same time. Changes from my other issues kept slipping into the merge request. I couldn't see it from the terminal. It only showed up in the merge request's diff, after I had already pushed. It happened three pushes in a row.&lt;/p&gt;

&lt;p&gt;On top of that, my editor, Zed, quietly reformatted lines I had never touched whenever I saved a documentation file. I didn't see that coming at all, and I had to work out on my own where the extra changes were coming from.&lt;/p&gt;

&lt;p&gt;From a quick check, my merge request had signs too: unrelated files, formatting churn, a messy history. My reviewer looked past them. He stayed kind and waited for my final version, even though I could tell his patience was wearing thin, and I don't blame him for that. In the end I closed the merge request myself and opened a brand-new one with only the change I meant to make. It was merged a few days later and shipped in GitLab 18.10.&lt;/p&gt;

&lt;p&gt;The only real difference between my story and the SkyRelay contributor's is what the reviewer did with the signs. Mine read them as a person still learning. Theirs read them as a reason to stop reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the door stays open
&lt;/h2&gt;

&lt;p&gt;A few weeks later, I had the opposite experience, in a much harder spot.&lt;/p&gt;

&lt;p&gt;I was adding Kroki support to the GitLab Terraform provider, so teams who manage GitLab as code could switch on diagram rendering in their configuration instead of by hand. The provider is written in Go, a language I was still finding my feet in, and it took me three merge requests to get it right.&lt;/p&gt;

&lt;p&gt;The hardest part wasn't the code. My laptop at the time had 8 GB of RAM. Docker crashed it, WSL wouldn't start, and the project expects its documentation to be regenerated by a tool, not written by hand. I tried writing the docs manually anyway, and the pipeline kept failing, as it should.&lt;/p&gt;

&lt;p&gt;So I said exactly that, in the merge request. I explained my machine's limits, said I believed the implementation itself was complete, and asked whether someone could run the generation step for me or suggest another way.&lt;/p&gt;

&lt;p&gt;They did both. One maintainer explained that the docs step didn't need Docker at all, and pointed me to the command contributors are meant to run before opening a merge request. Another ran it for me, pushed the result, and suggested a remote development environment for next time. Later, when my diffs looked odd, they asked me to run Go's formatter, because my tabs and spaces were mixed up. Formatting again!&lt;/p&gt;

&lt;p&gt;With that help, I fixed a state drift problem that the acceptance tests showed, added a small safety check I noticed along the way, and traced the last pipeline failure to a mismatched file name in the CI setup rather than my change. In April 2026 it went onto the merge train and was merged. The request for that feature had been open for more than two years.&lt;/p&gt;

&lt;p&gt;I was so happy that I drew Kroki as a little crocodile riding the merge train, and shared it in the thread. Both of my Kroki drawings are in &lt;a href="https://gitlab-work-81a966.gitlab.io/contributions/kroki.html" rel="noopener noreferrer"&gt;my writeup of the contribution&lt;/a&gt;. And someone who had been waiting for the feature thanked me for my persistence and said they were looking forward to the release. I still smile about that.&lt;/p&gt;

&lt;p&gt;Nothing about that merge request was tidy. It took three attempts, it had a failing pipeline and mixed-up formatting, and I said openly that I couldn't run the project's own tooling. From a quick check, it had more signs than either of the other merge requests in this article. What it also had was transparency in both directions: I was honest about my limits, and the maintainers were honest about what I needed to do. That's all it took.&lt;/p&gt;

&lt;p&gt;That is the nature of this work. Technology moves fast, tools change, environments fail, and troubleshooting takes time, whether you started last month or twenty years ago. Most of software engineering is exactly this: troubleshooting, debugging, and trying again. A messy merge request isn't evidence that someone doesn't belong. It's evidence that they're doing the job.&lt;/p&gt;

&lt;p&gt;And a reviewer can't see who is on the other side of a merge request. It might be a newcomer, someone with years of history in the project, someone on an 8 GB laptop, or someone learning a new language or a new tool. That's exactly why the change is the right thing to look at. Nothing about what happened in SkyRelay was personal to the contributor. The label is what made it personal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same chain, in one merge request
&lt;/h2&gt;

&lt;p&gt;Here is how I see the steps link together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A quick check stands in for a review.&lt;/strong&gt; Maintainers are flooded, and a fast filter feels like self-defense. But a quick check is a classifier, and like any classifier it has false positives. The signs it looks for (noise, unrelated files, a failing pipeline) are exactly the signs of someone learning, someone in a hurry, someone on limited hardware, or someone whose tools did something they didn't expect.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The label lands on the person, not the change.&lt;/strong&gt; "This needs work" tells you what to fix. "AI nonsense" tells you what someone thinks of you. Only the first one gives you anything to act on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The person leaves, quietly.&lt;/strong&gt; Nobody argued and nobody complained. The merge request was simply closed, and nothing anywhere records that a working fix walked out the door. In the first article I called this Link 6: failure goes quiet. It's the same silence, just in a review thread instead of on a dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem stays, and costs more later.&lt;/strong&gt; In October 2026, seven months on, another maintainer opened a new merge request for the same page's slow loading. It's careful, well-measured work, and I'm glad it's happening. One of the causes it names is that same per-satellite query, and the part it doesn't cover yet is listed as a follow-up, measured at over nine seconds on production-sized data. The closed merge request had already offered an answer to that query. A review would have been a chance to learn from it, or to explain why it didn't fit. That's Link 7: the correction comes late, and it costs more.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The way in narrows.&lt;/strong&gt; Every newcomer who reads that thread learns something before they write a single line: the first response here might be a label. I know, because I read it, and I decided not to contribute. That's assumption F failing, one person at a time.&lt;/p&gt;

&lt;p&gt;Notice that no step in this chain needed bad intentions. Each one makes sense on its own. That's exactly why it's worth writing down.&lt;/p&gt;

&lt;h2&gt;
  
  
  "No capacity" is a complete answer
&lt;/h2&gt;

&lt;p&gt;I understand where maintainers are. Many of them work unpaid, or look after far more code than any one person should. Reviewing a large change properly takes real time, and nobody is owed that time.&lt;/p&gt;

&lt;p&gt;So here is what I'd ask for instead, and it's small: if there isn't capacity to review, say that. It's honest, it's kind, and it's a complete answer. Something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We don't have review capacity for a change this size right now. Could you describe the plan on the issue first, so we can agree on the approach before you spend more time on it?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That reply takes the same few seconds as a label. It keeps the person, keeps the fix, and keeps the door open. And the second half of the SkyRelay comment was already most of the way there. If those three requests had been the whole comment, I don't think this article would exist.&lt;/p&gt;

&lt;p&gt;Since then, SkyRelay has published an AI policy for contributors. It says plainly that the project can't police how a merge request was written, so it doesn't try. It allows AI-assisted contributions, as long as a person understands the change and takes full responsibility for it, and it's firm about repeated low-quality submissions, which is fair. I think that's the right answer. It moves the question from "how was this written?" to "do you understand it, and do you stand behind it?", and the second question is one a person can actually answer. A first response built on that question would have gone very differently.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a review gives that a label can't
&lt;/h2&gt;

&lt;p&gt;In Learning to Build Without a Ladder, I wrote that open source still works as an apprenticeship. You send something small, someone reviews it and tells you plainly what is wrong, and you improve. For a lot of us, that loop is how we became engineers after the junior roles disappeared.&lt;/p&gt;

&lt;p&gt;A label skips the one step that makes the loop work. A review tells you the truth about your code. A label tells you someone's guess about you.&lt;/p&gt;

&lt;p&gt;For people who learn the way I do, self-taught, neurodivergent, building our own apprenticeship in public, early merge requests are often noisy. That noise is what learning looks like from the outside. If the first response to it is a label, the people who most need that apprenticeship are the first ones it turns away.&lt;/p&gt;

&lt;p&gt;I also wrote that judgment is the skill that matters now: can a person understand, test, and improve what a tool produced. I still believe that, and I've realized it applies to reviewers as much as to contributors. What a reviewer can answer is whether the change works, and whether the person behind it can explain it. Whether a tool was involved is something nobody can see from a diff, and it was never the most useful question to ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I build my work around one line: software should never fail a person in silence. A merge request closed under a label is a quiet failure of exactly that kind. Nobody sees an error message. A person just stops coming back, and the problem they solved waits seven months for someone else.&lt;/p&gt;

&lt;p&gt;I've seen both sides of that door this year. When a maintainer reads the change and asks what the person needs, a feature people waited two years for gets merged. When a label comes first, a working fix walks away.&lt;/p&gt;

&lt;p&gt;Reviewing the change instead of the person isn't glamorous, and it isn't free. But it's the cheapest way I know to keep the door open, for the next contributor, and for the people who will look after these projects once today's maintainers step back.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>ai</category>
      <category>career</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Built to Stop Bots, Built to Stop Me: How CAPTCHAs Fail the Humans They Claim to Protect</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Mon, 05 Oct 2026 18:21:53 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/built-to-stop-bots-built-to-stop-me-how-captchas-fail-the-humans-they-claim-to-protect-e3b</link>
      <guid>https://dev.to/dobybaxter127/built-to-stop-bots-built-to-stop-me-how-captchas-fail-the-humans-they-claim-to-protect-e3b</guid>
      <description>&lt;h2&gt;
  
  
  The gate I could not pass
&lt;/h2&gt;

&lt;p&gt;I was invited to join a community. Between me and the people who invited me stood one question: are you human?&lt;/p&gt;

&lt;p&gt;I am. I could not prove it. The images moved faster than I could track, and I failed again and again until I gave up and emailed the organizer instead.&lt;/p&gt;

&lt;p&gt;I build validation systems for a living, so this stung twice. A validator built to stop software had rejected a real person. And it was far from the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  What failing looks like
&lt;/h2&gt;

&lt;p&gt;As an autistic person with ADHD and sensory processing sensitivities, a challenge built on rapidly changing images is not a mild annoyance. It is a task designed around exactly the kind of processing I find hardest.&lt;/p&gt;

&lt;p&gt;I have failed every kind of CAPTCHA, honestly and repeatedly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Distorted text.&lt;/strong&gt; Sometimes I simply cannot tell what the word is meant to be. No amount of staring helps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image grids.&lt;/strong&gt; I select every traffic light I can see, and it is never what the system wants. Does a light cut off at the tile edge count? Sometimes yes, sometimes no, and nothing tells you the rule.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotating and moving puzzles.&lt;/strong&gt; Hard to track, and sometimes rejected even when my answer is correct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invisible scoring.&lt;/strong&gt; When it decides I look suspicious, it sends me straight back to the puzzles above.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CAPTCHAs have stopped me more times than I can count. They have never once stopped a bot on my behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  Machines now pass more easily than people
&lt;/h2&gt;

&lt;p&gt;In 2023, UC Irvine researchers had 1,400 people solve 14,000 real CAPTCHAs (&lt;a href="https://www.usenix.org/conference/usenixsecurity23/presentation/searles" rel="noopener noreferrer"&gt;USENIX Security 2023&lt;/a&gt;). Bots beat humans on both speed and accuracy across many types. On distorted text, bots were close to 100% accurate in under a second, while humans managed 50% to 84% and took up to 15 seconds (&lt;a href="https://techxplore.com/news/2023-08-bots-captcha-humans.html" rel="noopener noreferrer"&gt;coverage&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;In 2024, ETH Zurich researchers solved 100% of Google's reCAPTCHAv2 image challenges with off-the-shelf object-detection models (&lt;a href="https://arxiv.org/abs/2409.08831" rel="noopener noreferrer"&gt;Breaking reCAPTCHAv2&lt;/a&gt;). What really decided the outcome was cookies and browsing history, not the puzzle.&lt;/p&gt;

&lt;p&gt;Attackers who do not want to build a solver can simply pay CAPTCHA farms, where low-paid workers solve challenges at scale. So the bot gets in. The paid stranger gets in. The person who just wants to join may not.&lt;/p&gt;

&lt;h2&gt;
  
  
  A validator with a broken oracle
&lt;/h2&gt;

&lt;p&gt;Any validator makes two kinds of mistake: letting bad input through, and rejecting good input. A CAPTCHA now does both badly. It is weak against bots and strict against people who see, move, or process differently, including blind and low-vision users, people with motor disabilities, and neurodivergent people like me.&lt;/p&gt;

&lt;p&gt;The W3C has warned about this since 2005 in &lt;a href="https://www.w3.org/TR/turingtest/" rel="noopener noreferrer"&gt;Inaccessibility of CAPTCHA&lt;/a&gt;, and &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication.html" rel="noopener noreferrer"&gt;WCAG 2.2 criterion 3.3.8&lt;/a&gt; says authentication should not depend on a puzzle without an alternative. A timed, moving puzzle is far from that spirit.&lt;/p&gt;

&lt;p&gt;That is why it feels dehumanizing. The system is asked to recognize a human, fails to recognize a real one, and then tells them they are the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Better gates
&lt;/h2&gt;

&lt;p&gt;The fix is to stop asking humans to out-perform machines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; per IP, device, and time window, since most abuse is about volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Proof of work&lt;/strong&gt; that asks the browser, not the person, to spend computation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email or magic-link verification&lt;/strong&gt; instead of perception tests.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Honeypot fields&lt;/strong&gt; that humans never see and naive bots fill in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A human fallback path&lt;/strong&gt;, a clear "I can't complete this" route to a real person.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Bolt-on fixes like hCaptcha's &lt;a href="https://www.hcaptcha.com/learning/accessibility/hcaptcha-accessibility/" rel="noopener noreferrer"&gt;accessibility cookie&lt;/a&gt; help some people, but they push extra work onto the person already locked out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Question the test
&lt;/h2&gt;

&lt;p&gt;I did get through in the end, by emailing a person. That is the lesson. A bot can now prove it is human more easily than I can. That should not make anyone question my humanity. It should make us question the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Searles et al., &lt;a href="https://www.usenix.org/conference/usenixsecurity23/presentation/searles" rel="noopener noreferrer"&gt;An Empirical Study &amp;amp; Evaluation of Modern CAPTCHAs&lt;/a&gt;, USENIX Security 2023&lt;/li&gt;
&lt;li&gt;Tech Xplore, &lt;a href="https://techxplore.com/news/2023-08-bots-captcha-humans.html" rel="noopener noreferrer"&gt;Bots are better at CAPTCHA than humans, researchers find&lt;/a&gt;, August 2023&lt;/li&gt;
&lt;li&gt;Plesner, Vontobel and Wattenhofer, &lt;a href="https://arxiv.org/abs/2409.08831" rel="noopener noreferrer"&gt;Breaking reCAPTCHAv2&lt;/a&gt;, ETH Zurich, 2024&lt;/li&gt;
&lt;li&gt;W3C, &lt;a href="https://www.w3.org/TR/turingtest/" rel="noopener noreferrer"&gt;Inaccessibility of CAPTCHA&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C, &lt;a href="https://www.w3.org/WAI/WCAG22/Understanding/accessible-authentication.html" rel="noopener noreferrer"&gt;Understanding SC 3.3.8: Accessible Authentication (Minimum)&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;hCaptcha, &lt;a href="https://www.hcaptcha.com/learning/accessibility/hcaptcha-accessibility/" rel="noopener noreferrer"&gt;Accessibility: Accommodation Options and Support Path&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>a11y</category>
      <category>security</category>
      <category>webdev</category>
      <category>neurodiversity</category>
    </item>
    <item>
      <title>Thank You, Every Time: What the Pyxel Team at ESA Taught Me That No Employer Ever Did</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Thu, 01 Oct 2026 23:24:22 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/thank-you-every-time-what-the-pyxel-team-at-esa-taught-me-that-no-employer-ever-did-c8c</link>
      <guid>https://dev.to/dobybaxter127/thank-you-every-time-what-the-pyxel-team-at-esa-taught-me-that-no-employer-ever-did-c8c</guid>
      <description>&lt;p&gt;Every time I sent the Pyxel team something, they said thank you.&lt;/p&gt;

&lt;p&gt;Not once at the end, as a formality. Every time. A fix, a new version of the tool I was building, an answer to a question: thank you. I want to write about that, because in all my years of working and applying for work, no employer ever thanked me for my work, and I didn't know how much I needed it until it happened.&lt;/p&gt;

&lt;p&gt;This isn't a technical post. I've written about &lt;a href="https://dev.to/dobybaxter127/learning-to-build-without-a-ladder-4bmm"&gt;what I built for Pyxel&lt;/a&gt; before. This one is about how it felt to build it, and what it taught me.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it started
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://esa.gitlab.io/pyxel/doc" rel="noopener noreferrer"&gt;Pyxel&lt;/a&gt; is an open-source detector simulation framework developed by a team at the European Space Agency. It's used by ESA, the European Southern Observatory and institutes around the world. I started the way anyone can start on an open-source project: I contributed on GitLab, and then I emailed the maintainer.&lt;/p&gt;

&lt;p&gt;I expected a short reply, maybe. What I got was an invitation to meet the whole team online. I introduced myself, and then each of them introduced themselves to me.&lt;/p&gt;

&lt;p&gt;That detail matters to me more than almost anything else in this story. It wasn't an interview, where one side asks the questions and the other side performs. It was a team meeting a new person and treating that person as someone worth knowing.&lt;/p&gt;

&lt;p&gt;They were grateful from the very first conversation. I hadn't proven anything yet. They were simply glad someone wanted to help.&lt;/p&gt;

&lt;h2&gt;
  
  
  What working together looked like
&lt;/h2&gt;

&lt;p&gt;I was building &lt;a href="https://pyxel-config-lab-ede25c.gitlab.io/" rel="noopener noreferrer"&gt;Pyxel Config Lab&lt;/a&gt;, a tool that helps people write and check Pyxel configurations without fighting the file format. The team wrote me extensive feedback on it. Not a quick "looks good", and not a list of everything wrong handed down from above. Careful, detailed notes from people who had clearly sat down with the tool and used it.&lt;/p&gt;

&lt;p&gt;Good feedback is a form of care. It says: I took your work seriously enough to spend my time on it.&lt;/p&gt;

&lt;p&gt;So I kept working. I sent the next version, read the next round of notes, and improved it again. Nobody told me the iterations were a waste of time. Iteration was the point. That's how good software gets made, and they knew it.&lt;/p&gt;

&lt;p&gt;And every time, they thanked me.&lt;/p&gt;

&lt;p&gt;I was included. I was part of their work, not a ticket to be closed. They were respectful to me as a person and as an engineer, and open to ideas that didn't start with them.&lt;/p&gt;

&lt;p&gt;The tool shipped alongside the &lt;a href="https://esa.gitlab.io/pyxel/post/2025-10-30_version_2_14/" rel="noopener noreferrer"&gt;official Pyxel 2.14 release&lt;/a&gt;. It was later named in the team's peer-reviewed &lt;a href="https://doi.org/10.1117/12.3104656" rel="noopener noreferrer"&gt;Pyxel 3.0 paper&lt;/a&gt; (Proc. SPIE 14157), which records that I led the development of Pyxel's new GUI. The maintainer also wrote me a reference, with a line in it I treasure:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Doby Baxter joined the Pyxel project on his own initiative and quickly became a key contributor."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Those are the achievements, and I'm proud of them. But they aren't what I treasure most. What I treasure is how it felt along the way. To this day, it's the best collaboration I've ever had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The other side
&lt;/h2&gt;

&lt;p&gt;Here is the comparison, as plainly as I can put it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, I emailed one person and was invited to meet the whole team. &lt;strong&gt;In job applications&lt;/strong&gt;, I mostly got automated rejections, or no reply at all, often before a person had read a word.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, the team introduced themselves to me. &lt;strong&gt;In hiring&lt;/strong&gt;, I was the only one expected to explain myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, my work got detailed feedback and a thank you. &lt;strong&gt;From employers&lt;/strong&gt;, it got silence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, iteration was welcome. &lt;strong&gt;With employers&lt;/strong&gt;, anything beyond exactly what I'd been told to do wasn't seen as care or initiative. It was a problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, I was included. &lt;strong&gt;Elsewhere&lt;/strong&gt;, I was replaceable, and so was everyone around me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;At Pyxel&lt;/strong&gt;, I was valued for what I brought. &lt;strong&gt;Elsewhere&lt;/strong&gt;, some employers were interested in my skills right up until they reached the part-time hours and adjustments I need. One role ended because adjustments that had been agreed were never put in place.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I could bring the same passion I brought to Pyxel to an employer, and it would be dismissed.&lt;/p&gt;

&lt;p&gt;Same person. Same passion. Same standard of work. Two completely different outcomes. The difference was never me. It was whether the people on the other side were open-minded or closed-minded.&lt;/p&gt;

&lt;h2&gt;
  
  
  The opposite of competition
&lt;/h2&gt;

&lt;p&gt;Corporate work taught me that work is a competition: for roles, for credit, for being seen as the one who did it.&lt;/p&gt;

&lt;p&gt;The Pyxel team taught me the opposite. Respect. Mutual care. Collaboration in the real sense of the word, where everyone's work makes everyone else's better, and people say so out loud.&lt;/p&gt;

&lt;p&gt;None of that cost them anything. Saying thank you is free. Introducing yourself is free. Reading someone's work carefully costs an hour. And it changed everything for me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's the reason I kept going
&lt;/h2&gt;

&lt;p&gt;I've stopped applying for jobs. That decision is made, and I'm at peace with it.&lt;/p&gt;

&lt;p&gt;But giving up on employment and giving up on engineering are two very different things, and the Pyxel team is the difference between them. Without them, every signal I'd received would have said the same thing: your work doesn't matter to anyone. With them, I have proof of the opposite. Good people exist. Open-minded people exist. People who will appreciate careful work, say so, and treat the person who made it with respect.&lt;/p&gt;

&lt;p&gt;I don't believe that because I hope it. I believe it because I've lived it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I carry into my own work
&lt;/h2&gt;

&lt;p&gt;That belief is what my business is built on. I now work for myself, doing reliability reviews and engineering for small businesses, and the clients I hope to work with are people like the Pyxel team: open, respectful, and glad when something is done with care.&lt;/p&gt;

&lt;p&gt;It goes both ways. I try to work the way they worked with me. Thank people for their time. Give feedback that shows I actually read the thing. Explain, and never talk down. Treat someone's system, and someone's work, as theirs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thank you
&lt;/h2&gt;

&lt;p&gt;So, to the Pyxel team at ESA: thank you. For the welcome, the feedback, the patience, and every single thank you. You didn't just help me build a tool. You showed me what working together is supposed to feel like, and you gave me a reason to keep going. I hope everyone gets to work with people like you at least once.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>career</category>
      <category>community</category>
      <category>space</category>
    </item>
    <item>
      <title>Why This Time Is Different: Following the Chain of Consequences</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Tue, 29 Sep 2026 23:49:34 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/why-this-time-is-different-following-the-chain-of-consequences-266d</link>
      <guid>https://dev.to/dobybaxter127/why-this-time-is-different-following-the-chain-of-consequences-266d</guid>
      <description>&lt;p&gt;I have been thinking a lot about where software engineering is heading, and I keep arriving at the same conclusion: this time is different.&lt;/p&gt;

&lt;p&gt;I know how that sounds. Whenever people worry about the tech industry, the usual reassurance is that we have been here before. There was the dot-com crash, then the financial crisis, and both times the industry recovered and came back bigger.&lt;/p&gt;

&lt;p&gt;I do not think that reassurance holds anymore, and I want to show why rather than just say it. When I want to rely on a system, I list what it depends on and check whether each dependency still holds. So that is how I approach this. First I describe how I see past recoveries working, then I follow what is happening now one link at a time, and look at which of those dependencies are still in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the old recovery loop worked
&lt;/h2&gt;

&lt;p&gt;The way I see it, past downturns followed a recognizable sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Demand fell, or money got expensive.&lt;/li&gt;
&lt;li&gt;Companies reduced staff to match.&lt;/li&gt;
&lt;li&gt;The people who left kept their skills, and many stayed close to the industry.&lt;/li&gt;
&lt;li&gt;Demand returned.&lt;/li&gt;
&lt;li&gt;Companies rehired, and because the work still needed juniors, juniors were trained again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It was painful, but it was a loop, and loops correct themselves. For this one to close, four things had to be true:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A. The cut was about demand, not about the work.&lt;/strong&gt; The jobs were paused, not retired. When demand came back, the roles came back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;B. Knowledge survived the cut.&lt;/strong&gt; It lived in people who could be rehired, or who could teach.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C. The training pipeline restarted.&lt;/strong&gt; Seniors taught juniors on real work, because real work still had to be done by people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;D. Failure was loud.&lt;/strong&gt; When a company cut too deep, something visibly stopped working, and that signal reached the people who make budget decisions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Underneath all four sits a quieter fifth assumption:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E. Change moved at human speed.&lt;/strong&gt; Systems changed about as fast as people could understand the changes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And a sixth, about the people outside the loop, which is easy to miss because it held for so long:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;F. There was a way in.&lt;/strong&gt; Someone outside the industry could still reach a person who would let them in: an entry-level role, an education that led to one, a maintainer with time to mentor.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the chain. Each link below is a decision or a consequence, and each one touches at least one of these assumptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 1: The cut is framed as substitution, not demand
&lt;/h2&gt;

&lt;p&gt;Some companies now explain workforce reductions with a new reason. Not "demand has fallen", but "this work will be done by AI."&lt;/p&gt;

&lt;p&gt;That changes what the cut means. A demand cut carries an implicit plan to rehire when demand returns. A substitution cut carries the opposite plan: the role is considered finished. There is no condition under which it comes back, because the reasoning never depended on demand in the first place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the trigger that used to reopen roles is gone. Assumption A no longer holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 2: Substitution starts with the entry-level work
&lt;/h2&gt;

&lt;p&gt;Which tasks are easiest to hand to an AI tool? Well-defined ones. Boilerplate, small fixes, writing tests, updating documentation, triaging tickets, first drafts of configuration.&lt;/p&gt;

&lt;p&gt;Those are exactly the tasks juniors used to learn on. They were never only output. They were the training ground: small enough to be safe, real enough to teach how the whole system fits together.&lt;/p&gt;

&lt;p&gt;Nobody has to decide to stop training people for training to stop. It is enough to automate the work that the training happened inside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the apprenticeship layer thins out as a side effect. Assumption C no longer holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 3: Knowledge leaves, and it was never written down
&lt;/h2&gt;

&lt;p&gt;Every long-lived system carries knowledge that is not in the code. Someone knows that an odd-looking setting is deliberate. Someone remembers the incident that explains why a particular validation exists. Someone knows which customer workflow cannot tolerate a certain kind of change.&lt;/p&gt;

&lt;p&gt;That knowledge is part of the system. It just is not stored anywhere a tool can read.&lt;/p&gt;

&lt;p&gt;In a demand cut, that knowledge sat in people who could be hired back. In a substitution cut, fewer seniors remain, no juniors are absorbing what they know, and the tools that replaced them can only read what was written. What was never written is simply gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the system now encodes more about itself, in behavior nobody can explain, than its operators know about it. Assumption B weakens sharply.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 4: The rate of change goes up
&lt;/h2&gt;

&lt;p&gt;Automation is very good at producing change. An agent can open more pull requests, generate more configuration and apply more fixes in a day than a small team could in a week.&lt;/p&gt;

&lt;p&gt;So at the same moment that understanding of the system is shrinking (Links 2 and 3), the amount of change flowing into it is growing. The ratio of changes to people who understand those changes moves in the wrong direction from both ends.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; systems start changing faster than anyone can follow. Assumption E no longer holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 5: Errors travel instead of stopping
&lt;/h2&gt;

&lt;p&gt;This is the technical heart of the chain.&lt;/p&gt;

&lt;p&gt;When traditional software fails, it usually fails in a recognizable way. It throws an exception, crashes, times out, or returns something malformed that a type check or a schema rejects. The failure stops at a boundary.&lt;/p&gt;

&lt;p&gt;A language model fails differently. Its wrong answers are well-formed, confident and plausible. They have the right shape, so any check that only looks at shape lets them through.&lt;/p&gt;

&lt;p&gt;Now put several automated steps in a row. Step one makes a small contextual mistake. Step two treats step one's output as fact, because it looks like fact. Step three builds on step two. By the end, a misunderstanding has become state: records written, decisions taken, configuration applied.&lt;/p&gt;

&lt;p&gt;Distributed systems have always had cascading failures. What is new is a participant whose mistakes look exactly like its successes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; a small error in context becomes a system-wide error in state, and nothing along the way objects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 6: Failure goes quiet
&lt;/h2&gt;

&lt;p&gt;Put Links 3, 4 and 5 together.&lt;/p&gt;

&lt;p&gt;Fewer people know what correct looks like. Change arrives faster than they can review it. And the component producing the errors makes them look correct.&lt;/p&gt;

&lt;p&gt;The result is not an outage. Outages are loud, and loud is useful: an outage is a signal that travels all the way up to whoever controls the budget. The result is drift. A record that is slightly wrong. A customer who is quietly mis-served and leaves without saying why. A report that is off by an amount nobody thinks to check.&lt;/p&gt;

&lt;p&gt;From the outside, the organization looks fine. The dashboards are green. So the conclusion drawn is that the cuts worked.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the signal that used to tell a company it had cut too deep never arrives. Assumption D no longer holds.&lt;/p&gt;

&lt;p&gt;To me, this is the most important link. The old recovery loop never depended on companies being wise. It depended on failure being visible. Take away visibility and the loop has nothing to respond to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 7: The correction comes late, and it costs more
&lt;/h2&gt;

&lt;p&gt;Drift does surface eventually. A regulator asks a question, an audit finds numbers that do not reconcile, or a customer raises something that cannot be ignored.&lt;/p&gt;

&lt;p&gt;At that point the organization needs people who understand the system. The ones who did have moved on. The juniors who would have become those people were never hired, and it takes years to grow an engineer who can reason about a system end to end. That capability cannot be bought back quickly, and what can be brought in from outside comes at a premium.&lt;/p&gt;

&lt;p&gt;So the correction is delayed twice: once by the time it takes a silent failure to become visible, and again by the time it takes to rebuild the skills to fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the recovery, when it comes, is slower and more expensive than the savings that started the chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Link 8: Automation answers automation
&lt;/h2&gt;

&lt;p&gt;Go back to Links 1 and 2, and look at them from outside the industry.&lt;/p&gt;

&lt;p&gt;The roles people used to enter through are being retired. Graduates finish degrees that no longer lead anywhere in particular. People who were laid off are looking at the same shrinking list. The old promise, that a good education and real skills lead to a stable career, no longer holds for a great many people who did everything it asked.&lt;/p&gt;

&lt;p&gt;And the advice they all hear is the same: apply widely, and build experience in open source.&lt;/p&gt;

&lt;p&gt;Meanwhile, the gates themselves have been automated. Applications are filtered by software before a person sees them. Many public services decide by form logic, with nobody to explain a rejection. As a disabled person I have met many gates like that, from government forms to CAPTCHAs, and I know how much each attempt costs when there is no person to reach.&lt;/p&gt;

&lt;p&gt;So people respond in the only way that scales: with automation of their own. Applications are generated and sent everywhere. Pull requests are generated and opened everywhere. Most of this is not malicious. It is a reasonable response to a gate nobody can talk to.&lt;/p&gt;

&lt;p&gt;Then the other side responds in kind. One HR person, now doing the work of a whole department, cannot read a thousand generated applications, so the screening becomes more automated. A volunteer maintainer, often doing the work of ten people for free, cannot review a flood of generated pull requests by hand, so review gets AI help too.&lt;/p&gt;

&lt;p&gt;Several open source projects now say outright that they cannot tell how a contribution was written. What they ask for instead is a person who understands the change and stands behind it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.pyopensci.org/blog/pycon-us-2026-generative-ai-open-source.html" rel="noopener noreferrer"&gt;pyOpenSci's write-up of PyCon US 2026&lt;/a&gt; describes the same pressure from the maintainers' side: reviewer burden, burnout, and contributor pipelines under strain. Amanda Casari's closing keynote, citing Charity Majors, put it in one line: "Disposable code is here to stay, but durable code is what runs the world."&lt;/p&gt;

&lt;p&gt;I agree with both halves. Disposable code is not the problem. The problem is that durable code needs people who understand it, and this loop wears out the people who do while making it harder for new ones to get in.&lt;/p&gt;

&lt;p&gt;Each step makes sense on its own. Together they form a loop that nobody chose and nobody wins: the more each side automates, the less either side reaches a person.&lt;/p&gt;

&lt;p&gt;What is new here is the timing. In earlier downturns, someone shut out of one industry usually had another way in: a different sector that was hiring, a qualification that still led to work, an entry-level role somewhere else. This time the same pressures are arriving in every sector and in every country at once, so there is nowhere obvious to wait it out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; the doors that used to let new people in now sit behind automation on both sides, and the human contact that made them work is disappearing. Assumption F no longer holds.&lt;/p&gt;

&lt;h2&gt;
  
  
  The chain at a glance
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Assumption the old loop relied on&lt;/th&gt;
&lt;th&gt;Past downturns&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;A. Cuts follow demand, so roles return&lt;/td&gt;
&lt;td&gt;Largely held&lt;/td&gt;
&lt;td&gt;Roles framed as retired&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B. Knowledge survives in people&lt;/td&gt;
&lt;td&gt;Largely held&lt;/td&gt;
&lt;td&gt;Leaves, and tools cannot read what was never written&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C. Training restarts&lt;/td&gt;
&lt;td&gt;Held, after a dip&lt;/td&gt;
&lt;td&gt;The training ground itself is automated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;D. Failure is loud&lt;/td&gt;
&lt;td&gt;Largely held&lt;/td&gt;
&lt;td&gt;Plausible errors produce drift, not outages&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E. Change moves at human speed&lt;/td&gt;
&lt;td&gt;Held&lt;/td&gt;
&lt;td&gt;Change outpaces understanding&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;F. There is a way in that reaches a person&lt;/td&gt;
&lt;td&gt;Largely held&lt;/td&gt;
&lt;td&gt;Gates automated on both sides&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So when someone tells me "we have been here before," this is my answer. In earlier downturns, the thing that shrank was demand. The capability to do the work stayed intact, waiting for demand to return. This time the capability itself is being taken apart, and the mechanism that would normally tell us to rebuild it is going quiet at the same time. Even the way back in, for the people who could help rebuild it, now runs through automated gates.&lt;/p&gt;

&lt;h2&gt;
  
  
  What would prove me wrong
&lt;/h2&gt;

&lt;p&gt;This is my opinion, and I would rather name where it could be wrong than pretend it is certain. Here is where I could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If organizations write down their implicit knowledge, Link 3 weakens considerably, and AI tools get the context they currently lack.&lt;/li&gt;
&lt;li&gt;If companies deliberately keep hiring juniors and give them meaningful work, Link 2 no longer follows from Link 1.&lt;/li&gt;
&lt;li&gt;If failures in AI-assisted systems are made visible by design, Link 6 does not happen, and the old feedback loop returns.&lt;/li&gt;
&lt;li&gt;If automated gates explain themselves, with a reason a person can act on and a route to a human when the gate cannot help, Link 8 loses most of its pull. People mostly turn to automation when nothing answers them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice something about that list. None of it depends on AI getting smarter. Every item is a decision about the system around the AI. That is the part I find genuinely hopeful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where engineers can repair the chain
&lt;/h2&gt;

&lt;p&gt;Every link above is something engineering already knows how to address. None of it is glamorous, and all of it matters more now than it did before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep people learning inside the loop (Link 2).&lt;/strong&gt; Reviewing AI output is real engineering work, and it teaches. A junior who reviews each change an agent proposes, with a senior reviewing that review, can learn a system faster than one who spent a year writing boilerplate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write down the why (Link 3).&lt;/strong&gt; Decision records, runbooks, and comments that explain intent rather than restate the code. Written context outlives the person who held it, and it has a second benefit: it is exactly the context an AI tool needs to stop making suggestions that are locally reasonable and globally wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Put deterministic boundaries around probabilistic parts (Links 4 and 5).&lt;/strong&gt; Validate meaning, not only shape. Run preflight checks before a change is applied. Keep human approval for consequential actions: production infrastructure, financial records, anything a person relies on. The architecture should always know the difference between "the AI suggested this" and "the system has verified this is safe to do." Those are two completely different statements.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make failure loud again (Link 6).&lt;/strong&gt; Every failure should produce a signal a person can read and act on: a clear message, a logged reason, an alert that reaches someone. This is the standard I build my own work around. I call it No Silent Failure, and it comes down to one line: software should never fail a person in silence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep a person reachable (Link 8).&lt;/strong&gt; A rejection with no reason is a silent failure too, just one that lands on a person. Every automated gate, whether it screens applications, forms or pull requests, should say why it said no and offer a route to a human when it cannot help. Open source projects are already showing one way to do this: a published contribution policy that says clearly what is welcome tells people how to reach a reviewer, instead of leaving them to guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;AI is often described as a force arriving from outside and changing everything it touches. I think it is closer to a mirror with an amplifier attached. Give it a well-run organization, with good tests, written context, sensible permissions and careful review, and it amplifies all of that. Give it an organization reducing costs faster than it can understand its own systems, and it amplifies that instead.&lt;/p&gt;

&lt;p&gt;This time is different, but not because the technology is so much smarter. It is different because the loop that used to correct our mistakes depended on four things: people who understood the systems, a pipeline that produced more of them, failures loud enough to notice, and a way in that reached a person. The current path is removing all four at once.&lt;/p&gt;

&lt;p&gt;The good news is that each of those is something we know how to build. It is careful, unglamorous work, and I think it is the most important engineering work of the next few years.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Odoo 19 Quietly Drops Your _sql_constraints: How to Find Them Before Your Data Does</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:00:57 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/odoo-19-quietly-drops-your-sqlconstraints-how-to-find-them-before-your-data-does-3jeb</link>
      <guid>https://dev.to/dobybaxter127/odoo-19-quietly-drops-your-sqlconstraints-how-to-find-them-before-your-data-does-3jeb</guid>
      <description>&lt;p&gt;Odoo 19 changed how a model declares a database constraint, and it made the change quietly. A module that still uses &lt;code&gt;_sql_constraints&lt;/code&gt; keeps installing, keeps updating and keeps passing its tests. The only sign is one warning in the server log, and the rule the module declared is no longer enforced by anything.&lt;/p&gt;

&lt;p&gt;That is the kind of failure I care most about: nothing breaks on the day, and the cost turns up months later as duplicate records nobody can explain. This post walks through what the 19.0 source does with the old attribute, what happens to a constraint that already existed before the upgrade, and how to check your own modules and database.&lt;/p&gt;

&lt;p&gt;This article was first published &lt;a href="https://dobybaxter127.gitlab.io/articles/odoo-19-drops-your-sql-constraints/" rel="noopener noreferrer"&gt;on my site&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;p&gt;Up to Odoo 18, a model declared its SQL constraints as a list of tuples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trade.account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_sql_constraints&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reference_unique&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNIQUE(reference)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Trade account references must be unique.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In 18.0 there is no other way to write it. Odoo's own 18.0 core declares &lt;code&gt;_sql_constraints&lt;/code&gt; in 174 files and has no &lt;code&gt;models.Constraint&lt;/code&gt; anywhere, so nearly every custom module written before 19 uses the old form.&lt;/p&gt;

&lt;p&gt;Odoo 19 replaces the list with declarations on the class itself: &lt;code&gt;models.Constraint&lt;/code&gt;, &lt;code&gt;models.Index&lt;/code&gt; and &lt;code&gt;models.UniqueIndex&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TradeAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;trade.account&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;reference&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Char&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;_reference_unique&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;models&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Constraint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNIQUE(reference)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Trade account references must be unique.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Odoo 19 does with the old attribute
&lt;/h2&gt;

&lt;p&gt;When 19.0 builds a model class and finds &lt;code&gt;_sql_constraints&lt;/code&gt; on it, it logs this and carries on (&lt;code&gt;odoo/orm/model_classes.py&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Model attribute '_sql_constraints' is no longer supported, please define models.Constraint on the model.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not convert the list. Constraints are now applied from the model's table objects, which are the &lt;code&gt;models.Constraint&lt;/code&gt; and &lt;code&gt;models.Index&lt;/code&gt; declarations, and nothing reads &lt;code&gt;_sql_constraints&lt;/code&gt; any more. For a module written for 19 that still uses the old form, the constraint is simply never created.&lt;/p&gt;

&lt;h2&gt;
  
  
  On an upgraded database, the update removes it
&lt;/h2&gt;

&lt;p&gt;It is reasonable to hope that a database upgraded from 18 keeps the constraints it already had. Reading the 19.0 source, a module update removes them instead:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;At the end of loading, Odoo reflects each model's table objects into &lt;code&gt;ir.model.constraint&lt;/code&gt; and marks their external ids as loaded (&lt;code&gt;_reflect_model&lt;/code&gt; in &lt;code&gt;ir_model.py&lt;/code&gt;). A constraint declared only in &lt;code&gt;_sql_constraints&lt;/code&gt; is not a table object, so its external id is never marked.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ir.model.data._process_end&lt;/code&gt; then deletes whatever the updated module no longer provides, and a constraint whose external id was not loaded counts as no longer provided.&lt;/li&gt;
&lt;li&gt;Deleting an &lt;code&gt;ir.model.constraint&lt;/code&gt; record runs &lt;code&gt;ALTER TABLE ... DROP CONSTRAINT&lt;/code&gt; and logs &lt;code&gt;Dropped CONSTRAINT&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So the update that brings a custom module onto 19 is the update that drops its old constraints, and the log records it as routine housekeeping. I traced this through the 19.0 source rather than on a live upgrade, so check your own database with the query further down rather than relying on my reading or on the hope.&lt;/p&gt;

&lt;h2&gt;
  
  
  It happens in real projects
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://openspp.org/en/" rel="noopener noreferrer"&gt;OpenSPP&lt;/a&gt;, a social-protection platform built on Odoo, runs on 19. A May 2026 refactor migrated three of its &lt;code&gt;_sql_constraints&lt;/code&gt; declarations to &lt;code&gt;models.Constraint&lt;/code&gt;. Another, the rule that each change request type has only one document rule per reason, stayed in the old form until an August 2026 fix (#395), and until then duplicates could be saved. The project now runs a lint check of its own for Odoo 19 patterns. None of their tests had failed, because nothing fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to find them
&lt;/h2&gt;

&lt;h3&gt;
  
  
  In the code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="s2"&gt;"_sql_constraints"&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;.py addons/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any hit in a module whose &lt;code&gt;__manifest__.py&lt;/code&gt; version starts with &lt;code&gt;19.&lt;/code&gt; is a constraint that does not exist. In a 16, 17 or 18 module it is correct, and changing it there would break the module.&lt;/p&gt;

&lt;h3&gt;
  
  
  In the log
&lt;/h3&gt;

&lt;p&gt;Restart the server and search the log for &lt;code&gt;is no longer supported&lt;/code&gt;. There is one line per model still using the old form. After a module update, also search for &lt;code&gt;Dropped CONSTRAINT&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  In the database
&lt;/h3&gt;

&lt;p&gt;List what PostgreSQL actually enforces on the table, and compare it with what the module declares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;conname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pg_get_constraintdef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;oid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;pg_constraint&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;conrelid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'trade_account'&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;regclass&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;contype&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'u'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'c'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a uniqueness rule is missing, find out whether duplicates have already got in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;trade_account&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;reference&lt;/span&gt;
&lt;span class="k"&gt;HAVING&lt;/span&gt; &lt;span class="k"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  With a checker
&lt;/h3&gt;

&lt;p&gt;This is one of the rules in &lt;a href="https://dobybaxter127.gitlab.io/products/nexa/" rel="noopener noreferrer"&gt;Nexa&lt;/a&gt;, the Odoo checker I build, as &lt;code&gt;odoo.sql-constraints-are-ignored-from-19&lt;/code&gt;. It reads the file and the series in the module's manifest, and it stays silent on 16 to 18, where the old form is the only form. Run over OpenSPP's history, it fires on all four of the fixed declarations as the files stood before each fix, and on none after. With no model involved, it takes a fraction of a second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nexa check models/trade_account.py --no-model

  impact     likelihood
  high       defect     line 18    odoo.sql-constraints-are-ignored-from-19
    TradeAccount declares _sql_constraints, which Odoo 19 ignores: the registry logs a
    warning and creates no constraint, so nothing enforces it -- declare each one as
    models.Constraint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Migrating without losing the rule a second time
&lt;/h2&gt;

&lt;p&gt;The change itself is small, as the two examples at the top show. Three details decide whether the rule survives it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clear the duplicates first.&lt;/strong&gt; If rows already break the rule, adding the constraint fails. Odoo 19 treats that as a warning, "not a deployment showstopper" in the words of its own comment in &lt;code&gt;registry.py&lt;/code&gt;, and carries on without the constraint. So migrating the declaration is not enough on its own: run the duplicates query, merge or fix what it finds, and only then update.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the name.&lt;/strong&gt; Odoo 18 named the constraint &lt;code&gt;{table}_{key}&lt;/code&gt;. Odoo 19 names it &lt;code&gt;{table}_{attribute}&lt;/code&gt;, with the leading underscore removed. So &lt;code&gt;("reference_unique", ...)&lt;/code&gt; becomes &lt;code&gt;_reference_unique = models.Constraint(...)&lt;/code&gt;, and both produce &lt;code&gt;trade_account_reference_unique&lt;/code&gt;. With the same name, the update recognizes the existing constraint as the one you declared instead of removing it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the definition text identical.&lt;/strong&gt; Odoo stores the definition as a comment on the constraint and compares it with what you declare. If the text differs, even in spacing, it drops the constraint and adds it again, and that re-add is exactly the step that fails quietly when duplicates exist.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then run the &lt;code&gt;pg_constraint&lt;/code&gt; query again after the update. The constraint should be there, under the name you expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The general lesson
&lt;/h2&gt;

&lt;p&gt;A framework upgrade does not only break things loudly. Sometimes it retires a way of saying something, keeps accepting the old way, and records the difference in a log line that nobody reads. The only defense is to check what the database actually enforces, not what the code appears to declare.&lt;/p&gt;

&lt;p&gt;If you run Odoo and would rather have someone read your custom modules for this and the other failures that raise no error, that is what my &lt;a href="https://dobybaxter127.gitlab.io/services/#odoo" rel="noopener noreferrer"&gt;Odoo reliability reviews&lt;/a&gt; are for. They are read-only, fixed-price, and start at £200.&lt;/p&gt;

</description>
      <category>odoo</category>
      <category>python</category>
      <category>postgres</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Software Is Where My Actions Count: What a Special Interest Actually Does</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 20 Sep 2026 13:25:49 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/software-is-where-my-actions-count-what-a-special-interest-actually-does-4cgl</link>
      <guid>https://dev.to/dobybaxter127/software-is-where-my-actions-count-what-a-special-interest-actually-does-4cgl</guid>
      <description>&lt;p&gt;&lt;em&gt;People assume a special interest is just a hobby turned up too loud. It isn't. It's the one environment where my actions reliably produce consequences I can see, understand, and stand behind. Mine maps onto software, and that single fact explains more about my career than any decision I ever consciously made.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What a special interest actually is
&lt;/h2&gt;

&lt;p&gt;The casual version of the term is something like "an autistic person who is really into trains." Technically true, completely missing the mechanism.&lt;/p&gt;

&lt;p&gt;What's actually happening is that a particular domain becomes disproportionately capable of activating things that are otherwise hard for me to reach: motivation, sustained attention, emotional regulation, a sense of competence, a sense of coherence. Not because the subject is objectively more important than everything else in my life, but because my nervous system responds to it in a way it doesn't respond to almost anything else.&lt;/p&gt;

&lt;p&gt;That's a very different claim from "I enjoy this." Plenty of things are enjoyable. Very few things reliably switch me on.&lt;/p&gt;

&lt;p&gt;There's a name for this, and it isn't mine. &lt;a href="https://monotropism.org/murray-lesser-lawson/" rel="noopener noreferrer"&gt;Monotropism&lt;/a&gt;, developed by the autistic researchers Dinah Murray, Mike Lesser, and Wenn Lawson, treats attention as quantitatively limited: it pools into a small number of intense channels instead of spreading thinly across many. The image in the original work is a torch beam rather than diffused light, and the state it produces is an &lt;em&gt;attention tunnel&lt;/em&gt;. Once you model attention that way, most of what follows here stops being a personality quirk and starts being a predictable consequence of how the attention is allocated.&lt;/p&gt;

&lt;p&gt;So when someone asks why I'll work on a problem at 2am while physically exhausted, the honest answer isn't discipline and it isn't recklessness. &lt;strong&gt;It's that this is one of the few places where my brain will actually start.&lt;/strong&gt; Telling me to apply that energy to something more sensible assumes the energy is fungible. It isn't. It's tunnel-bound, and the tunnel picked me rather than the other way around.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same wiring, different surface
&lt;/h2&gt;

&lt;p&gt;I read an account once by an autistic person describing why she spent enormous amounts of time in video games. The word she reached for wasn't "addictive." It was control. Agency. Autonomy. A world where her actions had consequences, the rules were legible, competence was achievable, progress was visible, and mistakes were recoverable. Everything her actual environment, at home and in institutions, did not reliably offer.&lt;/p&gt;

&lt;p&gt;That turns out not to be an idiosyncratic account. It's close to a consensus finding in the qualitative research on autistic gamers, to the point where one study in the &lt;em&gt;British Journal of Developmental Psychology&lt;/em&gt; takes its title straight from a participant: &lt;a href="https://bpspsychub.onlinelibrary.wiley.com/doi/10.1111/bjdp.12424" rel="noopener noreferrer"&gt;"I can actually do it without any help or someone watching over me all the time and giving me constant instruction"&lt;/a&gt;. A &lt;a href="https://dl.acm.org/doi/full/10.1145/3803421" rel="noopener noreferrer"&gt;large survey of autistic gamers&lt;/a&gt; puts stress relief and a sense of achievement at the top of the list of reasons, and &lt;a href="https://www.sciencedirect.com/science/article/abs/pii/S0747563222004381" rel="noopener noreferrer"&gt;work on escapism in autistic adults&lt;/a&gt; frames it as a self-regulation strategy rather than a pathology.&lt;/p&gt;

&lt;p&gt;I recognized my own relationship with software in all of it immediately, because structurally it's the same thing.&lt;/p&gt;

&lt;p&gt;A game gives you a bounded world that responds to you. Software gives you almost exactly the same properties, except the thing you produce exists outside the world you were playing in. You think of something, build it, hit a wall, learn, solve it, see it work, publish it, and someone can use it.&lt;/p&gt;

&lt;p&gt;Same wiring. Different surface. &lt;strong&gt;The pull was never the subject matter, it was the structure: a domain where my actions count.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What my brain wants, and what software supplies
&lt;/h2&gt;

&lt;p&gt;Here's the mapping, as concretely as I can state it.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;legible rules&lt;/strong&gt;. Software is a system where the rules, however complex, are ultimately knowable. When something breaks, there is a reason. That reason can be found. Nothing about it depends on correctly reading a room.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;fast, honest feedback&lt;/strong&gt;. Software tells you unambiguously and quickly whether what you did worked. The test passes or it doesn't. The build succeeds or it doesn't. No ambiguity, no social interpretation layer, no waiting weeks to find out whether you misjudged something.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;depth without a ceiling&lt;/strong&gt;. Most interests eventually exhaust what's available to learn about them. Software does not. However far down I want to go, there is always another layer, and going deeper is rewarded rather than treated as excessive. A domain that never says "that's enough detail now" is an unusually comfortable place to put a tunnel.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;self-directed scope&lt;/strong&gt;. I choose the problem, the approach, the architecture, the stopping point. Nobody assigns me the next thing. That matters enormously, because externally imposed structure is exactly what my executive function handles worst, and self-chosen structure is what it handles best.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;recoverable mistakes&lt;/strong&gt;. Version control means almost nothing I do is irreversible. Being able to try something badly, see what happens, and roll it back is what makes exploration possible for someone who cannot reliably plan the whole route in advance.&lt;/p&gt;

&lt;p&gt;My brain wants &lt;strong&gt;something that persists&lt;/strong&gt;. At the end, an artifact exists. A repository, a release, a service, a skill I didn't have last week.&lt;/p&gt;

&lt;p&gt;Put those six together and you get a description of an environment that could have been designed for how I work. It wasn't, obviously. It just happens to be the shape it is, and my attention found it and stayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The autonomy I don't get anywhere else
&lt;/h2&gt;

&lt;p&gt;This is the part that's hardest to explain to people who assume I'm describing a preference.&lt;/p&gt;

&lt;p&gt;I actually have a lot of control over my physical environment. I live alone. I arrange my space exactly how I need it. I eat a small, consistent set of foods that work. I go outside at night, when it's quiet and dim and there are no people, because the daytime version of that same errand costs more than my nervous system can consistently pay.&lt;/p&gt;

&lt;p&gt;That's real autonomy and it genuinely helps. But &lt;strong&gt;controlling my environment is not the same as being able to participate in the world.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every one of those adaptations still reads as "I can do this, but only under these conditions." I didn't choose 2am because it's my favorite hour to be alive. I chose it because it's when the cost drops to something payable. So even inside a life I've deliberately optimized around myself, I'm permanently aware of the set of things that remain out of reach, and no amount of rearranging my flat changes that.&lt;/p&gt;

&lt;p&gt;Software doesn't work that way. In software the barrier between what I intend and what I can execute is thin and getting thinner, because the entire discipline is built out of tools whose whole purpose is to absorb load. I don't have to hold everything in working memory. I don't have to remember every API. I don't have to independently sustain a research thread across days, which is precisely the kind of continuous perseverance I'm worst at. The tooling carries the part I can't, and I do the part I can: the judgment, the design, the decisions, the responsibility for what ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the difference. Everywhere else I'm adapting the world to the boundaries of my nervous system. Here I'm using tools that let me cross them.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So when the interest activates, it isn't activating in a vacuum. It's activating the only reliable supply of something scarce in the rest of my life: I decided this, I did this, it worked, and it's mine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the artifact matters so much
&lt;/h2&gt;

&lt;p&gt;There's an extra layer on top of the loop, and I underrated it for years.&lt;/p&gt;

&lt;p&gt;The work leaves evidence.&lt;/p&gt;

&lt;p&gt;Each finished thing is a durable record that says: I learned this, I solved this, I built this, I can stand behind this. That matters disproportionately to someone whose memory doesn't reliably preserve their own history, and whose sense of capability fluctuates with energy levels that swing hard. On a bad day I can't feel competent. But I can look at something I built and read the receipt.&lt;/p&gt;

&lt;p&gt;That's also why the interest didn't stay private. Publishing the work turns the internal loop into something with an external footprint: a portfolio, a service, a skill someone else can benefit from. &lt;strong&gt;A special interest that produces artifacts stops looking like a fixation and starts looking like a career&lt;/strong&gt;, which is a fairly arbitrary distinction, but it's the one the world actually uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why leaving is hard, and why "addictive" is the wrong word
&lt;/h2&gt;

&lt;p&gt;I want to be honest about the cost, because the same mechanism that makes this good for me can consume everything I have.&lt;/p&gt;

&lt;p&gt;Addiction imports a set of claims that don't fit here. What's actually happening is simpler: &lt;strong&gt;an activity becomes extraordinarily reinforcing when it supplies something that's scarce everywhere else in your life.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monotropism gives a cleaner account of the exit cost than willpower ever did. Leaving an attention tunnel isn't like closing a tab, because you aren't next to the thing, you're inside it. The original paper puts it starkly: for someone in an attention tunnel, unanticipated change is briefly catastrophic. That's not a metaphor for mild annoyance. It's a description of why being pulled out of something feels the way it does.&lt;/p&gt;

&lt;p&gt;There's a companion term for the other half of it. &lt;a href="https://www.psychologicalscience.org/publications/observer/student-notebook-autistic-inertia-srinivasan.html" rel="noopener noreferrer"&gt;Autistic inertia&lt;/a&gt; covers both the difficulty of starting and the difficulty of stopping, the latter sometimes called termination resistance, and &lt;a href="https://www.frontiersin.org/journals/psychology/articles/10.3389/fpsyg.2021.741421/full" rel="noopener noreferrer"&gt;autistic people describe it as among the most disabling parts of being autistic&lt;/a&gt; while simultaneously describing deep absorption as one of the most restorative states available to them. Both things are true at once, which is exactly why this is hard to talk about honestly.&lt;/p&gt;

&lt;p&gt;It's also worth separating two things that often get merged. Hyperfocus describes an unusually intense stretch of attention, hours or maybe days. It doesn't explain why someone returns to the same domain for years. A special interest is the long-horizon pattern, and it can become a genuine organizing feature of a life and an identity. The two interact, but they're not the same phenomenon, and collapsing them makes the whole thing sound more pathological and less structural than it is.&lt;/p&gt;

&lt;p&gt;The consequence is that stopping isn't experienced as "I'm done for today." It lands closer to "I'm leaving the place where my actions count." That's a far stronger pull than enjoyment alone, and it's why the physical exhaustion always arrives long before the motivation does. There's no backpressure in the loop. Nothing in it signals that the upstream resource is depleted.&lt;/p&gt;

&lt;p&gt;The only off-ramp I've found that actually works is making stopping cheap. Before I close everything, I leave myself enough state to resume without reconstructing the whole mental picture: the symptom, the current hypothesis, the last thing I changed, the next concrete step. That note isn't a reminder that the task exists. It's proof the thread hasn't been lost, which is the specific fear that keeps me working when I should be asleep. Once my brain accepts that the tunnel will still be there tomorrow, it will let me put it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The interest is not the problem
&lt;/h2&gt;

&lt;p&gt;If there's a failure mode here, it isn't "I care about software too much."&lt;/p&gt;

&lt;p&gt;It's that a single domain is carrying the entire burden of proving that my actions matter. That's a load-balancing problem, and the fix is not to take away the thing that works. Autistic accounts consistently point the other way: &lt;a href="https://autismdetect.co.uk/how-special-interests-help-prevent-autism-burnout/" rel="noopener noreferrer"&gt;special interests function as protection against burnout&lt;/a&gt; precisely because they restore a sense of control. The fix is to widen the set of places where agency is available, even in small, unimpressive ways, so the interest gets to be one strong source instead of the only one.&lt;/p&gt;

&lt;p&gt;What I'd resist entirely is the framing where a special interest is something to be moderated on principle, or grown out of. Mine is where my motivation lives. It's where I'm most competent, most regulated, and most recognizably myself. It produced a skill set, a body of work, and an income.&lt;/p&gt;

&lt;p&gt;So if one idea survives this whole essay, let it be this:&lt;/p&gt;

&lt;p&gt;I didn't build a career and then discover I enjoyed it. My attention found the one environment where it could reliably act, went all the way in, and the world happened to call the result a profession.&lt;/p&gt;

&lt;p&gt;That's not a hobby that got out of hand. That's the only place my agency was ever fully available, and I'd be foolish to treat it as anything less.&lt;/p&gt;

</description>
      <category>neurodiversity</category>
      <category>a11y</category>
      <category>softwareengineering</category>
      <category>career</category>
    </item>
    <item>
      <title>AI as a Fluency Machine: What Software Engineering Taught Me About Checking a Model's Answer</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 13 Sep 2026 16:33:01 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/ai-as-a-fluency-machine-what-software-engineering-taught-me-about-checking-a-models-answer-mgj</link>
      <guid>https://dev.to/dobybaxter127/ai-as-a-fluency-machine-what-software-engineering-taught-me-about-checking-a-models-answer-mgj</guid>
      <description>&lt;p&gt;&lt;em&gt;We refuse to install a dependency without a lockfile, a checksum, and a license audit. Then we paste a language model's answer straight into a model file and call it velocity. The scrutiny didn't disappear — it just stopped applying to the thing writing the code.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The paradox
&lt;/h2&gt;

&lt;p&gt;We have built an extraordinary apparatus of distrust around our own work. Lockfiles pin transitive dependencies to exact versions. Checksums verify the bytes arrived intact. Signed commits prove authorship. CI refuses a merge over one failing test. Code review exists because we decided, collectively, that no engineer's confidence in their own code is sufficient evidence that the code is correct.&lt;/p&gt;

&lt;p&gt;Then a model produces forty lines, and they go in.&lt;/p&gt;

&lt;p&gt;Not because anyone sat down and decided generated code deserves less scrutiny. It's that every mechanism we built assumes a &lt;em&gt;human author&lt;/em&gt; on the other end — someone who can be asked why, who hedges when unsure, who owns the thing afterwards. Generated code walks past all of it, because none of it was pointed at that door.&lt;/p&gt;

&lt;p&gt;The standards haven't changed. The risk hasn't changed. Only one thing has: the single participant in the process that cannot be held responsible for being wrong is now also the one we check the least.&lt;/p&gt;

&lt;p&gt;As a software engineer, that asymmetry tells me the real disagreement isn't about whether AI writes good code. It's about what we are willing to call &lt;strong&gt;checked&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hallucination isn't the interesting question
&lt;/h2&gt;

&lt;p&gt;Most debates about AI and code ask whether the model hallucinates. Fair enough — it does. But hallucination is the &lt;em&gt;easy&lt;/em&gt; failure, because it announces itself. An invented API doesn't exist. The import fails, the type checker complains, the test goes red. The system you already have catches it.&lt;/p&gt;

&lt;p&gt;The failure that matters is quieter.&lt;/p&gt;

&lt;p&gt;During development, my 7B cited &lt;code&gt;odoo.onchange-is-not-validation&lt;/code&gt; — a real rule, in my corpus, traceable to upstream documentation — and then wrote an &lt;code&gt;@api.onchange&lt;/code&gt; to guard the value. Every citation verified. The answer was still wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Citation existence is not rule compliance.&lt;/strong&gt; That one sentence is what the whole project is built on.&lt;/p&gt;

&lt;p&gt;I've come to think of the underlying machine as a &lt;em&gt;fluency machine&lt;/em&gt;. I use the word deliberately. It doesn't manufacture wrong answers — wrong answers predate it by the entire history of the profession. What it manufactures is the &lt;strong&gt;appearance of having checked&lt;/strong&gt;. A junior engineer who is unsure hedges, and you can hear it in the hedge. A model that is unsure produces the same fluent, well-structured, confidently-cited paragraph as a model that is right. The uncertainty signal we spent careers learning to read simply isn't in the output.&lt;/p&gt;

&lt;p&gt;Fluency is not a confidence interval. It just renders like one.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prompt is not a specification
&lt;/h2&gt;

&lt;p&gt;Here's where software engineering has something to say. Our entire discipline runs on abstraction — interfaces, APIs, frameworks, platforms — each layer letting one engineer reason about one problem without drowning in all the others. A prompt is an abstraction too. It compresses everything you know about a domain into prose and hands it over as though prose were a contract.&lt;/p&gt;

&lt;p&gt;But every abstraction hides what it discarded, and prose hides more than most, because &lt;strong&gt;prose has no schema&lt;/strong&gt;. You can't diff it meaningfully, you can't validate it, and you cannot test whether a paragraph still means what it meant before you edited it.&lt;/p&gt;

&lt;p&gt;I found the edge of that the ordinary way. Prompt tuning plateaued after two iterations. Hardening the Odoo rule about returning &lt;code&gt;false&lt;/code&gt; for an empty field fixed the scalar cases and simultaneously broke the Many2one tuple handling that had been correct the day before. A 7B holds a bounded number of constraints; past that point, more instruction text just trades one error for another.&lt;/p&gt;

&lt;p&gt;The same limit showed up in routing. My first attempt used a single system prompt covering both Odoo and TypeScript. Asked a plainly Odoo question — &lt;em&gt;cap a sale order line quantity&lt;/em&gt; — it answered entirely in TypeScript with a Zod schema. The instructions bleed. A small model doesn't reliably route between two stacks described in one paragraph.&lt;/p&gt;

&lt;p&gt;So I moved the rules out of the prompt and into data. Each rule lives in &lt;code&gt;corpus/rules/*.json&lt;/code&gt; against a JSON Schema: a citable id, a severity, the failure it prevents, a wrong/correct code pair, an upstream documentation URL where one exists, and where possible a regex that detects the violation in generated code. The system prompts and the retrieval index became &lt;strong&gt;build artifacts&lt;/strong&gt;, generated from that corpus by &lt;code&gt;nexa build&lt;/code&gt;. Same principle as &lt;code&gt;z.infer&amp;lt;typeof Schema&amp;gt;&lt;/code&gt; — one definition, derived artifacts, no drift.&lt;/p&gt;

&lt;p&gt;That didn't make the model smarter. It changed the failure mode. Instead of rewriting prose and hoping, a rule is written once and flows to the prompt, the retrieval index, and the compliance checker together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody misconfigured it, but the system decayed
&lt;/h2&gt;

&lt;p&gt;Distributed systems engineers know the shape of the next part. A latency bug in one service cascades through a queue, triggers retries, saturates a database, and surfaces as an outage somewhere else entirely. Every component behaved to spec. The system as a whole did something nobody designed.&lt;/p&gt;

&lt;p&gt;I benchmarked the 7B at 37 tok/s. Four instrumented runs later, the report showed roughly 10. I offered three diagnoses; the first two were wrong.&lt;/p&gt;

&lt;p&gt;The actual cause was that Ollama's memory estimator is conservative and leaves layers on the CPU with VRAM still free — &lt;strong&gt;11.7 tok/s automatic versus 38.6 forced&lt;/strong&gt;, same model, same context. My Modelfiles carried &lt;code&gt;num_gpu 99&lt;/code&gt;, but the pipeline was calling the raw base model, which has no such parameter. Meanwhile the KV cache environment variables had been set while the server was already running, so they never took effect at all. And two orphaned &lt;code&gt;llama-server&lt;/code&gt; processes were sitting there holding 1.4 GB of VRAM, left behind when their parent was killed.&lt;/p&gt;

&lt;p&gt;Nobody misconfigured anything. Ollama's estimator was being careful. Windows was doing what it does with environment variables set after a process starts. The orphans were doing what orphans do. Collectively, the system ran at roughly a third of its speed for most of a working session — while I read a number I had measured once and believed ever since.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An unmeasured number decays into a claim.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is the entire reason &lt;code&gt;nexa doctor&lt;/code&gt; exists, and why it checks GPU residency and each of those traps by name rather than just confirming that Ollama is up. There's an old rule in operations: someone owns every service. Complexity raises the effort of attribution. It never removes the need for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Patterns inherit their author's blind spots
&lt;/h2&gt;

&lt;p&gt;You can't check for "is this code correct" — the phrase is too vague to be an objective. So, exactly as we do everywhere in engineering, we check &lt;em&gt;proxies&lt;/em&gt;. A regex matching &lt;code&gt;@api.onchange&lt;/code&gt; in a validation context is a proxy for "this code is using the wrong mechanism to hold an invariant."&lt;/p&gt;

&lt;p&gt;Individually, these are reasonable. The problem is that a proxy always discards information, and it quietly inherits the assumptions of whoever wrote it.&lt;/p&gt;

&lt;p&gt;So the patterns get tested too. Every rule already carries a wrong and a correct sample, which means the test data existed for free. &lt;code&gt;nexa validate&lt;/code&gt; fails the build on three faults: a pattern that doesn't match its own wrong sample catches nothing; a pattern that fires on its own correct sample cries wolf; a &lt;code&gt;satisfied_pattern&lt;/code&gt; that clears its own wrong sample is inert by construction.&lt;/p&gt;

&lt;p&gt;Then &lt;code&gt;nexa crosscheck&lt;/code&gt; fires every pattern at every &lt;em&gt;other&lt;/em&gt; rule's correct sample — because self-testing proves a pattern handles its own case and says nothing about whether it goes off on unrelated good code. The first run found three real defects. &lt;code&gt;float-comparison&lt;/code&gt; fired on &lt;code&gt;qty &amp;gt; 0&lt;/code&gt;, including inside SQL where &lt;code&gt;float_compare&lt;/code&gt; doesn't apply; narrowed to equality, which is the actual trap. &lt;code&gt;odoo-read-returns-array&lt;/code&gt; fired on any &lt;code&gt;Schema.parse(await ...)&lt;/code&gt;. And &lt;code&gt;ondelete-is-a-decision&lt;/code&gt; correctly flagged a sample &lt;strong&gt;in my own corpus&lt;/strong&gt; that didn't follow its own rule.&lt;/p&gt;

&lt;p&gt;That last one is the one I'd keep. The checker was right and the corpus was wrong.&lt;/p&gt;

&lt;p&gt;It's also the loop worth being nervous about: my rules become the system prompt, the system prompt shapes the answer, and the answer is then checked against the same rules that produced it. A rule that is subtly wrong is wrong in three places simultaneously, and the pipeline will report VERIFIED at every step. All models are wrong but some are useful — the danger was never the simplification. It's forgetting what got discarded in it.&lt;/p&gt;

&lt;p&gt;Which is why the limitation goes in the docs, in plain words: 27 of 32 rules trace to upstream documentation, and the rest rest on general knowledge and deserve more scepticism. These rules were written by an assistant, not harvested from production incidents. Rules drawn from real incidents would be worth more.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Verified" can be theatre
&lt;/h2&gt;

&lt;p&gt;The standard reassurance in this space is that the tool validates its own output. From an engineering perspective, that tells you almost nothing about how the system behaves. Inserting a step into a pipeline doesn't guarantee verification, any more than a deployment approval guarantees oversight — an approval only matters if the reviewer has the information to spot risk, the authority to say no, and the time to actually look.&lt;/p&gt;

&lt;p&gt;We have a name for the human half of this: &lt;strong&gt;automation bias&lt;/strong&gt;. Once software earns a reputation for being usually right, reviewers start confirming its output instead of interrogating it.&lt;/p&gt;

&lt;p&gt;The checker half has a sharper version of the same problem. A checker that fires on good code is worse than no checker, because it teaches you to dismiss it — and once you're dismissing it, it will be dismissed on the day it was right. So five of my rules deliberately carry &lt;strong&gt;no pattern at all&lt;/strong&gt;, with the reason recorded for each. Two need set comparison between a decorator's arguments and the attributes its body reads, which is AST work, not regex work. One lives in a diff between module versions. Two depend on context a snippet doesn't contain. Those rules return &lt;strong&gt;no verdict&lt;/strong&gt; rather than a false pass. 84% coverage with an honest list of the gap beats 100% with five patterns producing noise.&lt;/p&gt;

&lt;p&gt;So "did it verify?" is the wrong question. The right one is: &lt;em&gt;what exactly was checked, and what does the absence of a finding actually prove?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;In Nexa, VERIFIED means the checks that exist found nothing. It does not mean correct. Compliance is only checked against rules retrieved for that question, plus pinned ones — so a rule that was never retrieved is a rule that was never enforced. I'd rather ship that sentence than a green checkmark.&lt;/p&gt;

&lt;h2&gt;
  
  
  Small and local is not a lesser tool
&lt;/h2&gt;

&lt;p&gt;Now flip the paradox around.&lt;/p&gt;

&lt;p&gt;A 7B-to-30B local model does not match a frontier model on hard architectural reasoning. That gap is a function of scale, and no amount of prompting closes it. I put that in the README, near the top, deliberately.&lt;/p&gt;

&lt;p&gt;But engineers make a distinction when optimizing: removing overhead is not the same as increasing capability. Stripping out overhead lets existing resources do more — it doesn't upgrade the hardware. Nexa doesn't make a small model smarter. It removes the thing that made a small model untrustworthy: the gap between an answer and any means of checking it.&lt;/p&gt;

&lt;p&gt;And for this particular job, that trade isn't obviously a loss. The frontier model has more capability. The local one has more &lt;strong&gt;provenance&lt;/strong&gt; — it answers from rules I can read, cite, version and test, on hardware I own, with client code that never leaves the laptop. No API key, no network call, nothing phoning home. On a machine used for client work that isn't a feature, it's a precondition, which is also why telemetry records rule ids, counts, model names and timings, and not question or answer text unless you explicitly turn it on. A telemetry file is the last place client code should quietly accumulate.&lt;/p&gt;

&lt;p&gt;Same technology, different question. One asks how capable this can be. The other asks how wrong it can be without me finding out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification cannot be delegated to fluency
&lt;/h2&gt;

&lt;p&gt;Every mature engineering discipline eventually learns the same lesson: a system can't be trusted merely because it functions. It has to be observable, auditable, and — this is the part we keep skipping with generated code — &lt;em&gt;falsifiable&lt;/em&gt;. We'd never run production without logs, traces and deployment history, because once causality goes opaque, reliability becomes impossible. Yet we routinely accept generated code on terms we'd never accept from a library: no provenance, no stated assumptions, no way to ask what it was checked against.&lt;/p&gt;

&lt;p&gt;Observability can't be bolted on after a system fails. Verifiability follows the same rule. If a tool isn't designed to make its own answers checkable from the start, no amount of downstream review will conjure that property later — you'll just be reading fluent paragraphs faster.&lt;/p&gt;

&lt;p&gt;So if one idea survives this whole article, let it be this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The true measure of a coding assistant is not how often it is right. It's how reliably you find out when it is wrong.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Correctness can't be prompted into existence. It can't be inferred from formatting, borrowed from a real-looking citation, or assumed because the prose was confident and the rule id resolved. Someone still has to write the rule down in a form that can be checked, decide what a failing check means, and accept what the gaps cost. Those decisions stay irreducibly human — which is precisely what I want from a tool I let anywhere near a client's codebase.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Nexa&lt;/strong&gt; is source-available under PolyForm Noncommercial 1.0.0 — free to read, run, modify and evaluate for any noncommercial purpose. It's a few hundred kilobytes of Python (stdlib only) that orchestrates open models you pull from Ollama; no weights ship with it. 32 rules today across Odoo, TypeScript and JavaScript, 27 with an automated check, 0 cross-hits.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dobybaxter127.gitlab.io/products/nexa/" rel="noopener noreferrer"&gt;Product page&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitlab.com/dobybaxter127/nexa" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://huggingface.co/DBax127/nexa" rel="noopener noreferrer"&gt;Hugging Face&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;a href="https://gitlab.com/dobybaxter127/nexa/-/blob/main/ARCHITECTURE.md" rel="noopener noreferrer"&gt;ARCHITECTURE.md&lt;/a&gt; records every decision with the evidence behind it, including the ones that were wrong first — and an honest account of what it cannot do. If you only read one file, read the limitations section.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>When Is a System Not Fit for Purpose?</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Tue, 08 Sep 2026 18:27:47 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/when-is-a-system-not-fit-for-purpose-587d</link>
      <guid>https://dev.to/dobybaxter127/when-is-a-system-not-fit-for-purpose-587d</guid>
      <description>&lt;h2&gt;
  
  
  The question
&lt;/h2&gt;

&lt;p&gt;When do we get to say that a system is not fit for purpose?&lt;/p&gt;

&lt;p&gt;In software engineering, we usually have a fairly straightforward answer. A system is fit for purpose if it reliably does the thing it was designed to do.&lt;/p&gt;

&lt;p&gt;That sounds obvious, but we seem to apply a different standard to public systems.&lt;/p&gt;

&lt;p&gt;A government service can have a department behind it, an application portal, a helpline, a set of criteria, a budget, and hundreds of people working within it. It can process thousands of applications and schedule thousands of appointments.&lt;/p&gt;

&lt;p&gt;None of that necessarily tells us whether it is actually achieving its purpose.&lt;/p&gt;

&lt;p&gt;A system existing is not the same thing as a system working.&lt;/p&gt;

&lt;p&gt;And from a developer's perspective, that distinction matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Activity is not an outcome
&lt;/h2&gt;

&lt;p&gt;Imagine an application with a perfectly functioning workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  ↓
Application
  ↓
Validation
  ↓
Assessment
  ↓
Decision
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every component works.&lt;/p&gt;

&lt;p&gt;The application is submitted successfully.&lt;/p&gt;

&lt;p&gt;The validation passes.&lt;/p&gt;

&lt;p&gt;The assessment takes place.&lt;/p&gt;

&lt;p&gt;The decision is recorded.&lt;/p&gt;

&lt;p&gt;From an operational perspective, the system has performed exactly as expected.&lt;/p&gt;

&lt;p&gt;But what if the user finishes the process without receiving the support they actually needed?&lt;/p&gt;

&lt;p&gt;The system has produced activity, but not an outcome.&lt;/p&gt;

&lt;p&gt;This is a familiar problem in software engineering. We can build dashboards showing requests processed, jobs completed, transactions handled, and uptime maintained, but those metrics don't necessarily tell us whether the system is solving the problem it was built to solve.&lt;/p&gt;

&lt;p&gt;Applications received are activity.&lt;/p&gt;

&lt;p&gt;Appointments attended are activity.&lt;/p&gt;

&lt;p&gt;Cases opened are activity.&lt;/p&gt;

&lt;p&gt;Forms processed are activity.&lt;/p&gt;

&lt;p&gt;The more important question is:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did the user's situation actually change?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That is an outcome.&lt;/p&gt;

&lt;p&gt;And when a system measures activity more effectively than it measures outcomes, it becomes possible for the system to look healthy while the people using it experience failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The edge-case problem
&lt;/h2&gt;

&lt;p&gt;One of the first things you learn as a developer is that the happy path isn't the whole system.&lt;/p&gt;

&lt;p&gt;You can design a beautiful workflow for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Input → Processing → Success
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and still have a terrible system if you haven't thought carefully about what happens when the input doesn't fit your assumptions.&lt;/p&gt;

&lt;p&gt;Real systems encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;invalid input&lt;/li&gt;
&lt;li&gt;missing information&lt;/li&gt;
&lt;li&gt;unsupported configurations&lt;/li&gt;
&lt;li&gt;unavailable dependencies&lt;/li&gt;
&lt;li&gt;unexpected states&lt;/li&gt;
&lt;li&gt;conflicting requirements&lt;/li&gt;
&lt;li&gt;users who don't behave exactly as expected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Good software doesn't pretend those cases don't exist.&lt;/p&gt;

&lt;p&gt;It detects them.&lt;/p&gt;

&lt;p&gt;It explains them.&lt;/p&gt;

&lt;p&gt;And, where possible, it provides a recovery path.&lt;/p&gt;

&lt;p&gt;Public services have the same problem.&lt;/p&gt;

&lt;p&gt;A system should be able to tell someone not only that they qualify, but also when it cannot provide what they need.&lt;/p&gt;

&lt;p&gt;That isn't necessarily a failure.&lt;/p&gt;

&lt;p&gt;In many cases, &lt;strong&gt;early rejection is better system behavior than indefinite processing.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an API knows that it cannot fulfill a request, returning a clear error is preferable to accepting the request, passing it through ten other services, consuming resources, and eventually failing without explaining why.&lt;/p&gt;

&lt;p&gt;The same principle applies to services involving people.&lt;/p&gt;

&lt;p&gt;If a program cannot realistically help someone, keeping that person inside the process for months isn't evidence that the program is working.&lt;/p&gt;

&lt;p&gt;It may simply mean the system doesn't have a meaningful failure state.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a good failure state looks like
&lt;/h2&gt;

&lt;p&gt;Consider two systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  System A
&lt;/h3&gt;

&lt;p&gt;A person enters a service.&lt;/p&gt;

&lt;p&gt;The system determines that it cannot help them.&lt;/p&gt;

&lt;p&gt;It explains why and provides an appropriate onward route.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Person
  ↓
Assessment
  ↓
Not suitable
  ↓
Explanation
  ↓
Alternative service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The person hasn't received the original service, but the system has still produced a useful outcome.&lt;/p&gt;

&lt;p&gt;Now consider System B:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Person
  ↓
Application
  ↓
Appointment
  ↓
Documentation
  ↓
Follow-up
  ↓
More documentation
  ↓
More appointments
  ↓
Eventually: cannot help
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;System B may produce considerably more measurable activity.&lt;/p&gt;

&lt;p&gt;But it is arguably the worse system.&lt;/p&gt;

&lt;p&gt;From an engineering perspective, the difference is obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;System A has a defined failure state. System B has an expensive failure path.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The user spends time and resources discovering something the system could potentially have established at the beginning.&lt;/p&gt;

&lt;p&gt;That's not just inefficient.&lt;/p&gt;

&lt;p&gt;It's poor system design.&lt;/p&gt;

&lt;h2&gt;
  
  
  The infinite loop
&lt;/h2&gt;

&lt;p&gt;There is another failure mode that becomes particularly interesting when you look at public services as systems: the infinite loop.&lt;/p&gt;

&lt;p&gt;Suppose a program is intended to help bridge the gap between a candidate and an employer.&lt;/p&gt;

&lt;p&gt;The intended architecture might look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Candidate
   ↓
Program
   ↓
Employer connection
   ↓
Employer engagement
   ↓
Employment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But what happens if the program can only meaningfully intervene after an employer has already engaged?&lt;/p&gt;

&lt;p&gt;The process becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Candidate applies
   ↓
Employer does not engage
   ↓
Application rejected / ignored
   ↓
No employment
   ↓
No employer-side intervention
   ↓
Candidate applies again
   ↓
Repeat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing has technically crashed.&lt;/p&gt;

&lt;p&gt;There may be no obvious software equivalent of an exception.&lt;/p&gt;

&lt;p&gt;The system is simply unable to transition into the state required for success.&lt;/p&gt;

&lt;p&gt;In software terms, I would immediately ask:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What state transition is missing?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the purpose of the system is to help create an employer connection, but the system depends on an employer connection already existing before it can provide that intervention, then there is a circular dependency.&lt;/p&gt;

&lt;p&gt;The system is waiting for the event it is supposed to help create.&lt;/p&gt;

&lt;p&gt;That deserves architectural scrutiny.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every system has boundaries
&lt;/h2&gt;

&lt;p&gt;This is also where transparency becomes part of system design.&lt;/p&gt;

&lt;p&gt;Every system has a boundary.&lt;/p&gt;

&lt;p&gt;An API has supported inputs.&lt;/p&gt;

&lt;p&gt;A database has constraints.&lt;/p&gt;

&lt;p&gt;A service has capacity.&lt;/p&gt;

&lt;p&gt;A platform has supported environments.&lt;/p&gt;

&lt;p&gt;Good systems document those boundaries.&lt;/p&gt;

&lt;p&gt;You don't want a developer discovering after six months that an API simply cannot support the operation they have been building around.&lt;/p&gt;

&lt;p&gt;The same should be true for services involving people.&lt;/p&gt;

&lt;p&gt;If a program only has established employer relationships in particular sectors, that matters.&lt;/p&gt;

&lt;p&gt;If certain types of work are outside its scope, that matters.&lt;/p&gt;

&lt;p&gt;If particular circumstances mean the service is unlikely to produce a useful outcome, that matters.&lt;/p&gt;

&lt;p&gt;The user needs that information early enough to make decisions about their own time.&lt;/p&gt;

&lt;p&gt;Otherwise, the system effectively hides its own limitations and leaves the user to discover them through repeated failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability matters
&lt;/h2&gt;

&lt;p&gt;Modern software systems rely heavily on observability.&lt;/p&gt;

&lt;p&gt;We want to know:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what happened,&lt;/li&gt;
&lt;li&gt;when it happened,&lt;/li&gt;
&lt;li&gt;where it happened,&lt;/li&gt;
&lt;li&gt;why it happened,&lt;/li&gt;
&lt;li&gt;how long it took,&lt;/li&gt;
&lt;li&gt;where the failure occurred,&lt;/li&gt;
&lt;li&gt;and what happened afterward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without observability, debugging becomes guesswork.&lt;/p&gt;

&lt;p&gt;The same principle should apply to public systems.&lt;/p&gt;

&lt;p&gt;If a program claims to connect people with employers, we should be able to answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which employers have established relationships with the program?&lt;/li&gt;
&lt;li&gt;Which sectors are covered?&lt;/li&gt;
&lt;li&gt;How many participants were successfully connected through those relationships?&lt;/li&gt;
&lt;li&gt;How many were simply advised to submit standard applications?&lt;/li&gt;
&lt;li&gt;How many cases were identified as unsuitable?&lt;/li&gt;
&lt;li&gt;What happened to those people afterward?&lt;/li&gt;
&lt;li&gt;How many received an effective onward referral?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't hostile questions.&lt;/p&gt;

&lt;p&gt;They're essentially &lt;strong&gt;observability questions&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If the system cannot answer them, then it becomes difficult to determine whether the system is performing the function it was designed to perform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metrics can create the wrong behavior
&lt;/h2&gt;

&lt;p&gt;This is another familiar problem in software engineering.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What you measure influences what you optimize for.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If an engineering team is measured primarily on the number of tickets closed, engineers will naturally become better at closing tickets.&lt;/p&gt;

&lt;p&gt;That doesn't necessarily mean the product is improving.&lt;/p&gt;

&lt;p&gt;If a call center is measured primarily on call duration, reducing call duration becomes an obvious optimization target.&lt;/p&gt;

&lt;p&gt;That doesn't necessarily mean customers are getting their problems solved.&lt;/p&gt;

&lt;p&gt;The same problem can exist in public services.&lt;/p&gt;

&lt;p&gt;If success is primarily represented through:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;applications,&lt;/li&gt;
&lt;li&gt;appointments,&lt;/li&gt;
&lt;li&gt;interactions,&lt;/li&gt;
&lt;li&gt;cases,&lt;/li&gt;
&lt;li&gt;referrals,&lt;/li&gt;
&lt;li&gt;and completed processes,&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;then the organization can become extremely good at generating activity.&lt;/p&gt;

&lt;p&gt;But activity isn't necessarily progress.&lt;/p&gt;

&lt;p&gt;The difficult metric is the one that matters most:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What changed for the person?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Did they get employment?&lt;/p&gt;

&lt;p&gt;Did they receive the support they needed?&lt;/p&gt;

&lt;p&gt;Did the barrier actually get removed?&lt;/p&gt;

&lt;p&gt;Did they reach another service that could help them?&lt;/p&gt;

&lt;p&gt;Those are outcome metrics.&lt;/p&gt;

&lt;p&gt;And outcomes are often considerably harder to measure than activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  The user becomes part of the error-handling layer
&lt;/h2&gt;

&lt;p&gt;There is another cost that doesn't always appear in system metrics.&lt;/p&gt;

&lt;p&gt;When a system fails, somebody has to compensate for the failure.&lt;/p&gt;

&lt;p&gt;In software, that might mean a retry mechanism, a dead-letter queue, a fallback service, or an operator intervention.&lt;/p&gt;

&lt;p&gt;In poorly designed human-facing systems, that responsibility can quietly move onto the user.&lt;/p&gt;

&lt;p&gt;A form fails, so they try again.&lt;/p&gt;

&lt;p&gt;An email goes unanswered, so they follow up.&lt;/p&gt;

&lt;p&gt;A verbal commitment isn't recorded, so they document it themselves.&lt;/p&gt;

&lt;p&gt;A case is transferred, so they reconstruct the history.&lt;/p&gt;

&lt;p&gt;A process doesn't explain what is happening, so they maintain their own timeline.&lt;/p&gt;

&lt;p&gt;Eventually, the person becomes the system's external state store.&lt;/p&gt;

&lt;p&gt;They keep the records.&lt;/p&gt;

&lt;p&gt;They track the state.&lt;/p&gt;

&lt;p&gt;They reproduce missing context.&lt;/p&gt;

&lt;p&gt;They provide the observability the system itself failed to provide.&lt;/p&gt;

&lt;p&gt;From a developer's perspective, that's a serious design smell.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The system has outsourced state management to the user.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Documentation becomes infrastructure
&lt;/h2&gt;

&lt;p&gt;This is why documenting every interaction can become so important.&lt;/p&gt;

&lt;p&gt;Emails, dates, appointments, names, decisions, promises, and supporting documents effectively become a second system running alongside the official one.&lt;/p&gt;

&lt;p&gt;The user maintains it because the official system may not reliably preserve the context they need.&lt;/p&gt;

&lt;p&gt;Imagine having to manually maintain your own audit log because a production system doesn't provide one.&lt;/p&gt;

&lt;p&gt;You would consider that an architectural problem.&lt;/p&gt;

&lt;p&gt;Yet people navigating complicated administrative systems can end up doing exactly this.&lt;/p&gt;

&lt;p&gt;They become responsible for preserving the state of their own case.&lt;/p&gt;

&lt;p&gt;That work has a cost.&lt;/p&gt;

&lt;p&gt;It takes time.&lt;/p&gt;

&lt;p&gt;It takes attention.&lt;/p&gt;

&lt;p&gt;It takes organization.&lt;/p&gt;

&lt;p&gt;It takes emotional energy.&lt;/p&gt;

&lt;p&gt;And none of those costs necessarily appear in the institution's metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is part of system design
&lt;/h2&gt;

&lt;p&gt;This becomes even more important when the system is intended to support disabled people.&lt;/p&gt;

&lt;p&gt;A system that requires sustained attention, precise documentation, repeated communication, and extensive administrative work may unintentionally create barriers for the very people it is intended to support.&lt;/p&gt;

&lt;p&gt;That doesn't necessarily mean every requirement should be removed.&lt;/p&gt;

&lt;p&gt;It means the system needs to account for the users who have to operate it.&lt;/p&gt;

&lt;p&gt;In software, we call this accessibility.&lt;/p&gt;

&lt;p&gt;We don't expect every user to interact with a system in exactly the same way. We provide alternatives, reduce unnecessary friction, and design interfaces around the capabilities and needs of the people using them.&lt;/p&gt;

&lt;p&gt;Human services should be treated with the same principle.&lt;/p&gt;

&lt;p&gt;If completing the process requires overcoming the same kind of barrier that the service is supposed to help with, then the architecture deserves examination.&lt;/p&gt;

&lt;p&gt;You shouldn't need the outcome in order to have the capacity to navigate the process that produces the outcome.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure should be visible
&lt;/h2&gt;

&lt;p&gt;A well-designed system makes failure observable.&lt;/p&gt;

&lt;p&gt;If a dependency is unavailable, the system should say so.&lt;/p&gt;

&lt;p&gt;If an input is invalid, it should explain why.&lt;/p&gt;

&lt;p&gt;If a request cannot be fulfilled, it should fail clearly.&lt;/p&gt;

&lt;p&gt;And if the system itself cannot provide the required service, that limitation should be visible as early as possible.&lt;/p&gt;

&lt;p&gt;The alternative is a system where failure is hidden behind continued activity.&lt;/p&gt;

&lt;p&gt;The user keeps attending appointments.&lt;/p&gt;

&lt;p&gt;More forms are submitted.&lt;/p&gt;

&lt;p&gt;More emails are exchanged.&lt;/p&gt;

&lt;p&gt;The case remains open.&lt;/p&gt;

&lt;p&gt;The process continues.&lt;/p&gt;

&lt;p&gt;Eventually, months later, the system reaches the same conclusion it could potentially have reached at the beginning.&lt;/p&gt;

&lt;p&gt;From the organization's perspective, there was plenty of activity.&lt;/p&gt;

&lt;p&gt;From the user's perspective, there was no progress.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, when is a system not fit for purpose?
&lt;/h2&gt;

&lt;p&gt;I don't think the answer is simply "when the system isn't perfect."&lt;/p&gt;

&lt;p&gt;No serious system is perfect.&lt;/p&gt;

&lt;p&gt;And I don't think saying no is inherently a failure.&lt;/p&gt;

&lt;p&gt;Sometimes the correct system behavior is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We cannot help you with this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The important part is what happens next.&lt;/p&gt;

&lt;p&gt;A system becomes questionable when it &lt;strong&gt;cannot identify its own boundaries, cannot communicate them clearly, and can continue consuming a person's time and resources without producing a meaningful outcome.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In software, we'd investigate that.&lt;/p&gt;

&lt;p&gt;We'd look at the architecture.&lt;/p&gt;

&lt;p&gt;We'd examine the state transitions.&lt;/p&gt;

&lt;p&gt;We'd inspect the failure modes.&lt;/p&gt;

&lt;p&gt;We'd check the metrics.&lt;/p&gt;

&lt;p&gt;We'd look at the monitoring.&lt;/p&gt;

&lt;p&gt;And we'd ask whether the system is actually optimizing for the thing that matters.&lt;/p&gt;

&lt;p&gt;Public services deserve the same level of scrutiny.&lt;/p&gt;

&lt;p&gt;Because the ultimate test isn't whether the process ran.&lt;/p&gt;

&lt;p&gt;It's whether the system achieved its purpose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A system can be operational, compliant, fully staffed, and processing thousands of cases while still not be fit for purpose.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes the most important failure isn't that the system crashed.&lt;/p&gt;

&lt;p&gt;It's that &lt;strong&gt;the system kept running exactly as designed while the person it was supposed to help went nowhere.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>systemsdesign</category>
      <category>a11y</category>
      <category>technology</category>
      <category>publicservices</category>
    </item>
    <item>
      <title>Learning to Build Without a Ladder</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 06 Sep 2026 00:52:55 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/learning-to-build-without-a-ladder-4bmm</link>
      <guid>https://dev.to/dobybaxter127/learning-to-build-without-a-ladder-4bmm</guid>
      <description>&lt;p&gt;I never got the junior job.&lt;/p&gt;

&lt;p&gt;Not for lack of trying. I spent well over a year applying, building, learning, and applying again. For long stretches I could not get past the first filter, let alone into a room with an engineer. And I am not unusual. I am one of a very large group of people who entered software and security over the past five years or so and found that the door marked "start here" had quietly been taken off its hinges.&lt;/p&gt;

&lt;p&gt;We are often described as self-taught, as if that were a personality trait or a lifestyle choice. For most of us it was not a choice. It was the only path left standing once the others were dismantled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ladder that used to exist
&lt;/h2&gt;

&lt;p&gt;The traditional way you became a good engineer looked roughly like this:&lt;/p&gt;

&lt;p&gt;learn, get a junior role, make mistakes under supervision, become competent, become mid-level, eventually become senior.&lt;/p&gt;

&lt;p&gt;Every rung mattered, and the early ones mattered most, because that is where you were allowed to be wrong cheaply. Someone experienced sat near you and said "not there, we tried that two years ago, here is what happened." That sentence is worth more than any tutorial ever written.&lt;/p&gt;

&lt;p&gt;What replaced it, for a lot of people, is this:&lt;/p&gt;

&lt;p&gt;learn, apply, rejected, apply, rejected, apply, rejected, somehow already be experienced.&lt;/p&gt;

&lt;p&gt;And then the same industry turns around and says it needs experienced developers. Where, exactly, are they supposed to come from? You cannot eliminate the junior opportunities and then complain that there is a shortage of seniors. Those seniors had to be made somewhere. The industry has been eating its seed corn and acting surprised at the harvest.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is that no single company is behaving irrationally. Each one reasonably asks why it should train someone for two years when it can hire a person who already knows the system, especially when hundreds of thousands of experienced developers have been laid off and are already on the market. Individually rational, collectively ruinous. The full cost does not show up on a quarterly report. It shows up years later, when the people who could have become the next generation of seniors were never given the chance to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did instead
&lt;/h2&gt;

&lt;p&gt;If the apprenticeship was gone, I had to build my own. So I went where the door was still open: open source.&lt;/p&gt;

&lt;p&gt;Open source has always been one of the few places where you can say "I do not have a job yet, but I want to become good," and actually be taken up on it. You find a project. You read the code until it stops being frightening. You fix something small. You submit it. Someone reviews it and tells you, plainly, what is wrong. You improve. You come back for something harder. That loop is an apprenticeship. It is the apprenticeship, just without the salary or the org chart.&lt;/p&gt;

&lt;p&gt;Here is what that looked like for me, concretely.&lt;/p&gt;

&lt;p&gt;I contributed a fix to GitLab core that was reviewed, merged by a maintainer, and shipped to production in a release. Small in scope, but it was real code in a system that millions of people depend on, held to the standard of people who maintain that system for a living.&lt;/p&gt;

&lt;p&gt;I built the Pyxel Config Lab, a tool for building and validating configuration for the European Space Agency's Pyxel detector-simulation framework. It was credited by ESA in an official release announcement. I did not have a job at ESA. I had a problem I understood and the willingness to solve it in the open.&lt;/p&gt;

&lt;p&gt;I started contributing to OpenSPP, an open-source social protection platform, because reliability and validation are what I care about most and because that work touches systems that actually matter to people's lives. My first contribution there was, characteristically, about validation: making sure a record could not accept an impossible value and quietly produce nonsense downstream.&lt;/p&gt;

&lt;p&gt;I began working into OpenTelemetry, because the instinct that made me care about clean configuration and honest error messages is the same instinct that makes me care about whether a distributed system, or an AI system, can tell you the truth about what it is doing.&lt;/p&gt;

&lt;p&gt;Alongside that I built accessibility tooling as browser extensions, and I write. A lot. The writing is not a side project; it is how I think, and it is how a person with no institutional letterhead makes their work legible to strangers.&lt;/p&gt;

&lt;p&gt;None of this was assigned to me. Nobody onboarded me. I assembled the apprenticeship out of whatever was reachable, and the reachable thing was the open commons.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can learn from home
&lt;/h2&gt;

&lt;p&gt;Here is the part I want to be honest about, because I think the "just teach yourself" crowd gets it badly wrong in the other direction.&lt;/p&gt;

&lt;p&gt;Information is everywhere now. You can learn languages, databases, distributed systems, compilers, networking, and architecture from your bedroom. What you cannot fully learn there is what it feels like to be responsible for a system that has been running for six years, with dozens of developers, three generations of architectural decisions layered on top of each other, real customers depending on it, and someone telling you not to touch a particular corner because the last person who did caused an outage.&lt;/p&gt;

&lt;p&gt;That knowledge is situational. It is institutional memory. It is the difference between knowing what a database index is and having migrated a live database with users on it at an hour you would rather forget. No course, no book, and no chat assistant can hand you that. You have to live it, inside a real organization, with real consequences. Which is precisely the thing the closed door denies us.&lt;/p&gt;

&lt;p&gt;So I am not claiming the self-taught path is equivalent. I am claiming it is what was left, that many of us became genuinely capable through it, and that the fact we survived the missing ladder does not prove the ladder was unnecessary. It proves how resourceful people get when a system fails them.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI is an accelerant, and the direction is a choice
&lt;/h2&gt;

&lt;p&gt;I work AI-assisted. I do not type every line unaided from a blank file, and I am not embarrassed by that. I remain accountable for every design decision and can explain any of them. For someone learning without a senior at the next desk, an AI collaborator is genuinely powerful: it will explain an unfamiliar architecture, propose alternatives, surface edge cases, write tests that expose your own assumptions, and shorten the hours you would otherwise spend stuck. That is real mentorship-shaped help, available to people the labor market gave nothing to.&lt;/p&gt;

&lt;p&gt;But the tool is neutral, and the institutions using it are not. Picture two companies with the identical AI.&lt;/p&gt;

&lt;p&gt;Company A says: apprentices use this, seniors review their work and teach them why things are wrong. AI becomes an accelerator for apprenticeship. You can fast-track a motivated beginner into a productive engineer faster and more cheaply than before, and they want to be there, so the company wins too.&lt;/p&gt;

&lt;p&gt;Company B says: the AI writes most of the code, so we do not need juniors, give us a handful of seniors. AI becomes an accelerator for headcount reduction, and the learning layer is deleted.&lt;/p&gt;

&lt;p&gt;Same technology, opposite outcomes. Company B looks better this quarter. Company B is also quietly ensuring there are no seniors to hire in five years.&lt;/p&gt;

&lt;p&gt;There is a bitter twist here for people like me. The same industry that shut the door increasingly rejects the way self-taught engineers actually work. I have been ruled out of roles for using AI in an assessment, judged by a process that bans the exact tool the situation forced me to master, while the label "vibe coder" gets thrown at anyone who works this way. You cannot remove the apprenticeship, hand people a tool to survive without it, and then penalize them for having learned to use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flood is fine. Judgment is the skill.
&lt;/h2&gt;

&lt;p&gt;People worry about the flood: too many apps, too many libraries, too many books, too much of everything now that the barrier to creation has dropped. I do not share the worry. Lowering the barrier to making things is good. If someone who could never program before builds a small game for their friends, wonderful. Not every act of creation has to be economically optimized, any more than every painting has to sell.&lt;/p&gt;

&lt;p&gt;The real line is not between AI-generated and hand-written, and it is not between accessible and gatekept. It is between creation connected to understanding and creation disconnected from it, in the places where correctness actually matters. Accessibility is wonderful. Learning is necessary. Blind delegation is dangerous. "Ban the tool" answers none of that. The skill that matters now is judgment: can the person understand, test, and improve what the tool produced. That is the thing we should be teaching, and it is exactly the thing you build by doing real work under review, which is exactly the thing being taken away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Engineering is not a business subplot
&lt;/h2&gt;

&lt;p&gt;Business constraints are real. Cost, deadlines, regulation, and risk shape engineering, and an engineer who pretends otherwise builds beautiful things nobody can afford. But there is a healthy boundary, and it has been crossed repeatedly. Business should set the constraints and the goals. Engineering should decide how those goals can be met safely and realistically. When leadership starts dictating technical reality instead, that this will take two weeks, that tests are optional, that the generated code is fine as-is, that another senior is an unnecessary expense, that the mentorship layer can go, you get exactly the collapse we are living through. A society can outsource the production of goods. It cannot indefinitely outsource the production of expertise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The generation that learned to build without a ladder
&lt;/h2&gt;

&lt;p&gt;That is not a happy ending, and I will not pretend it is one. The ladder mattered. Removing it hurt real people and is storing up a shortage the industry will feel for years. But it is also true that the commons is still open, that the work is still real, and that a reviewer on an open-source project will still tell you the truth about your code, which is more than most job applications ever will.&lt;/p&gt;

&lt;p&gt;I am building my own path now as an independent engineer, and I try to pass the ladder back down where I can, including teaching basic digital skills to people who have even less access than I did. Not because the system worked, but because it did not, and someone has to keep the door propped open.&lt;/p&gt;

&lt;p&gt;The question I would put to the industry is simple, and it is not "will AI replace programmers." It is this: who is going to produce tomorrow's experienced engineers, if today's companies keep removing every opportunity to become one?&lt;/p&gt;

</description>
      <category>career</category>
      <category>opensource</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Enforcing Data Invariants in Odoo: Model Constraints, Migrations, and Tests That Actually Cover the Fix</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sat, 05 Sep 2026 14:30:10 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/enforcing-data-invariants-in-odoo-model-constraints-migrations-and-tests-that-actually-cover-the-1le0</link>
      <guid>https://dev.to/dobybaxter127/enforcing-data-invariants-in-odoo-model-constraints-migrations-and-tests-that-actually-cover-the-1le0</guid>
      <description>&lt;p&gt;This post collects a few things I ran into while contributing fixes to OpenSPP, a social-protection platform built on Odoo. They are all variations on one question: when a value has to satisfy a rule, where does that rule need to live so it holds on every path the value can arrive through.&lt;/p&gt;

&lt;h2&gt;
  
  
  Write paths in Odoo
&lt;/h2&gt;

&lt;p&gt;A stored model field can be written through several paths:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The form UI.&lt;/li&gt;
&lt;li&gt;ORM &lt;code&gt;create()&lt;/code&gt; and &lt;code&gt;write()&lt;/code&gt; from other code.&lt;/li&gt;
&lt;li&gt;CSV and Excel import (&lt;code&gt;load()&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;Remote writes over XML-RPC, the API, and DCI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These do not share the same hooks. Where a guard is placed determines which of these paths it covers.&lt;/p&gt;

&lt;h2&gt;
  
  
  The relevant decorators
&lt;/h2&gt;

&lt;p&gt;Three decorators come up, and the difference between them is the whole point:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Decorator&lt;/th&gt;
&lt;th&gt;Fires when&lt;/th&gt;
&lt;th&gt;Covers import / RPC / ORM&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@api.onchange("field")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;In the form UI only, as the user edits&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@api.constrains("field")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;On every create/write touching the field&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;@api.depends("field")&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;When a computed field recomputes&lt;/td&gt;
&lt;td&gt;Yes, for stored computes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;onchange&lt;/code&gt; runs client-side in the form. &lt;code&gt;constrains&lt;/code&gt; runs server-side on the stored field. If a rule needs to hold everywhere, it has to be a &lt;code&gt;constrains&lt;/code&gt; (or a database constraint), not an &lt;code&gt;onchange&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: a future birthdate
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;res.partner.birthdate&lt;/code&gt; had a single guard, an &lt;code&gt;onchange&lt;/code&gt; that reset the field if the date was in the future. That guard only runs in the form, so ORM writes, imports, and API writes all stored future dates. The non-stored &lt;code&gt;age&lt;/code&gt; compute then rendered those as negative values.&lt;/p&gt;

&lt;p&gt;The fix was to add a stored-field &lt;code&gt;@api.constrains("birthdate")&lt;/code&gt;. Because the field is stored and writeable, the constraint fires on create, write, and import, so the rule no longer depends on which path the value came through. The &lt;code&gt;onchange&lt;/code&gt; can stay as immediate form feedback, but it is no longer the only check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalid state propagates to other fields
&lt;/h2&gt;

&lt;p&gt;A second case shows why the location of the check matters beyond the field itself. A plan model had an &lt;code&gt;is_current&lt;/code&gt; flag meaning "the current plan for this case." Completing a plan set its state to &lt;code&gt;completed&lt;/code&gt; but never cleared &lt;code&gt;is_current&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Downstream, several things read that pair:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A derived &lt;code&gt;current_plan_id&lt;/code&gt; (based on &lt;code&gt;is_current&lt;/code&gt;) kept pointing at the completed plan.&lt;/li&gt;
&lt;li&gt;A "has an active plan" check read false.&lt;/li&gt;
&lt;li&gt;A one-current-plan-per-case constraint then refused to let a new plan be marked current, because the completed plan still held the flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The incoherent value was written in one place and surfaced as a failure elsewhere: a user could not mark a new plan as current. The fix folded &lt;code&gt;is_current = False&lt;/code&gt; into the same &lt;code&gt;write()&lt;/code&gt; that sets the completed state, so every path that completes a plan also releases the flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  A code fix only covers future writes
&lt;/h2&gt;

&lt;p&gt;A write-time constraint or a corrected &lt;code&gt;write()&lt;/code&gt; does not touch rows that are already in the bad state. Existing deployments still held completed plans flagged as current, and those cases stayed blocked after the fix shipped.&lt;/p&gt;

&lt;p&gt;That needs a separate data migration. When the manifest version increases, Odoo runs matching migration scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module/
  migrations/
    19.0.2.0.1/
      post-migration.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;migrate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE spp_case_intervention_plan &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SET is_current = false &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;WHERE state = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;completed&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; AND is_current = true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notes on the migration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The signature is always &lt;code&gt;def migrate(cr, version):&lt;/code&gt;, and &lt;code&gt;cr&lt;/code&gt; is the raw cursor.&lt;/li&gt;
&lt;li&gt;Keep the SQL literal. No f-strings, &lt;code&gt;.format()&lt;/code&gt;, or composed identifiers, or the security linters (Semgrep, pylint-odoo) flag it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;if not version:&lt;/code&gt; guard skips fresh installs, where there is nothing to repair.&lt;/li&gt;
&lt;li&gt;Repairing existing rows and preventing new bad rows are two separate jobs. Both are needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Fixture and demo data can re-seed the bad state
&lt;/h2&gt;

&lt;p&gt;The same module's demo data generator was creating the bad state directly (writing &lt;code&gt;state = "completed"&lt;/code&gt; without going through the completion path, and passing &lt;code&gt;is_current: True&lt;/code&gt; alongside a completed state). A migration does not help here, because a fresh install recreates the rows after the migration would have run.&lt;/p&gt;

&lt;p&gt;The fix was to route both generator sites through the real completion action, so generated demo data satisfies the same invariant as production data. Worth checking the code that seeds data, not only the code that validates it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that pass without the fix
&lt;/h2&gt;

&lt;p&gt;A test can go green for a reason unrelated to the code under test. Odoo applies field defaults before running validation, and &lt;code&gt;_validate_fields&lt;/code&gt; fires constraints on the defaulted fields too.&lt;/p&gt;

&lt;p&gt;In this model, &lt;code&gt;registration_date&lt;/code&gt; defaults to today, and a pre-existing constraint requires registration date to be after birthdate. So creating a record with a future birthdate raised a &lt;code&gt;ValidationError&lt;/code&gt; from that older constraint, before the new birthdate constraint ran. The test asserted only the exception type, so it passed with the new constraint removed.&lt;/p&gt;

&lt;p&gt;Two ways to make such a test actually cover the fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Assert on the message, for example &lt;code&gt;assertRaisesRegex(ValidationError, "Date of birth cannot be in the future")&lt;/code&gt;, so you know which constraint fired.&lt;/li&gt;
&lt;li&gt;Or pass explicit values for the defaulted fields so only the constraint under test can raise.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful check either way: revert the fix locally and confirm the test fails. If it stays green, it is not covering the change.&lt;/p&gt;

&lt;p&gt;It also helps to assert the reported symptom rather than the internal flag. If the bug was "a completed plan blocks marking a new one current," the test should complete the plan and then create a second plan and assert it does not raise, which is the behavior a revert has to break.&lt;/p&gt;

&lt;h2&gt;
  
  
  Date comparisons and timezone
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;fields.Date.today()&lt;/code&gt; returns the server date, which is effectively UTC. &lt;code&gt;fields.Date.context_today(record)&lt;/code&gt; returns the date in the acting user's timezone.&lt;/p&gt;

&lt;p&gt;For a "not in the future" check, the server date can reject a valid date for users ahead of UTC. A user in Pacific/Auckland at 10:00 local is already on the next calendar day while the server is still on the previous one, so a same-day birthdate compares as "in the future" against the server date. Using &lt;code&gt;context_today(record)&lt;/code&gt; compares against the user's date instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;For a rule that must always hold:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforce it with &lt;code&gt;@api.constrains&lt;/code&gt; or a database constraint, on the stored field, not with &lt;code&gt;@api.onchange&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Add a migration to repair rows already in the invalid state.&lt;/li&gt;
&lt;li&gt;Check fixture and demo data so they do not re-seed the invalid state.&lt;/li&gt;
&lt;li&gt;Write the test to fail without the fix, and assert the reported symptom.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;context_today&lt;/code&gt; for date comparisons that depend on the user's calendar day.&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>python</category>
      <category>odoo</category>
      <category>softwareengineering</category>
      <category>testing</category>
    </item>
    <item>
      <title>If GOV.UK Knows the Application Is Invalid, Why Did It Take My Payment?</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Fri, 21 Aug 2026 13:22:32 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/if-govuk-knows-the-application-is-invalid-why-did-it-take-my-payment-2o3a</link>
      <guid>https://dev.to/dobybaxter127/if-govuk-knows-the-application-is-invalid-why-did-it-take-my-payment-2o3a</guid>
      <description>&lt;p&gt;Maybe this is another one of those posts where I end up roasting the government. But stick with me, because this isn't really a politics post. It's a post about state machines, validation layers, and what happens when you put an irreversible action at the end of a workflow that never once checked whether the user should have been allowed to reach it.&lt;/p&gt;

&lt;p&gt;The government part is just the catalyst. The engineering part is the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually happened
&lt;/h2&gt;

&lt;p&gt;I applied for a British passport on GOV.UK.&lt;/p&gt;

&lt;p&gt;Early in the form, it asks something like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do you have a certificate of naturalisation or a certificate of registration?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There are a lot of yes/no questions on that journey, and this one behaves exactly like the rest of them. I answered &lt;strong&gt;No&lt;/strong&gt;, and the form did what it always does: it let me continue.&lt;/p&gt;

&lt;p&gt;So I continued. I answered the remaining questions. I entered my personal details. I entered my bank details. The system accepted my payment of £107.&lt;/p&gt;

&lt;p&gt;Only afterward did I learn that the certificate wasn't supporting evidence I could send in later. It was a &lt;strong&gt;prerequisite&lt;/strong&gt; — something I needed &lt;em&gt;before&lt;/em&gt; the application was valid at all.&lt;/p&gt;

&lt;p&gt;I asked whether the application could be paused while I resolved the prerequisite. No. I was told I had roughly three months, and that the payment is non-refundable. If I can't complete everything in that window, the money is simply gone.&lt;/p&gt;

&lt;p&gt;Here's the part that matters as an engineer: &lt;strong&gt;the system had my answer from the very beginning.&lt;/strong&gt; I told it "No" on question three. It took my money on question twenty-something. Nothing in between used the information it already had.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem isn't the question
&lt;/h2&gt;

&lt;p&gt;Asking "Do you have certificate X? Yes/No" is completely fine. That's not the bug.&lt;/p&gt;

&lt;p&gt;The bug is what the system does &lt;em&gt;after&lt;/em&gt; the answer.&lt;/p&gt;

&lt;p&gt;A binary question that gates eligibility should not behave identically to a binary question that merely records a preference. If "No" makes the entire application invalid, then "No" is not informational — it's a &lt;strong&gt;hard gate&lt;/strong&gt;. But the interface gave me no signal that I'd just walked through a gate. It looked like every other question, so I reasonably read it as every other question.&lt;/p&gt;

&lt;p&gt;That's a semantic failure dressed up as a UI detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Input validation was never the issue
&lt;/h2&gt;

&lt;p&gt;This is where my actual specialization kicks in — I spend most of my working life on configuration, error messaging, validation, and deterministic workflows — so let me be precise about &lt;em&gt;which&lt;/em&gt; validation failed, because "the form should validate better" is too vague to be useful.&lt;/p&gt;

&lt;p&gt;There are several distinct layers, and they are not interchangeable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Syntactic validation&lt;/strong&gt; — is this field shaped correctly? (Is the date a date?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic validation&lt;/strong&gt; — does this value make sense in context? (Is the date in the past?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business-rule validation&lt;/strong&gt; — is this allowed by the rules of the domain? (Are you old enough to apply?)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Workflow validation&lt;/strong&gt; — given everything you've told me &lt;em&gt;so far&lt;/em&gt;, are you eligible to be at this step at all?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submission validation&lt;/strong&gt; — is the application in a state where it can legally, financially, and administratively be finalized?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My passport form almost certainly nailed the first three. Every field was well-formed. Nothing was syntactically wrong.&lt;/p&gt;

&lt;p&gt;It failed at &lt;strong&gt;workflow validation&lt;/strong&gt; and &lt;strong&gt;submission validation&lt;/strong&gt; — the two layers that actually protect the user from spending money on something that cannot succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let an invalid state propagate
&lt;/h2&gt;

&lt;p&gt;Here's the conceptual shape of what should have happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;START
  → eligibility questions
  → prerequisite missing
  → [ STOP: application cannot proceed ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's what actually happened:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;START
  → answer indicates prerequisite missing
  → continue
  → continue
  → continue
  → payment accepted
  → application submitted
  → "you cannot actually complete this process"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An invalid state that is detectable at step 3 should never be allowed to survive until step 20. In any other production system, we'd call that &lt;strong&gt;error propagation&lt;/strong&gt; — a known-bad condition carried silently downstream until it detonates at the most expensive possible point.&lt;/p&gt;

&lt;p&gt;The fix doesn't require anything clever. It doesn't need machine learning or a smarter form. It needs one correctly defined state transition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prerequisite_required&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;prerequisite_present&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;block_submission&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;explain_why&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;explain_how_to_resolve&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;preserve_progress&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Never gate an irreversible action on an optimistic assumption
&lt;/h2&gt;

&lt;p&gt;Payment is the textbook example of an irreversible action. Money leaves your account. The system tells you it won't come back.&lt;/p&gt;

&lt;p&gt;So the condition for accepting payment cannot be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payment_allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_reached_payment_page&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has to be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;payment_allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;application_state&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;READY_FOR_SUBMISSION&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reaching the payment screen is a fact about &lt;em&gt;navigation&lt;/em&gt;. Being ready to submit is a fact about &lt;em&gt;eligibility&lt;/em&gt;. The system conflated the two, and the user paid for the difference.&lt;/p&gt;

&lt;p&gt;Any time an action is irreversible — payment, submission, deletion, dispatch — the system should treat it as a boundary that requires the whole prior state to be provably valid, not merely reachable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real world is asynchronous, and the workflow needs to model that
&lt;/h2&gt;

&lt;p&gt;This is the part administrative software chronically gets wrong.&lt;/p&gt;

&lt;p&gt;Completing a prerequisite in real life is rarely something the user can just &lt;em&gt;do&lt;/em&gt;. It might depend on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an appointment with a third party,&lt;/li&gt;
&lt;li&gt;a document held by someone else,&lt;/li&gt;
&lt;li&gt;a certificate that must be requested, issued, posted, scanned, and certified,&lt;/li&gt;
&lt;li&gt;a process with its own multi-week turnaround.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that happens synchronously just because the applicant wants it to.&lt;/p&gt;

&lt;p&gt;So a good workflow has to distinguish between two states that look identical on the surface but are completely different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"The user hasn't done this yet."&lt;/strong&gt; (Their responsibility. Nudge them.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"The user cannot currently do this because another process hasn't completed."&lt;/strong&gt; (Not their responsibility. Wait, don't penalize.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A three-month non-refundable countdown that assumes every dependency is under the user's control is modeling the wrong reality. It's charging people for the latency of institutions they don't control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility isn't only screen readers
&lt;/h2&gt;

&lt;p&gt;Here's a distinction I wish more teams internalized: &lt;strong&gt;accessibility is not just a property of the UI. It's a property of the workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A service can have perfect semantic HTML, keyboard navigation, ARIA labels, screen-reader support, and AAA contrast — and still be &lt;em&gt;functionally inaccessible&lt;/em&gt;, because the underlying process is confusing, unforgiving, and impossible to recover from.&lt;/p&gt;

&lt;p&gt;A workflow that quietly assumes users can instantly obtain every prerequisite, infer every unstated dependency, remember every required document, and correctly reinterpret an ambiguous yes/no question is already fragile for everyone. For anyone dealing with cognitive load, memory difficulty, limited resources, or dependence on third parties, that fragility stops being an inconvenience and becomes a &lt;strong&gt;wall&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You can pass every automated accessibility audit and still ship something that's impossible for a real person to complete safely. GOV.UK's design system is genuinely excellent at the component level. This failure lives one layer up, in the workflow, where the audits don't look.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a fail-safe version would do
&lt;/h2&gt;

&lt;p&gt;None of this is exotic. Concretely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dependency-aware questions&lt;/strong&gt; — a gating answer visibly changes the journey instead of blending in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hard eligibility gates&lt;/strong&gt; — invalid state stops at detection, not at checkout.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit prerequisite explanations&lt;/strong&gt; — say &lt;em&gt;why&lt;/em&gt; you're blocked and &lt;em&gt;how&lt;/em&gt; to fix it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent, resumable progress&lt;/strong&gt; — don't force people to gamble against a clock they don't control.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-payment validation&lt;/strong&gt; — establish eligibility &lt;em&gt;before&lt;/em&gt; taking money, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asynchronous dependency states&lt;/strong&gt; — model "waiting on someone else" as a first-class status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Human escalation paths&lt;/strong&gt; — a real route to intervene when the machine gets it wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback where the system permitted an invalid transaction&lt;/strong&gt; — if the service let money through in a state it should have blocked, the refund is the system's responsibility, not the user's problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The uncomfortable question
&lt;/h2&gt;

&lt;p&gt;So here's where I'll leave it.&lt;/p&gt;

&lt;p&gt;If a system lets a user make an error, already holds enough information to recognize the consequences of that error, allows the error to propagate through the entire workflow, accepts payment, and &lt;em&gt;then&lt;/em&gt; refuses to let the user recover — where exactly did the error occur?&lt;/p&gt;

&lt;p&gt;It did not occur the moment the user clicked the wrong button.&lt;/p&gt;

&lt;p&gt;It occurred when the system was designed without a safe state in which that mistake could be recovered from.&lt;/p&gt;

&lt;p&gt;That's the bug. Everything else is just where it happened to surface.&lt;/p&gt;

</description>
      <category>uk</category>
      <category>ux</category>
      <category>govuk</category>
      <category>gov</category>
    </item>
  </channel>
</rss>
