<?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%2Fb399b9df-89c3-4ef8-ac4c-d0b2e50f3256.jpg</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>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>
    <item>
      <title>A Floor Beneath Every Person: Design Choices in the First Social Resource Floor Blueprint</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Fri, 14 Aug 2026 00:12:29 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/a-floor-beneath-every-person-design-choices-in-the-first-social-resource-floor-blueprint-2hb5</link>
      <guid>https://dev.to/dobybaxter127/a-floor-beneath-every-person-design-choices-in-the-first-social-resource-floor-blueprint-2hb5</guid>
      <description>&lt;p&gt;TL;DR — I've been building the Social Resource Floor: an open blueprint for coordinating one person's access to basic survival resources — food, housing, energy, healthcare, and more — across many independent providers, so that reaching those resources is grounded in being human rather than in financial access. The first blueprint version is now complete: language-neutral schemas, prose specifications, a reference implementation, and a first adapter. This post is about the engineering choices behind it, and the reasons for each — how it stays a contract rather than a product, how it keeps personal data out of the coordination layer, why it binds to existing standards instead of inventing new ones, and how I check that the contracts are implementation-independent rather than just claiming they are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem the Floor is trying to help with
&lt;/h2&gt;

&lt;p&gt;Today, for most people, survival routes through financial access. To reach food, housing, energy, or healthcare you generally need money, and to hold or move money you need banking, employment, or purchasing power. Financial access has become the gate standing in front of the resources a person needs to stay alive.&lt;/p&gt;

&lt;p&gt;The goal of the Social Resource Floor is narrow and specific: to help make it so that financial status is not the condition that determines whether a person can reach the basic resources required to survive. It does not try to abolish money, banks, or markets — money stays a first-class resource and delivery method. It aims at one thing: a floor beneath which no person should fall, defined locally, reachable regardless of financial circumstances.&lt;/p&gt;

&lt;p&gt;That's the mission. Everything technical below exists to make that mission buildable by the institutions — governments, municipalities, NGOs, cooperatives, community providers — that would actually run it, without asking any of them to give up their own systems or hand over their data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the Floor sits
&lt;/h2&gt;

&lt;p&gt;The delivery systems for social protection already exist and are strong: OpenSPP for orchestration, OpenG2P for delivery rails, OpenCRVS for civil registration. The Floor is not a replacement for any of them. It's a coordination layer &lt;em&gt;above&lt;/em&gt; them, whose job is to let independent systems coordinate one person's whole survival floor — across many providers and many resource types at once — through shared, open interfaces, while each provider keeps its own mission and its own data.&lt;/p&gt;

&lt;p&gt;The whole design follows from that position. A layer that sits above independently-operated systems and coordinates without owning them has a specific set of constraints, and each of the choices below is an answer to one of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 1: Contracts first, implementation second
&lt;/h2&gt;

&lt;p&gt;The source of truth is a set of language-neutral JSON Schemas. Prose specifications describe their intent, a conformance suite validates against them, and a reference implementation demonstrates them — but nothing in the repository is permitted to depend on that reference implementation. Every dependency arrow points inward, toward the schemas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;schemas/           source of truth — depends on nothing
  ^   ^   ^
  |   |   |
specification/   conformance/   reference/ + adapters/
  (prose)        (validates)    (demonstrate, non-authoritative)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The guiding rule is &lt;em&gt;interface over implementation&lt;/em&gt;: the blueprint says what a component must be able to do, not which product must do it. The reason is adoption. If the reference implementation were the source of truth, then "conformant" would quietly come to mean "matches my code," and every participant would inherit my choices of language, database, and framework. Keeping the schemas authoritative and the reference deliberately non-authoritative means an independent party can build to the same contracts in a different stack and still interoperate. The concrete guardrail I hold myself to: if an adapter ever imports from the reference implementation, the dependency has inverted, and the project has started shipping a product instead of a contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 2: A small data spine
&lt;/h2&gt;

&lt;p&gt;The core data model is three nouns:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;provider&lt;/strong&gt; publishes resources.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;resource&lt;/strong&gt; is something a provider can supply — food, energy, housing, or money, which is one resource type among several rather than the organizing principle.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;entitlement&lt;/strong&gt; allocates a resource to a subject and names the provider responsible for fulfilling it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Around that spine sit six more schema areas — decision, provenance, event, consent, capability, and a public transparency report — for nine in total. Keeping the spine this small is deliberate: the fewer required concepts a participant has to adopt to join, the lower the barrier for a small community provider to become a full participant. Everything else is additive.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 3: Keep personal data out of the coordination layer
&lt;/h2&gt;

&lt;p&gt;A layer coordinating across many providers is a tempting place for personal data to accumulate, and that would be exactly the wrong outcome for a system meant to serve vulnerable people. So the design is data-minimizing by construction, not by policy.&lt;/p&gt;

&lt;p&gt;The mechanism is a single field, &lt;code&gt;subject_ref&lt;/code&gt;: an opaque, pseudonymous handle that means "the same subject, consistently, across the documents that need to correlate" — and nothing more. It carries no name, no cleartext ID number, no readable date of birth. Resolving it into actual facts about a person is done by systems that already do that — civil registries, national ID systems, credential issuers — entirely outside the Floor. The Floor never makes the &lt;code&gt;resolve()&lt;/code&gt; call itself; it only ever holds the opaque result. Identity is treated as an interface with zero provider lock-in, the same way policy is.&lt;/p&gt;

&lt;p&gt;This choice shows up throughout. The conformance suite resolves cross-references like a resource's &lt;code&gt;provider_id&lt;/code&gt; or an entitlement's &lt;code&gt;resource_id&lt;/code&gt;, but it deliberately never resolves &lt;code&gt;subject_ref&lt;/code&gt; — there's nothing there to resolve, and that absence is the privacy property working as intended. Event envelopes, which are built to be broadcast between systems, carry the same discipline: &lt;code&gt;subject_ref&lt;/code&gt; stays opaque and the payload carries coordination facts, never personal ones. Provenance records describe systems and operators, never subjects. A &lt;code&gt;subject_ref&lt;/code&gt; is also not globally stable by default — a handle minted for one provider or jurisdiction need not be the same string used elsewhere for the same person, which limits how far any correlation can travel.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 4: Bind to what exists; invent as little as possible
&lt;/h2&gt;

&lt;p&gt;The most consistent decision across the specification is that before defining anything, I checked whether a mature standard or an existing system already solved it, and bound to that rather than inventing a parallel version. Each spec area records what was checked first. In practice that meant:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Policy / eligibility.&lt;/strong&gt; The Floor does not define a rules language. It defines the interface a policy engine must sit behind — &lt;code&gt;evaluate(policy_id, policy_version, jurisdiction, subject_context, resource?) -&amp;gt; DecisionRecord&lt;/code&gt; — and names existing engines as candidate bindings: OpenFisca for legislation-as-code, CEL (which OpenSPP already uses for eligibility), and OPA/Rego for authorization-style rules. The decision record's &lt;code&gt;engine.binding&lt;/code&gt; field just names which one produced a given outcome. Any of them can be swapped without changing anything downstream.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consent.&lt;/strong&gt; Rather than a boolean flag on a subject's file, consent is modeled as records following the W3C Data Privacy Vocabulary and ISO/IEC TS 27560 — a status tied to a specific purpose, optionally narrowed to particular data categories or a particular provider/resource/decision. OpenSPP's own consent module is already DPV-aligned, so a participating deployment has something concrete to bind to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Identity.&lt;/strong&gt; Bound to DCI's typed-identifier lookups, W3C Verifiable Credentials with OIDC4VCI, and OIDC/eSignet/Keycloak as candidate resolvers — none mandated, all external to the Floor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization.&lt;/strong&gt; The cross-provider "which system may do what" question binds to OAuth2 client-credential scopes, the machine-to-machine pattern OpenSPP's API and DCI already use. It's kept separate from OpenSPP's &lt;code&gt;spp_user_roles&lt;/code&gt; RBAC, which solves a different problem — access control &lt;em&gt;within&lt;/em&gt; one deployment, not &lt;em&gt;between&lt;/em&gt; independent providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Interoperability and capability discovery.&lt;/strong&gt; These follow the pattern G2P Connect already uses in production: named, versioned, independently-adoptable interface codes, where a participant implements only the codes relevant to its role and advertises which ones before an exchange begins.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reason for this discipline is partly humility and partly durability: a coordination layer that reinvents consent, identity, and policy would be both arrogant about work others have done well and brittle against the systems it's meant to sit above. Binding to established interfaces means the Floor inherits their maturity and stays swappable as they evolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 5: Append-only records, and reproducibility
&lt;/h2&gt;

&lt;p&gt;Two smaller choices support auditability, which matters more than usual when the records decide what real people receive.&lt;/p&gt;

&lt;p&gt;Records that change are never edited in place. A consent withdrawal is a &lt;em&gt;new&lt;/em&gt; record with &lt;code&gt;status: withdrawn&lt;/code&gt; and a &lt;code&gt;supersedes&lt;/code&gt; pointer to the record it revokes, producing an append-only history a subject or auditor can walk. Provenance corrections work the same way — a correction supersedes rather than silently overwrites.&lt;/p&gt;

&lt;p&gt;Decisions are built to be reproducible. &lt;code&gt;policy_version&lt;/code&gt; is required on every decision record, because a decision that can't be re-evaluated against a fixed policy version can't be honestly audited later, whether for an appeal or a review. The same reasoning is why capability descriptors pin a &lt;code&gt;schema_version&lt;/code&gt;: a version mismatch between two participants should be detectable before an exchange, not discovered as a validation failure after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choice 6: Federation, and capability-based participation
&lt;/h2&gt;

&lt;p&gt;The Floor is federated in the established sense — independent systems keeping control of their own operation and data, coordinating through shared open interfaces, in the pattern of email and the web where no one owns the protocol. It does not use or assume blockchain, ledgers, or tokens; "no single authority" refers to distributed governance, not to removing trusted parties, since providers, auditors, and operators remain trusted actors in accountable roles.&lt;/p&gt;

&lt;p&gt;Participation is capability-based: a participant declares and implements only the subset of areas relevant to its role. A small community provider might only publish resources and consume entitlements aimed at it; a national ministry might publish resources, decisions, and entitlements while consuming consent records. Both are full, conformant participants. This is what makes the structure scale-independent — one provider on its own is a valid deployment, and it grows into a union of many providers without changing shape.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking that the contracts are implementation-independent
&lt;/h2&gt;

&lt;p&gt;A blueprint that says "anyone can implement these interfaces" is making a claim, and I wanted that claim to be testable rather than taken on trust. That's what the conformance suite is for.&lt;/p&gt;

&lt;p&gt;Fixtures are organized by intent: documents under &lt;code&gt;valid/&lt;/code&gt; must validate, and documents under &lt;code&gt;invalid/&lt;/code&gt; must be rejected, with each invalid fixture isolating a single violation so a failure points at one clear reason. The runner checks three layers, because the contracts are stricter than any single JSON Schema can express:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Structural&lt;/strong&gt; — each document is validated against its area's schema (JSON Schema 2020-12).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Referential&lt;/strong&gt; — cross-references between areas must resolve. A resource's &lt;code&gt;provider_id&lt;/code&gt;, an entitlement's &lt;code&gt;resource_id&lt;/code&gt;, &lt;code&gt;provider_id&lt;/code&gt;, and &lt;code&gt;decision_ref&lt;/code&gt;, and nested references like a consent record's &lt;code&gt;scope.provider_ref&lt;/code&gt; all have to point at records that exist. &lt;code&gt;subject_ref&lt;/code&gt;, as above, is intentionally excluded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic&lt;/strong&gt; — invariants the schema standard can't state. The public transparency report enforces a small-cell floor: a breakdown bucket can't report a count below a declared threshold, because small aggregates re-identify people. JSON Schema 2020-12 can't require one number to be at least as large as a sibling field, so the suite enforces it in code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The first blueprint version passes at &lt;strong&gt;44 fixtures, 0 failures&lt;/strong&gt;, and the full chain resolves end to end — a resource is published, a decision references it, an entitlement is drawn from it and pinned to the fulfilling provider — including a cross-provider case where a nationally-published resource is fulfilled by a different provider under the same shared floor.&lt;/p&gt;

&lt;p&gt;To check that "implementable by anyone" holds rather than just "implementable by my checker," there are two independent conformance runners over the same schemas and fixtures: the reference is Python with the &lt;code&gt;jsonschema&lt;/code&gt; library, and the second is Node with &lt;code&gt;ajv&lt;/code&gt; — a different language and a different validation engine, chosen so that agreement can't come from shared machinery. A small script runs both and compares their per-fixture verdicts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;py&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;python3 conformance/runner/validate.py | verdicts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nv"&gt;node&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;implementations/node &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; node conformance.js | verdicts&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$py&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"cross-check: OK — both implementations agree on all &lt;/span&gt;&lt;span class="nv"&gt;$count&lt;/span&gt;&lt;span class="s2"&gt; fixtures"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;diff &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$py&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$node&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Right now both agree on all 44 fixtures, and I confirmed the mismatch path actually fails when fed a deliberate disagreement, so the check is doing real work. Alongside the runners, the reference implementation validates every document against the schemas on publication (it relies on the same contracts it demonstrates) and stores no subjects at all, and the first adapter maps OpenSPP's native concepts onto the provider and resource contracts with its output validated the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this version is, and what it isn't
&lt;/h2&gt;

&lt;p&gt;To be precise about scope, since the mission is easy to overstate:&lt;/p&gt;

&lt;p&gt;This version is a coherent, language-neutral contract for coordinating survival resources across independent providers; a conformance suite that makes "implementable" testable; and a demonstration, checked by two independent implementations, that the contracts rather than one codebase decide conformance.&lt;/p&gt;

&lt;p&gt;It is not a running network, a governance body, or a funded pilot. Two runners I wrote agreeing is a strong signal that the schemas are implementation-independent, but it is not the same as two separate organizations interoperating in production. The next steps follow directly from that gap: an independent party running the same suite against their own output, and one small, honest pilot — a single municipality, a single resource type, one capability path end to end, consent-gated, with a transparency report — to show the small thing works before claiming the large one does.&lt;/p&gt;

&lt;p&gt;That sequencing is intentional: build the shared understanding and the open contracts first, ship the smallest useful slice, and earn the standing to propose the larger structure to the institutions that would run it. Shared infrastructure of this kind rarely arrives because one person built all of it; it arrives because the groundwork already existed when a window opened.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get involved
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Repository: &lt;a href="https://gitlab.com/dobybaxter127/social-resource-floor" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/social-resource-floor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live site: &lt;a href="https://social-resource-floor-5a9cc0.gitlab.io/" rel="noopener noreferrer"&gt;https://social-resource-floor-5a9cc0.gitlab.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The best contribution at this stage is a hard question or a challenge to an assumption — governance, funding, and privacy are open by design.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Social Resource Floor — repository. &lt;a href="https://gitlab.com/dobybaxter127/social-resource-floor" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/social-resource-floor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSPP. &lt;a href="https://openspp.org/en/" rel="noopener noreferrer"&gt;https://openspp.org/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenG2P. &lt;a href="https://www.openg2p.org/" rel="noopener noreferrer"&gt;https://www.openg2p.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenCRVS. &lt;a href="https://www.opencrvs.org/" rel="noopener noreferrer"&gt;https://www.opencrvs.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Digital Convergence Initiative. &lt;a href="https://spdci.org/" rel="noopener noreferrer"&gt;https://spdci.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;G2P Connect. &lt;a href="https://g2p-connect.github.io/" rel="noopener noreferrer"&gt;https://g2p-connect.github.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenFisca. &lt;a href="https://openfisca.org/" rel="noopener noreferrer"&gt;https://openfisca.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;W3C Data Privacy Vocabulary (DPV). &lt;a href="https://w3c.github.io/dpv/" rel="noopener noreferrer"&gt;https://w3c.github.io/dpv/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;JSON Schema 2020-12. &lt;a href="https://json-schema.org/" rel="noopener noreferrer"&gt;https://json-schema.org/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>architecture</category>
      <category>socialgood</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>The Layer Above the Safety Net: Starting the Social Resource Floor</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Mon, 10 Aug 2026 00:40:18 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/the-layer-above-the-safety-net-starting-the-social-resource-floor-1khm</link>
      <guid>https://dev.to/dobybaxter127/the-layer-above-the-safety-net-starting-the-social-resource-floor-1khm</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — A day after I shipped my first contribution to OpenSPP, an open-source social protection platform, I couldn't stop looking up. Fixing a two-line bug meant tracing how one field moves through a system that decides what real people receive — and when I lifted my eyes from that field, I saw a gap that isn't a bug in any single project. There is no open, shared layer &lt;em&gt;above&lt;/em&gt; the delivery systems that coordinates one person's whole access to survival resources, across many providers and many resource types. So I started one: a blueprint, not a product, called the &lt;strong&gt;Social Resource Floor&lt;/strong&gt;. It is early, exploratory, and open. This is why it exists, what it is careful &lt;em&gt;not&lt;/em&gt; to be, and where you can help.&lt;/p&gt;

&lt;h2&gt;
  
  
  It started one layer down
&lt;/h2&gt;

&lt;p&gt;My first OpenSPP contribution was small on paper. A beneficiary's date of birth could be set to a date in the future, which made their computed age render as a negative number across the registry. The fix was a constraint. The education was everything around it: following every path that can write that field, and remembering that on a social protection platform a wrong age isn't cosmetic, because age feeds eligibility.&lt;/p&gt;

&lt;p&gt;Doing that work put my hands on the machinery that decides whether a person is &lt;em&gt;seen correctly&lt;/em&gt; by the program meant to help them. And it left me with a question that had nothing to do with birthdates. This system delivers benefits well within its mission — but what sits above it? Who coordinates when a single household needs food from one provider, energy help from another, and housing support from a third, across organizations that have never heard of each other?&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I couldn't unsee
&lt;/h2&gt;

&lt;p&gt;Right now, for most people, survival routes through financial access. To reach food, housing, energy, or healthcare, you generally need money — and to hold or move money you need banking, employment, or purchasing power. Financial access has quietly become the gate standing in front of the resources a person needs to stay alive.&lt;/p&gt;

&lt;p&gt;I want to be exact here, because this idea attracts a more dramatic version of itself that I don't hold. I am not saying money is bad. I am not proposing we abolish banks, markets, or the financial system. Those can keep existing and doing what they do.&lt;/p&gt;

&lt;p&gt;The claim is narrower: financial access should not be the &lt;em&gt;condition&lt;/em&gt; that determines whether a human can reach the basic resources required to survive. Money becomes one possible resource, and one possible delivery method — not the organizing principle of survival itself. That is a smaller thesis than "replace the economy," and a far more buildable one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Social Resource Floor is — and what it refuses to be
&lt;/h2&gt;

&lt;p&gt;The Social Resource Floor is an open blueprint for a coordination layer that sits above existing social-protection infrastructure. Its job is to let independent systems — government programs, municipalities, NGOs, cooperatives, community providers — coordinate access to survival resources through shared, open interfaces, while each keeps its own mission and its own data.&lt;/p&gt;

&lt;p&gt;It is defined just as much by what it does not try to be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Not a replacement&lt;/strong&gt; for OpenSPP, OpenG2P, OpenCRVS, or any delivery platform. Those systems keep delivering; the Floor coordinates above them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not universal basic income&lt;/strong&gt;, and not a bank, a government, or an NGO. It is a structure, not an institution that owns resources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not anti-money.&lt;/strong&gt; Cash stays a first-class resource and delivery method.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not, yet, working infrastructure.&lt;/strong&gt; It is a blueprint — definitions, relationships, and interfaces — developed in the open.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stating the negatives this loudly is the point. Ambition without boundaries is just noise; the boundaries are what make it possible to actually build something.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Decentralized" was hiding four different questions
&lt;/h2&gt;

&lt;p&gt;Early on I kept reaching for the word &lt;em&gt;decentralized&lt;/em&gt;, and I kept confusing myself — until I realized the word was doing four jobs at once. Pulling them apart was most of the clarity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Authority&lt;/strong&gt; — who defines the rules? No single institution should govern every deployment; the floor is defined locally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operation&lt;/strong&gt; — who runs it, and where? It should be &lt;em&gt;locally deployable&lt;/em&gt;: anyone can run their own instance and stay interoperable with others.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt; — where does personal information live? It should be &lt;em&gt;data-minimizing&lt;/em&gt;: data stays with the systems that already hold it, and only what a given interaction needs is exchanged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordination&lt;/strong&gt; — how do independent systems talk? Through open interfaces anyone can implement, with no central hub sitting in the path of every interaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put together, that is not "decentralization" in the crypto sense. It is &lt;strong&gt;federation&lt;/strong&gt; — the same pattern as email and the web, where independent parties implement open standards and no one owns the network. No blockchain, no tokens, no trustless consensus. Just open interfaces and independent systems that agree to speak them.&lt;/p&gt;

&lt;h2&gt;
  
  
  I expected a crowd. I found building blocks.
&lt;/h2&gt;

&lt;p&gt;I assumed a hundred people were already building exactly this. What I found was better, and more humbling: a lot of serious, excellent work on the &lt;em&gt;pieces&lt;/em&gt;, and an open question about the layer above them.&lt;/p&gt;

&lt;p&gt;The delivery systems — OpenSPP, OpenG2P, OpenCRVS — are strong and explicitly interoperability-minded. There is real standards work underway too: the Digital Convergence Initiative is developing interoperability standards between identification and social-protection systems, with that same open stack in its demonstrations. None of this is siloed neglect waiting to be rescued, and I won't pretend it is.&lt;/p&gt;

&lt;p&gt;What I didn't find was an open, shared abstraction for &lt;em&gt;one person's whole survival floor&lt;/em&gt;, coordinated across many providers and many resource types at once. That is the specific question the Floor exists to ask. Framed honestly, it is less "nobody has done this" and more: these are building blocks — what does the layer above them look like, and can it be defined in the open?&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it actually is right now
&lt;/h2&gt;

&lt;p&gt;Early. Genuinely early. The current scope is deliberately conceptual, in four stages:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the concepts&lt;/strong&gt; — a shared vocabulary for people, providers, resources, policies, and entitlements.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map the relationships&lt;/strong&gt; — how needs, eligibility, entitlements, and delivery connect across providers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Find the interfaces&lt;/strong&gt; — the minimum contract two independent systems need to participate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name the open questions&lt;/strong&gt; — keeping governance, funding, privacy, and authority as explicit questions rather than buried assumptions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to get involved
&lt;/h2&gt;

&lt;p&gt;This is exactly the stage where a few extra minds change the shape of a thing. Whether your background is social protection, engineering, policy, or lived experience of these systems, there is a way in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read the idea&lt;/strong&gt; on the live site.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the README and specification drafts&lt;/strong&gt; in the repository.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open an issue&lt;/strong&gt; with a question, a gap, or a hard challenge — governance, privacy, and funding are open by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sharpen the models&lt;/strong&gt;, or research adapters for OpenSPP, OpenG2P, and OpenCRVS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Live website: &lt;a href="https://social-resource-floor-5a9cc0.gitlab.io/" rel="noopener noreferrer"&gt;https://social-resource-floor-5a9cc0.gitlab.io/&lt;/a&gt;&lt;br&gt;
Repository: &lt;a href="https://gitlab.com/dobybaxter127/social-resource-floor" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/social-resource-floor&lt;/a&gt;&lt;br&gt;
Open an issue: &lt;a href="https://gitlab.com/dobybaxter127/social-resource-floor/-/work_items" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/social-resource-floor/-/work_items&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Social Resource Floor — live site. &lt;a href="https://social-resource-floor-5a9cc0.gitlab.io/" rel="noopener noreferrer"&gt;https://social-resource-floor-5a9cc0.gitlab.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Social Resource Floor — repository. &lt;a href="https://gitlab.com/dobybaxter127/social-resource-floor" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/social-resource-floor&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSPP — Issue #362, the birthdate bug that started this. &lt;a href="https://github.com/OpenSPP/OpenSPP2/issues/362" rel="noopener noreferrer"&gt;https://github.com/OpenSPP/OpenSPP2/issues/362&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSPP. &lt;a href="https://openspp.org/en/" rel="noopener noreferrer"&gt;https://openspp.org/en/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenG2P. &lt;a href="https://www.openg2p.org/" rel="noopener noreferrer"&gt;https://www.openg2p.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenCRVS. &lt;a href="https://www.opencrvs.org/" rel="noopener noreferrer"&gt;https://www.opencrvs.org/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Digital Convergence Initiative — social protection interoperability standards. &lt;a href="https://spdci.org/" rel="noopener noreferrer"&gt;https://spdci.org/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The bug I started with was about one field being seen correctly. The Floor is the same concern at a different scale: whether a person can be seen — and reached — by the systems meant to help them, without money being the price of admission.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>architecture</category>
      <category>socialgood</category>
      <category>distributedsystems</category>
    </item>
    <item>
      <title>You Don't Validate in the Form — You Validate Where Every Write Path Converges</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 09 Aug 2026 16:37:30 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/you-dont-validate-in-the-form-you-validate-where-every-write-path-converges-25ao</link>
      <guid>https://dev.to/dobybaxter127/you-dont-validate-in-the-form-you-validate-where-every-write-path-converges-25ao</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt; — My first OpenSPP contribution fixes a small bug with a big shadow: a beneficiary's date of birth could be set to a date in the &lt;em&gt;future&lt;/em&gt;, which made the computed &lt;code&gt;age&lt;/code&gt; go negative and render as things like &lt;code&gt;-3&lt;/code&gt; across the registry. The web form already blocked it — but only the form. Every other way data enters the system walked straight past that guard. This post is about what I found when I followed each write path, why the one check that &lt;em&gt;looked&lt;/em&gt; like it covered this was dead code, and the seven rules I now hold myself to about where validation actually belongs. The system is &lt;a href="https://openspp.org/en/" rel="noopener noreferrer"&gt;OpenSPP&lt;/a&gt;, an open-source social protection platform, which raises the stakes: on a platform like this, a wrong age isn't cosmetic — it feeds eligibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth I had to give up first
&lt;/h2&gt;

&lt;p&gt;For a long time I believed that if the form validated an input, the input was validated. You type a bad value, the UI catches it, the user fixes it — done. That belief is comfortable because it's &lt;em&gt;usually&lt;/em&gt; true from where you're sitting, which is in front of the form.&lt;/p&gt;

&lt;p&gt;It stops being true the moment you remember that a form is one door into the building, not the building itself.&lt;/p&gt;

&lt;p&gt;OpenSPP stores a beneficiary's date of birth on &lt;code&gt;birthdate&lt;/code&gt;, and derives a non-stored computed &lt;code&gt;age&lt;/code&gt; from it. In the web form, a small guard called &lt;code&gt;_birthdate_onchange&lt;/code&gt; already reacts when someone types a future date: it clears the field and shows a friendly warning. If the form were the only door, this bug wouldn't exist. But a registry is written to by imports, scripts, and APIs far more than by people typing into a form — and none of them ring the doorbell.&lt;/p&gt;

&lt;p&gt;So I stopped asking "does the form catch this?" and started asking the only question that matters for a system of record: &lt;strong&gt;does every path that can write this field catch it?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I found when I followed every write path
&lt;/h2&gt;

&lt;p&gt;I traced each way a &lt;code&gt;birthdate&lt;/code&gt; can be set. The findings were consistent, and each one peels back the same assumption.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The onchange guard only runs in the form UI.&lt;/strong&gt; This is not an OpenSPP quirk — it's how Odoo works. &lt;code&gt;@api.onchange&lt;/code&gt; methods fire on form-view interaction and are never triggered by direct write calls. The friendly reset I was relying on is, by design, a UI convenience, not a data guarantee.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every non-UI path skips it entirely.&lt;/strong&gt; A future birthdate is accepted through direct ORM calls (&lt;code&gt;create()&lt;/code&gt; / &lt;code&gt;write()&lt;/code&gt;), through CSV and Excel import, and through the API surfaces (XML-RPC, API v2, DCI endpoints). Each of those is a door with no guard on it. In a social registry, those are the &lt;em&gt;busy&lt;/em&gt; doors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The check that looked like it covered this was dead code.&lt;/strong&gt; There had been an &lt;code&gt;@api.constrains("age")&lt;/code&gt; guard that appeared to validate exactly this. It never ran. &lt;code&gt;age&lt;/code&gt; is a non-stored computed field, and a constraint on a field like that doesn't fire the way a constraint on a real, stored column does. It was removed in an earlier PR (&lt;a href="https://github.com/OpenSPP/OpenSPP2/pull/357" rel="noopener noreferrer"&gt;#357&lt;/a&gt;) as a behavior-preserving cleanup — removing something that already did nothing. The lesson landed hard: a check that silently validates &lt;em&gt;nothing&lt;/em&gt; is worse than no check, because it buys you false confidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Once stored, the bad value spreads.&lt;/strong&gt; A future &lt;code&gt;birthdate&lt;/code&gt; makes the computed &lt;code&gt;age&lt;/code&gt; negative, and that &lt;code&gt;-3&lt;/code&gt; then renders in views, exports, and anywhere downstream logic reads the field. The invalid value doesn't stay where it entered; it leaks into every surface that trusts the registry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And this is a social protection platform.&lt;/strong&gt; A clean date of birth isn't a display detail here — age feeds eligibility and program logic. A negative age is, concretely, a person being mis-seen by the exact system meant to protect them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mapping what I found to rules I now hold myself to
&lt;/h2&gt;

&lt;p&gt;Findings are only worth the tracing if they change what I do next time. Here's the translation, each rule tied to what I saw and to the fix I shipped.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 1 — Validate where every write path converges, not at the door the user happens to use
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; the onchange guard runs only in the form; every other path skips it.&lt;/p&gt;

&lt;p&gt;The form is one entrypoint; the model is where they all meet. So the guarantee has to live on the model. A stored-field constraint runs on create, write, import, and API alike, because all of them ultimately go through the ORM:&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="nd"&gt;@api.constrains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;birthdate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_check_birthdate_not_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;birthdate&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;birthdate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;today&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValidationError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&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;Date of birth cannot be in the future.&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;h3&gt;
  
  
  Rule 2 — Constrain the input you store, not the value you derive
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; the dead &lt;code&gt;@api.constrains("age")&lt;/code&gt; on a non-stored computed field.&lt;/p&gt;

&lt;p&gt;The old guard tried to validate &lt;code&gt;age&lt;/code&gt;, a derived value that isn't a real column — so it never fired. The fix constrains &lt;code&gt;birthdate&lt;/code&gt;, the stored field the user actually writes. Validate the thing that gets persisted, and know which of your framework's hooks genuinely run versus which only look like they do.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 3 — Keep the gentle guard &lt;em&gt;and&lt;/em&gt; the hard backstop
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; the onchange's silent-reset UX is friendlier than a raised error.&lt;/p&gt;

&lt;p&gt;The right move isn't to replace the form guard with the constraint — it's to keep both. The onchange stays as a soft, in-UI first line of defense that quietly corrects a typo before anyone hits save; the constraint sits behind it as the backstop that no import or API call can bypass. Defense in depth, not defense in replacement.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 4 — When you refuse, speak the user's domain
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; the raw failure is a negative number and schema noise; the person needs a sentence.&lt;/p&gt;

&lt;p&gt;A refusal has to say what's wrong in the user's world, not the parser's. The message is &lt;code&gt;Date of birth cannot be in the future.&lt;/code&gt; — a plain, domain-level statement — not a schema violation about a stored column or a stray &lt;code&gt;-3&lt;/code&gt; surfacing three screens away. The registry's users think in beneficiaries and dates, so the error talks in beneficiaries and dates.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 5 — A write-time constraint doesn't heal history
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; a constraint only validates on write.&lt;/p&gt;

&lt;p&gt;Records that &lt;em&gt;already&lt;/em&gt; hold a future birthdate stay invalid until they're next touched — and could then block otherwise-unrelated saves. So a constraint isn't the whole job for a deployed database; it wants a paired data-quality check or a migration note to find and fix the values that slipped in before the guard existed. Stopping new bad data and cleaning old bad data are two tasks, not one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 6 — Test every path you claim to cover, before the fix
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; the bug existed precisely because only one path was ever exercised.&lt;/p&gt;

&lt;p&gt;I'm following the standard OpenSPP flow: write the failing test first, then the fix, and make the test cover create, write, and import — not just the form. A fix I only proved on the path that already worked would be repeating the original mistake in a nicer font.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rule 7 — On a social platform, correctness is dignity, not polish
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;From:&lt;/em&gt; age feeds eligibility and program logic.&lt;/p&gt;

&lt;p&gt;This is the spine the other six hang on. The reason to get a two-line constraint exactly right isn't tidiness — it's that a clean date of birth decides whether a real person is represented accurately to the program deciding what they receive. "Small bug" and "small stakes" are not the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The synthesis: validate where every path converges
&lt;/h2&gt;

&lt;p&gt;Put it together and the shape is simple. A form guard answers "did &lt;em&gt;this user, in this UI,&lt;/em&gt; enter something sane?" A model constraint answers "can this invalid value exist in the database &lt;em&gt;at all&lt;/em&gt;, no matter who or what wrote it?" Those are different questions, and for a system of record only the second one keeps you safe.&lt;/p&gt;

&lt;p&gt;The bug was never really about birthdates. It was about mistaking the door I could see for the whole building. The interesting part of the fix isn't the two-line constraint — it's understanding &lt;em&gt;why&lt;/em&gt; the old guard never fired, &lt;em&gt;where&lt;/em&gt; every write path enters the model, and &lt;em&gt;how&lt;/em&gt; to protect existing data without breaking unrelated saves.&lt;/p&gt;

&lt;p&gt;You don't validate in the form. You validate at the layer every path has to pass through — and you say what's wrong in words the person on the other end can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The form is a door, not the building.&lt;/strong&gt; UI validation guards one entrypoint; a system of record is written to by imports, scripts, and APIs that never touch it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Put the guarantee on the model.&lt;/strong&gt; A stored-field constraint runs on create, write, import, and API alike, because they all converge on the ORM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Constrain what you store, not what you derive.&lt;/strong&gt; A check on a non-stored computed field can be dead code that silently validates nothing — the most expensive kind of check, because it feels safe.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the gentle guard and the hard backstop.&lt;/strong&gt; Defense in depth: a friendly onchange in front, an unbypassable constraint behind.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speak the user's domain when you refuse.&lt;/strong&gt; "Date of birth cannot be in the future," not a schema error or a stray negative number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A write-time constraint doesn't heal history&lt;/strong&gt; — pair it with a data-quality check or migration note for records that predate the guard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On a social protection platform, correct data is a person's dignity.&lt;/strong&gt; That's why a two-line fix is worth this much care.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;OpenSPP — Issue #362, &lt;em&gt;spp_registry accepts future birthdates via ORM / import / API.&lt;/em&gt; &lt;a href="https://github.com/OpenSPP/OpenSPP2/issues/362" rel="noopener noreferrer"&gt;https://github.com/OpenSPP/OpenSPP2/issues/362&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSPP — PR #357, the behavior-preserving removal of the dead &lt;code&gt;@api.constrains("age")&lt;/code&gt; guard. &lt;a href="https://github.com/OpenSPP/OpenSPP2/pull/357" rel="noopener noreferrer"&gt;https://github.com/OpenSPP/OpenSPP2/pull/357&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Odoo 17 — &lt;em&gt;ORM API reference&lt;/em&gt; (&lt;code&gt;@api.constrains&lt;/code&gt;, &lt;code&gt;@api.onchange&lt;/code&gt;, computed fields). &lt;a href="https://www.odoo.com/documentation/17.0/developer/reference/backend/orm.html" rel="noopener noreferrer"&gt;https://www.odoo.com/documentation/17.0/developer/reference/backend/orm.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OpenSPP — the platform this contribution is for. &lt;a href="https://openspp.org/en/" rel="noopener noreferrer"&gt;https://openspp.org/en/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>odoo</category>
      <category>opensource</category>
      <category>testing</category>
    </item>
    <item>
      <title>Paving the wrong Path: What a Broken Government System Taught Me About Feedback Loops</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Mon, 27 Jul 2026 23:17:33 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/paving-the-wrong-path-what-a-broken-government-system-taught-me-about-feedback-loops-2h24</link>
      <guid>https://dev.to/dobybaxter127/paving-the-wrong-path-what-a-broken-government-system-taught-me-about-feedback-loops-2h24</guid>
      <description>&lt;p&gt;Last year I was made redundant from an NHS role. Around the same time, the team I worked in got a "new system." Everything that used to live on paper — or, honestly, in something that behaved like a shared Excel sheet — was going digital.&lt;/p&gt;

&lt;p&gt;I was still employed during the first few weeks of rollout, so I watched it happen. And the thing I remember most clearly is that it didn't make anything better. The forms moved from paper to screen, and that was more or less the whole story. The same duplications, the same overlaps, the same unanswered questions. If anything, it felt more primitive than the retro spreadsheet it replaced.&lt;/p&gt;

&lt;p&gt;I've been chewing on why ever since, and I think it's worth writing down, because the failure mode is one every engineer will recognize once it's named.&lt;/p&gt;

&lt;h2&gt;
  
  
  Paving the wrong path
&lt;/h2&gt;

&lt;p&gt;There's an old phrase in software: &lt;em&gt;if you digitize a bad process, you get a digital bad process.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The engineering equivalent is porting a legacy codebase line-for-line into a new language and calling it a rewrite. The syntax is new. The runtime is new. But you carried every piece of technical debt across the boundary intact, because you translated the &lt;em&gt;implementation&lt;/em&gt; without ever revisiting the &lt;em&gt;design&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Policy is the spec. Software is the implementation.
&lt;/h2&gt;

&lt;p&gt;Here's the distinction I kept coming back to, and I think it's the crux of the whole thing. In a normal project, requirements drive implementation. The spec says what the system should do; the code is one interpretation of that spec. When they diverge, you have a defect.&lt;/p&gt;

&lt;p&gt;In public systems, &lt;strong&gt;policy is the spec and software is the implementation.&lt;/strong&gt; Every required field, every workflow step, every deadline, every automated decision is a concrete interpretation of a policy. This is where policy stops being an abstract document and becomes something a real person collides with at 4pm on a Friday.&lt;/p&gt;

&lt;p&gt;So when a policy says "provide tailored support" but the software only ships a single hard-coded workflow, the implementation contradicts the spec. Nobody signed off on that contradiction. It just emerged from the gap between the two layers — the same way a bug emerges when the code does something the requirements never sanctioned.&lt;/p&gt;

&lt;h2&gt;
  
  
  A dependency with a bus factor of zero
&lt;/h2&gt;

&lt;p&gt;The part that still bothers me most is how the system arrived. No manager knew who built it. Nobody could tell me where it came from, who maintained it, or how it fit the existing workflow. There was no training.&lt;/p&gt;

&lt;p&gt;Frame that in engineering terms and it's absurd. This was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an &lt;strong&gt;undocumented third-party dependency&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;deployed straight to production&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;with &lt;strong&gt;no README, no runbook, no changelog, no owner&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;and a &lt;strong&gt;bus factor of zero&lt;/strong&gt; — not one person on site could answer a question about it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I only found out much later that it wasn't even a UK system; it was imported from the US. That's an unvetted external dependency shipped into a critical path, and the onboarding doc was the word "learn."&lt;/p&gt;

&lt;p&gt;We'd never accept this in a repo. We'd block the PR. But institutions do it constantly, because the people deploying the system and the people using it are separated by enough layers that the missing context never registers as a problem — until it lands on the person doing the job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is an architecture problem, not a UI problem
&lt;/h2&gt;

&lt;p&gt;When engineers hear "accessibility," we usually think keyboard navigation, screen readers, color contrast, text scaling. All important. But there's a broader meaning that matters just as much:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can people understand what the system is actually doing?&lt;/li&gt;
&lt;li&gt;Does it adapt to different circumstances, or force everyone down one path?&lt;/li&gt;
&lt;li&gt;Is it clear who made a decision, and why?&lt;/li&gt;
&lt;li&gt;Can someone challenge a decision without needing extraordinary time, money, or expertise?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those aren't UI questions. They're architectural ones. And you can't fix an architectural problem at the presentation layer. If an API has terrible semantics, no amount of pretty documentation saves it. If the underlying service model assumes everyone follows the same workflow, a bigger button doesn't help the person the workflow was never designed for — it just makes the dead end easier to click.&lt;/p&gt;

&lt;p&gt;The NHS system had this exact shape. The interface was new. The model underneath it still assumed one path for everyone, and that assumption is the thing that fails people.&lt;/p&gt;

&lt;h2&gt;
  
  
  The silent bug tracker
&lt;/h2&gt;

&lt;p&gt;Now the part I care about most, because it's the one that generalizes to &lt;em&gt;any&lt;/em&gt; organization.&lt;/p&gt;

&lt;p&gt;A system can only improve if it's allowed to learn. And a learning loop needs three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Honest feedback&lt;/strong&gt; — real signal about what's failing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A safe environment&lt;/strong&gt; for producing that signal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A transparent process&lt;/strong&gt; for deciding what happens with it&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Break any one and the loop stops closing.&lt;/p&gt;

&lt;p&gt;Engineers already understand this, because we understand observability. Feedback is telemetry. It's how a running system tells you where it hurts. Now imagine a project where every bug report is met with "why are you being so negative?" Give it a few months. The issue tracker goes quiet.&lt;/p&gt;

&lt;p&gt;From the outside, that looks like stability. It isn't. &lt;strong&gt;People just stopped filing.&lt;/strong&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;if&lt;/span&gt; &lt;span class="n"&gt;report_is_honest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;punish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reporter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# a few weeks later
&lt;/span&gt;&lt;span class="n"&gt;open_issues&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;conclusion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the system is stable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;reality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;telemetry&lt;/span&gt; &lt;span class="n"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;failures&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="n"&gt;silent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is survivorship bias applied to an entire workplace. The dashboards are green because you turned off the sensors. And in a lot of public systems right now, honesty &lt;em&gt;is&lt;/em&gt; punished — you get told off, told to get used to it, or told that's just how things are. So people stop believing they can change anything, and the signal dies. Not because everything works, but because reporting stopped feeling worth the cost.&lt;/p&gt;

&lt;p&gt;There's a second, quieter failure hiding next to it: collecting feedback you never act on. We do this with metrics all the time — we track something because we &lt;em&gt;can&lt;/em&gt;, not because we have a process to respond to it. A thousand survey responses that feed no decision are just vanity metrics. Gathering the data isn't the point. Closing the loop is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source is the unlock — but policy has to evolve with it
&lt;/h2&gt;

&lt;p&gt;I want to be clear: I think open source government infrastructure is genuinely one of the best things that could happen to public systems. For the first time, people who actually depend on a service — and the engineers who understand how it's built — can participate directly. They can read it, contribute, file issues, propose fixes, and have their needs acknowledged in a place everyone can see instead of scattered across emails, complaints, and tribunals. That's real power, and it's worth being excited about.&lt;/p&gt;

&lt;p&gt;But — and this is the whole point — that power only fires if the policies evolve at the same time.&lt;/p&gt;

&lt;p&gt;You can have public repos, open issue trackers, documented architecture, transparent decision logs — and &lt;em&gt;still&lt;/em&gt; run a culture where nobody dares say "this isn't working," because the last person who did got punished. Open code, closed culture. The loop is still broken; it's just broken in public now. You've handed people a contribution channel and simultaneously made contributing unsafe.&lt;/p&gt;

&lt;p&gt;So the two have to move together. Open sourcing the implementation while leaving the policy — and the culture around feedback — untouched just publishes the dysfunction. But open sourcing it &lt;em&gt;alongside&lt;/em&gt; policies that actually welcome and act on honest input is where the potential becomes real: frontline staff and citizens stop being passive recipients and become part of how the system improves.&lt;/p&gt;

&lt;p&gt;Because they're the ones hitting the failures every single day, their lived experiences hold the potential for your best telemetry. &lt;/p&gt;

&lt;h2&gt;
  
  
  The actual shift
&lt;/h2&gt;

&lt;p&gt;Most conversations about digital government ask: &lt;em&gt;how do we digitize existing processes?&lt;/em&gt; That question gets you paper-on-a-screen. It gets you the NHS rollout I watched.&lt;/p&gt;

&lt;p&gt;The better question is: &lt;em&gt;how do we build systems that continuously learn from the people who use them?&lt;/em&gt; That's not about replacing paper with software. It's about designing institutions with a working feedback loop built into the architecture — where the reasoning behind a system is documented, its assumptions are explicit, structural problems can be reported safely, and changes are visible instead of vanishing into private procurement and closed meetings.&lt;/p&gt;

&lt;p&gt;Under that framing, publishing the code is almost the last step. The important part is making the decision-making itself observable.&lt;/p&gt;

&lt;p&gt;And notice that none of it is really about technology. Python, Java, or paper — the values underneath are the same: transparency, accountability, shared responsibility, and a loop that's actually allowed to close. A system that only delivers a service is a system running blind. A system that learns from the people who build it and the people who depend on it is the one worth building.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>nhs</category>
      <category>architecture</category>
      <category>uk</category>
    </item>
    <item>
      <title>Reconstructing cloud identity intrusions from O365 and CloudTrail logs</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Mon, 20 Jul 2026 20:46:47 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/reconstructing-cloud-identity-intrusions-from-o365-logs-an-open-engine-that-ranks-attack-paths-fe4</link>
      <guid>https://dev.to/dobybaxter127/reconstructing-cloud-identity-intrusions-from-o365-logs-an-open-engine-that-ranks-attack-paths-fe4</guid>
      <description>&lt;p&gt;The uncomfortable truth of modern cloud IR is that there is often nothing to "detect" in the old sense. Attackers don't break in anymore; they log in. CrowdStrike's 2026 Global Threat Report put 82% of last year's detections in the malware-free column, with valid-account abuse behind roughly a third of cloud incidents. No dropper, no exploit chain — just a legitimate identity doing legitimate-looking things across Entra, M365, and a dozen SaaS apps.&lt;/p&gt;

&lt;p&gt;That changes the hard part of the job. Detection tells you &lt;em&gt;that&lt;/em&gt; something happened. It rarely tells you &lt;em&gt;how&lt;/em&gt;: which account was really patient zero, which token got replayed, which consent grant quietly opened the door, and in what order it all unfolded. Responders reconstruct that story by hand, stitching a timeline out of fragmented, inconsistent audit logs, under time pressure, while the clock on breakout time keeps shrinking.&lt;/p&gt;

&lt;p&gt;And manual reconstruction fails in a specific, predictable way: you anchor. You find the first narrative that fits the evidence, and you stop looking. The real path — usually the quieter one that doesn't announce itself — sits unexamined a branch away.&lt;/p&gt;

&lt;p&gt;I built a tool to attack that failure mode head-on. It's called Nimbus Vestige, it's open source, and its defining feature is that it will tell you when it doesn't know.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconstruction, not detection
&lt;/h2&gt;

&lt;p&gt;Nimbus Vestige (NV) is not a detector, a SIEM, a scanner, or an auditor. Those tools tell you that something happened and measure it. NV works backward — abductive reconstruction of the &lt;em&gt;mechanism&lt;/em&gt; from patterns. Given normalized identity telemetry (sign-ins, consent grants, service-principal and role changes, mailbox access), it reconstructs the most likely path an intrusion took across accounts and services.&lt;/p&gt;

&lt;p&gt;The one design decision everything else hangs off: it never hands you a single story. It hands you the ranked space of plausible paths. If your incident could have unfolded three ways, you see all three, ordered by how well the evidence supports each. That is the anchoring antidote — you can't tunnel on the first narrative when the tool is holding the alternatives up next to it. And the paths it &lt;em&gt;didn't&lt;/em&gt; rank first but still rates plausible are a bonus deliverable: they're a hardening backlog, the other doors that were standing open.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honesty problem
&lt;/h2&gt;

&lt;p&gt;This is the part I care most about, because it's where reconstruction tools usually cheat. It is easy to emit a confident number. It is hard to emit a number that &lt;em&gt;means&lt;/em&gt; something.&lt;/p&gt;

&lt;p&gt;Every step NV reconstructs carries a confidence in &lt;code&gt;[0, 1]&lt;/code&gt;, built by evidence corroboration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a per-technique base rate,&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;plus&lt;/strong&gt; a bonus for each independent corroborating signal (source IP, device, successful outcome, temporal adjacency, broad-consent flags),&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;minus&lt;/strong&gt; a penalty for signals that should be present and aren't (e.g. no source IP to corroborate an origin).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Path scores compose their links by &lt;strong&gt;geometric mean&lt;/strong&gt;, not average — so one weak link honestly drags the whole path down instead of being smoothed over by strong neighbors.&lt;/p&gt;

&lt;p&gt;Then there is a &lt;strong&gt;trust floor&lt;/strong&gt;. Below it, a step is not shown as a guess — it is &lt;em&gt;withheld&lt;/em&gt;, grayed out, explicitly unsupported. The floor is a slider: drag it up for strict, high-trust reconstruction, down for permissive, high-coverage. That is the honesty-versus-coverage tradeoff made physical, and it lives in the engine's output contract, not bolted onto the UI as an afterthought.&lt;/p&gt;

&lt;p&gt;Do the base rates and bonuses actually mean anything? That's a calibration question, and I treat it as one. The weights start as defensible priors, and a harness refits them against labeled ground truth — events where you know which were on the attack path and which were benign — reporting Brier score, ECE, a reliability table, and a trust-floor sweep before and after. A refit is only adopted if it genuinely lowers calibration error rather than just shuffling numbers around. Calibrated confidence means one concrete thing here: NV's 0.7 should correspond to a step that really was on the attack path about 70% of the time.&lt;/p&gt;

&lt;p&gt;The honest caveat, stated plainly because the whole point is honesty: the real calibration input is telemetry from an attack-simulation run (BadZure / MAAD-AF) in a live tenant. Until you point it at one, NV ships with a documented-shape &lt;em&gt;proxy&lt;/em&gt; corpus, and every artifact fitted from that proxy is stamped as &lt;code&gt;proxy&lt;/code&gt; in its provenance so it can never be mistaken for a live-tenant result.&lt;/p&gt;

&lt;h2&gt;
  
  
  One engine, more than one cloud
&lt;/h2&gt;

&lt;p&gt;Every substrate-independence claim deserves suspicion until a second substrate exists. NV now has one: the same engine that reconstructs an Entra/M365 intrusion reconstructs an &lt;strong&gt;AWS&lt;/strong&gt; one from CloudTrail.&lt;/p&gt;

&lt;p&gt;The design that makes this cheap is that the engine core — the graph builder, the confidence model, the abductive layer, the trust floor — never knew anything about Microsoft in the first place. Only the &lt;em&gt;vocabulary&lt;/em&gt; is provider-specific: which operation maps to which ATT&amp;amp;CK technique, and whether a step is an initial-access, pivot, or collection move. That vocabulary now lives in provider packs, and adding AWS meant adding a pack (IAM/STS/S3 → ATT&amp;amp;CK) plus a CloudTrail normalizer that emits the same event model. No engine change. The authorization gate generalized the same way: on AWS you scope-gate to an &lt;strong&gt;account&lt;/strong&gt; instead of a tenant domain, and NV still refuses to run on any estate you haven't explicitly authorized.&lt;/p&gt;

&lt;p&gt;Two details from the AWS eval are worth calling out because they're the honesty model doing its job on a new substrate, unmodified:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A deliberately-unknown IAM operation (&lt;code&gt;PutRolePermissionsBoundary&lt;/code&gt; — no pack names it) survives ingestion, gets reconstructed abductively, and is flagged &lt;code&gt;no known match&lt;/code&gt; at reduced confidence. The abductive layer doesn't care whose cloud it is.&lt;/li&gt;
&lt;li&gt;An AWS &lt;code&gt;ConsoleLogin&lt;/code&gt; lands &lt;em&gt;below&lt;/em&gt; the trust floor that an Entra sign-in clears. That's not a bug — CloudTrail carries no device-posture signal, so an AWS login genuinely corroborates less, and NV refuses to pretend otherwise. A tool that fabricated a device signal to make the number look better would be exactly the kind of tool this project exists to not be.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current AWS pack is deliberately narrow — IAM, STS, and S3 object reads, the identity-abuse core. It's a proof of generalization, not parity. Widening it is straightforward now that the seam exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  Staying current without going blind
&lt;/h2&gt;

&lt;p&gt;Every reconstruction tool meets the same two objections: &lt;em&gt;your patterns will go stale&lt;/em&gt;, and &lt;em&gt;you'll be useless on something novel&lt;/em&gt;. NV answers them on two independent fronts.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;known layer&lt;/strong&gt; stays fresh through a validated update pipeline — MITRE ATT&amp;amp;CK (STIX), vetted CTI over TAXII 2.1, and the Sigma community ruleset, pulled on a per-feed cadence. Ingestion is treated as adversarial: every pattern, built-ins included, passes a trusted-source allowlist, a content checksum, schema validation, and an ATT&amp;amp;CK technique-id check before it enters the library, with an audit record for every admission and every rejection. An auto-update pipeline is a poisoning surface in disguise — a bad feed that injects false patterns manufactures false reconstructions — so the same skepticism the engine applies to evidence, it applies to its own patterns. (The feeds are scoped to the M365 beachhead they were built for; they refresh the Entra bindings and leave the AWS pack alone rather than over-mapping across substrates.)&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;abductive layer&lt;/strong&gt; covers what no feed has named yet. When an observed operation matches no known pattern, NV doesn't drop it — it reconstructs from first principles and flags it &lt;code&gt;no known match&lt;/code&gt; at reduced confidence. The eval suite proves this twice over now: on a deliberately-unknown managed-identity token-theft step in the Entra corpus, and on the unknown IAM operation in the AWS one. Both caught, sequenced correctly, and honestly down-weighted. A signature engine goes blind on the zero-day; the abductive core degrades gracefully instead of going dark.&lt;/p&gt;

&lt;p&gt;The ingest layer is built to serve that guarantee, not undercut it. The normalizers keep every identity-plane operation — including ones NV has never named — so a novel operation reaches the abductive core instead of being silently filtered out upstream. And they never fabricate a signal: if the source record carries no IP, no device, no consent marker, the field is simply absent, and the scorer reads absence as missing evidence rather than inventing corroboration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it on public data
&lt;/h2&gt;

&lt;p&gt;The demo needs neither your tenant nor anyone's cooperation. It runs on the public invictus-ir Office 365 unified audit log corpus, so you can see the whole pipeline end to end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://gitlab.com/dobybaxter127/nimbus-vestige
&lt;span class="nb"&gt;cd &lt;/span&gt;nimbus-vestige
&lt;span class="c"&gt;# open nv_gui.html — it renders the bundled demo reconstruction&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you point it at real data, that data never leaves your environment — NV is fully self-hosted. You export the M365 unified audit log (CSV with the standard &lt;code&gt;AuditData&lt;/code&gt; column, or JSON/JSONL), normalize it, and run the engine scope-gated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. normalize raw O365/Entra audit logs into the event model&lt;/span&gt;
python3 nv_extract_identity_events.py auditrecords.csv nv_identity_events.jsonl

&lt;span class="c"&gt;# 2. reconstruct — scope-gated to a tenant you are authorized to investigate&lt;/span&gt;
python3 run_nv.py nv_identity_events.jsonl reconstruction.json &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--trust-floor&lt;/span&gt; 0.6 &lt;span class="nt"&gt;--authorize&lt;/span&gt; your-tenant.onmicrosoft.com

&lt;span class="c"&gt;# 3. view — open nv_gui.html and click "load reconstruction.json"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The GUI is a pure view layer: load the file the engine just produced and the same screen shows your incident, your entities, your confidence bands — and every dot on the embedding canvas re-runs the reconstruction for that entity on click, so pivoting from "the incident" to "what did &lt;em&gt;this&lt;/em&gt; account actually do" is one click, not a re-run.&lt;/p&gt;

&lt;p&gt;The AWS path is the same three steps against CloudTrail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 nv_extract_cloudtrail_events.py cloudtrail.json aws_events.jsonl
python3 run_nv.py aws_events.jsonl reconstruction.json &lt;span class="nt"&gt;--authorize&lt;/span&gt; aws:123456789012
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What it isn't, and where it's early
&lt;/h2&gt;

&lt;p&gt;Being straight about the boundaries, since I'd want the same from anyone else's tool:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not a detector or a SIEM.&lt;/strong&gt; It comes in after something is flagged, to explain how the pieces fit. It will not alert you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's identity-first.&lt;/strong&gt; Entra/M365 is the deep substrate; AWS is in as an eval-proven second one, but its pack is IAM/STS/S3 only so far. Wider AWS coverage and a third substrate (GCP, Okta) are the next steps — and network, endpoint, and OT remain roadmap.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Calibration is currently demonstrated on public / proxy data.&lt;/strong&gt; Live-tenant weights are what you would actually deploy, and that needs a run in your estate.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;It's built by one person and it's early.&lt;/strong&gt; That is exactly why I'm posting this.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://nimbus-vestige-88b564.gitlab.io/" rel="noopener noreferrer"&gt;https://nimbus-vestige-88b564.gitlab.io/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://gitlab.com/dobybaxter127/nimbus-vestige" rel="noopener noreferrer"&gt;https://gitlab.com/dobybaxter127/nimbus-vestige&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core engine, both provider ingests, GUI, and offline eval harness are free and open. There's a one-time commercial license for teams that want the curated live feeds, calibration tooling, and support — but nothing about that gates the core, and nothing asks you to send your telemetry anywhere.&lt;/p&gt;

</description>
      <category>security</category>
      <category>dfir</category>
      <category>cloud</category>
      <category>opensource</category>
    </item>
    <item>
      <title>AI as a Laundering Machine: What Software Engineering Taught Me About Accountability</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Sun, 19 Jul 2026 14:04:19 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/ai-as-a-laundering-machine-what-software-engineering-taught-me-about-accountability-14ah</link>
      <guid>https://dev.to/dobybaxter127/ai-as-a-laundering-machine-what-software-engineering-taught-me-about-accountability-14ah</guid>
      <description>&lt;p&gt;&lt;em&gt;If an employer uses AI to decide whether I'm worth interviewing, we call it innovation. If I use the same technology to write more clearly or work around a disability, we call it cheating. The algorithms haven't changed — only who benefits.&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;There's an asymmetry in how we talk about AI and work that I can't stop thinking about, because when an employer runs ten thousand applications through a ranking model, we call it efficiency. When an applicant uses that same technology to organize their thoughts or compensate for a cognitive barrier, the language suddenly turns to authenticity, fairness, and "unfair advantage."&lt;/p&gt;

&lt;p&gt;The technology hasn't changed. The algorithms haven't changed. Only one variable has: who benefits from the automation.&lt;/p&gt;

&lt;p&gt;As a software engineer, that asymmetry tells me our real disagreement isn't about AI at all. It's about who is allowed to use technology to amplify themselves, who has to justify it and who is answerable when automated systems shape someone else's life.&lt;/p&gt;

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

&lt;p&gt;Most debates about AI in hiring ask whether the algorithms are biased. Fair enough, but bias has existed in hiring for as long as hiring has. AI didn't invent discrimination. The more profound question is what AI does to &lt;strong&gt;accountability&lt;/strong&gt;. AI can become a &lt;em&gt;laundering machine&lt;/em&gt;. I use the word deliberately. Money laundering doesn't create dirty money, it changes its appearance until the origin is impossible to trace. The reality stays the same; the provenance disappears.&lt;/p&gt;

&lt;p&gt;A recruiter who rejects you leaves a chain of responsibility: someone read the application, someone reached a conclusion, someone exercised judgement. Now route that same rejection through applicant tracking systems, scoring models, ranking algorithms, and configurable thresholds. The outcome is identical, but the decision appears to have no author. "A recruiter rejected this" becomes "the model assigned a low suitability score." Judgement becomes an optimization objective. Discretion becomes probability. The rejection hasn't vanished; it's been dressed in the vocabulary of mathematics until it looks objective.&lt;/p&gt;

&lt;p&gt;That's the laundering machine. It doesn't wash away prejudice. It washes away prejudice's apparent &lt;em&gt;origin&lt;/em&gt;. The outcome remains real. Only responsibility becomes hard to see.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every abstraction hides causality
&lt;/h2&gt;

&lt;p&gt;Here's where software engineering has something to say. Our entire discipline is built on abstraction. We hide complexity behind interfaces, APIs, frameworks, and platforms so that each engineer can reason about one problem without drowning in all the others. Abstraction is a force multiplier — modern software couldn't exist without it.&lt;/p&gt;

&lt;p&gt;But every layer that hides complexity also hides &lt;strong&gt;causality&lt;/strong&gt;. We trade visibility for manageability, and most of the time that's a great deal. A web app calling an API doesn't need to know which processor ran the request. Responsibility is different. A software component can safely hide its internals because its behavior stays observable through its interface. Responsibility doesn't work that way. Put enough distance between a consequential decision and the humans who shaped it, and it stops being clear who exercised judgement, who accepted risk, and who should answer for the outcome.&lt;/p&gt;

&lt;p&gt;We already know this instinct in engineering. We'd never run production without logs, traces, and deployment histories — because once causality goes opaque, reliability becomes impossible. Yet when automated systems shape human decisions, we routinely accept a level of opacity we'd never tolerate in our own software. Observability can't be bolted on after a system fails. &lt;strong&gt;Accountability follows the same rule.&lt;/strong&gt; If a socio-technical system isn't designed to preserve attribution from the start, responsibility won't magically appear later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody decided, but the system did
&lt;/h2&gt;

&lt;p&gt;Modern hiring isn't a recruiter at a desk. It's a socio-technical system: people, software, policies, incentives, and models whose &lt;em&gt;collective&lt;/em&gt; behavior decides who gets access to work. Distributed systems engineers know how these behave. A latency bug in one microservice cascades through a queue, triggers retries, saturates a database, and surfaces as an outage somewhere completely different. Every component behaved to spec. The system as a whole did something no one designed.&lt;/p&gt;

&lt;p&gt;Hiring works the same way. The feature engineer optimizes predictive performance. The data scientist evaluates accuracy. The product manager improves workflow. HR reduces cost. Procurement picks a proven vendor. Every participant can honestly describe their contribution without ever saying &lt;em&gt;"I decided this person shouldn't get an interview."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Yet collectively, the system decided exactly that.&lt;/p&gt;

&lt;p&gt;This is where distributed responsibility gets mistaken for &lt;em&gt;absent&lt;/em&gt; responsibility. But there's an old rule in engineering: &lt;strong&gt;someone owns every service.&lt;/strong&gt; When an outage hits, we don't shrug and say the system has no owners because many people built it. We investigate. We trace root causes. Complexity raises the effort of attribution — it never removes the need for it.&lt;/p&gt;

&lt;p&gt;Human consequences deserve no lower standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proxies inherit the past
&lt;/h2&gt;

&lt;p&gt;You can't optimize for "find the best person" — the phrase is too vague to be an objective. So, exactly as we do everywhere in engineering, we optimize &lt;em&gt;proxies&lt;/em&gt;: years of experience for competence, continuous employment for reliability, prestigious universities for ability.&lt;/p&gt;

&lt;p&gt;Individually, none of these are unreasonable. The problem is that a proxy always discards information — and quietly inherits the assumptions baked into the history it was derived from.&lt;/p&gt;

&lt;p&gt;If an organization historically overlooked people with employment gaps, unconventional backgrounds, or disabilities requiring flexible arrangements, those decisions become training data. The model doesn't understand the social circumstances behind the pattern. It just observes that certain applicants were historically preferred, and faithfully reproduces it because doing so improves its score.&lt;/p&gt;

&lt;p&gt;From the model's view, these patterns are predictive. From a human view, they may just be historical exclusion.&lt;/p&gt;

&lt;p&gt;And it's self-reinforcing: human judgement becomes training data, training data becomes a model, the model shapes new decisions, and those decisions become tomorrow's training data. "All models are wrong, but some are useful" — the danger isn't that the model simplifies reality. It's that we stop noticing what got discarded in the simplification.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Human in the loop" can be theatre
&lt;/h2&gt;

&lt;p&gt;The standard reassurance is that there's "always a human in the loop." From an engineering perspective, that tells us almost nothing about how the system behaves. Inserting a step into a workflow doesn't guarantee oversight. A deployment approval only matters if the reviewer has the information to spot risk, the authority to say no, and the time to actually look. When volume outpaces review capacity, approval becomes procedure, not judgement.&lt;/p&gt;

&lt;p&gt;We even have a name for the failure mode: &lt;strong&gt;automation bias.&lt;/strong&gt; Once software earns a reputation for being "usually right," reviewers start confirming its outputs instead of questioning them. The human quietly shifts from making decisions to rubber-stamping them. There's a useful distinction here from distributed systems: &lt;em&gt;monitoring&lt;/em&gt; a process is not &lt;em&gt;controlling&lt;/em&gt; it. A recruiter watching an algorithm's recommendations isn't necessarily exercising control over its influence. So "was a human involved?" is the wrong question. The right one is: &lt;em&gt;where&lt;/em&gt; did human judgement actually enter, and did meaningful authority survive once the recommendations were generated?&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility is not an unfair advantage
&lt;/h2&gt;

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

&lt;p&gt;When engineers optimize a system, we distinguish between &lt;em&gt;removing overhead&lt;/em&gt; and &lt;em&gt;increasing capability&lt;/em&gt;. Stripping out unnecessary overhead lets existing resources do more — it doesn't magically upgrade the hardware.&lt;/p&gt;

&lt;p&gt;Accessibility works the same way. Adaptive technology doesn't elevate someone beyond their ability; it removes irrelevant barriers that were obscuring that ability in the first place. A programmer's competence isn't their typing speed. A researcher's insight doesn't depend on whether they dictate or type. Which brings us right back to the opening asymmetry. An employer using AI to process ten thousand applications is "innovating." A disabled applicant using AI to organize their thoughts before submitting is "gaining an unfair advantage." Same technology, same computational principles — one reduces organizational workload, the other reduces human barriers, and we embrace the first while treating the second with suspicion.&lt;/p&gt;

&lt;p&gt;Accessibility has never been about lowering standards. It has always been about removing obstacles that stopped people from meeting those standards on equal terms.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsibility cannot be virtualized
&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 accountable. The law is society's version of the same instinct — a mechanism for preserving &lt;em&gt;provenance&lt;/em&gt;, so that decisions can be traced back to the people and duties behind them.&lt;/p&gt;

&lt;p&gt;Employment decisions deserve no less. If an automated system rejects you, meaningful accountability means more than knowing software was involved. It means being able to reconstruct which data informed it, which policies shaped it, who set the thresholds, and who accepted responsibility for deploying it. Complexity doesn't reduce that need. It strengthens it.&lt;/p&gt;

&lt;p&gt;Because here's the thing responsibility is not: it's not a computational resource. Software distributes computation across thousands of processors. Cloud platforms abstract away infrastructure. But responsibility can't be containerized, orchestrated, or load balanced until no identifiable owner remains. Someone still decided what to build, why, how it should operate, and what risks were acceptable. Those decisions stay irreducibly human.&lt;/p&gt;

&lt;p&gt;Algorithms have no moral agency. A neural network cannot appear before a court, defend a hiring policy, or apologize to someone whose life it altered. The danger was never that computers start acting like people. It's that people start believing responsibility belongs to the system rather than to themselves.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;The true measure of an intelligent society is not how successfully it automates decision-making. It's how carefully it preserves human accountability after those decisions have been automated.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Responsibility cannot be optimized away. It cannot be abstracted away. It cannot be virtualized. Because the legitimacy of every system we build depends, in the end, not on the intelligence of its algorithms — but on the willingness of the people who build, deploy, and govern them to remain visibly, demonstrably responsible for what follows.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>ethics</category>
      <category>a11y</category>
    </item>
    <item>
      <title>The Safety Net Is an Accessibility Feature: ADHD, AI, and How I Actually Build Software</title>
      <dc:creator>Doby Baxter</dc:creator>
      <pubDate>Fri, 17 Jul 2026 20:07:29 +0000</pubDate>
      <link>https://dev.to/dobybaxter127/the-safety-net-is-an-accessibility-feature-adhd-ai-and-how-i-actually-build-software-2pm</link>
      <guid>https://dev.to/dobybaxter127/the-safety-net-is-an-accessibility-feature-adhd-ai-and-how-i-actually-build-software-2pm</guid>
      <description>&lt;p&gt;A while back I wrote that you don't need perfect code — you need a safety net that talks back. People read it as an engineering post, which it was. But I left something out. I don't build those systems only because they're good engineering. I build them because they're how I stay functional as a developer with ADHD and autism. The clear error messages, the validation that runs before anything else does, the tests that fail loudly and specifically — those aren't just reliability features to me. They're external memory. They're the structure my own brain doesn't reliably hold on its own.&lt;/p&gt;

&lt;p&gt;I think good engineering and accommodation are, quietly, the same thing. Here's what I mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks me isn't difficulty. It's silence.
&lt;/h2&gt;

&lt;p&gt;The hard part of a bug, for me, is almost never the logic. I can sit with a hard problem for a long time. What wrecks me is ambiguity: the silent failure, the config that loads fine and then falls over three steps later with no clue why, the "it just doesn't work" with nothing to grab onto.&lt;/p&gt;

&lt;p&gt;That's not a character flaw and it's not laziness. It's what happens when a system asks me to hold its entire state in my head at once. My working memory doesn't do that well. So a system that fails quietly is asking me for something I can't reliably give and a system that fails loudly, early, and specifically is handing the context back to me instead of demanding I reconstruct it. Here is the difference a message makes:&lt;/p&gt;

&lt;p&gt;This is the whole thing in miniature. Two versions of the same failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Error: invalid configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;versus:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timeout'&lt;/span&gt; &lt;span class="na"&gt;must be a positive integer (got&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;30s")&lt;/span&gt;
  &lt;span class="s"&gt;in pipeline.yaml, line &lt;/span&gt;&lt;span class="m"&gt;14&lt;/span&gt;
  &lt;span class="na"&gt;hint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;use a number of seconds, e.g. timeout&lt;/span&gt;&lt;span class="err"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one asks me to become a detective. The second one just tells me what's true and where to go. For any developer that's a nicer experience. For a brain that struggles to hold a stack of context, it's the difference between staying in the work and losing the whole thread.&lt;/p&gt;

&lt;p&gt;Once you notice that, you start seeing accessibility everywhere in ordinary engineering. Good defaults reduce decisions. Loud tests remove the "did I break something?" background anxiety. Reproducible environments mean I don't have to remember the fourteen steps that make the thing run. None of that is a special accommodation bolted on for disabled developers. It's just good design that happens to help most where people have the least slack to spare.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI actually fits
&lt;/h2&gt;

&lt;p&gt;AI landed in the middle of all this as, essentially, one more thing that talks back. And I want to be honest about it, because the hype and the doom are both exhausting. Where it genuinely helps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Activation energy.&lt;/strong&gt; The blank page is a real, physical barrier for me. Getting from &lt;em&gt;nothing&lt;/em&gt; to &lt;em&gt;something I can react to&lt;/em&gt; is the expensive part, and reacting is much cheaper than initiating. A rough first draft I can push against gets me moving in a way willpower never has.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A software that answers.&lt;/strong&gt; Talking through a design out loud, even to a model that's sometimes wrong, helps me externalize the thought instead of looping on it internally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turning vague intent into a starting structure.&lt;/strong&gt; "I want a validator that does roughly this" into a skeleton I can then correct. The correcting is where I'm actually good.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But also AI is confidently wrong in exactly the way that hurts me most. It papers over the silent-failure problem by inventing a plausible answer, which is the one thing my whole workflow is built to prevent. So I don't let it replace the safety net. I put the safety net around &lt;em&gt;it&lt;/em&gt;. Types, tests, and validation gatekeep its output the same way they gatekeep mine. It's a drafting partner working inside a system that checks its work. That distinction matters more for me than for most, but I'd argue it's the healthy way for anyone to use these tools. The model isn't the source of truth. The system that verifies the model is.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pattern underneath all of it
&lt;/h2&gt;

&lt;p&gt;If disability taught me one thing as an engineer, it's this: don't rely on willpower or memory to hold a system together. Externalize it. Make the system carry the load, so that being tired, or distracted, or in a fibro flare, or just human, doesn't quietly break everything.&lt;/p&gt;

&lt;p&gt;For a long time I thought of it as the compromised version of how a "real" developer works. I don't anymore. It's just good design. The system that fails loudly, explains itself, checks its own state, and doesn't depend on a perfect operator is more reliable for everyone who touches it. Building for the constraint made the thing better in general.&lt;/p&gt;

&lt;p&gt;There's a well-worn version of this idea: curb cuts were fought for by wheelchair users and now everyone with a stroller, a suitcase, or a bad knee uses them without thinking. Software is full of curb cuts waiting to be built. A clear error message is one. A config that validates before it runs is one. An environment that sets itself up is one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I don't usually put in my bio
&lt;/h2&gt;

&lt;p&gt;I'll be plain about this, because pretending otherwise doesn't help anyone reading it who's in the same place. I've been rejected by essentially every traditional developer route I've tried. And the one role I did land, I didn't lose because I couldn't do the work, I lost it because the environment couldn't bend even slightly to how I work. No adjustments, an inflexible structure, and a slow, predictable slide into burnout that had nothing to do with the code I was writing. For a long time I read that as a verdict on me. It isn't. It's a mismatch between real ability and a role built to accept only one shape of person.&lt;/p&gt;

&lt;p&gt;And here's the thing I most want to land: asking for adjustments is not asking for charity. Nobody calls a good error message a favor. Adjustments are simply the conditions under which I can do the work at all. Predictable hours, a manageable load, an environment that doesn't run me into the ground — those aren't perks that make a comfortable job nicer. They're the difference between me producing something real and me producing nothing. Remove them and you don't get a diminished version of the work. You get no work, from someone who was entirely capable of doing it.&lt;/p&gt;

&lt;p&gt;That's the same principle as everything above. Externalize the structure. Don't build a system — or a job — that only functions if the operator runs on willpower and never has a bad day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building sideways
&lt;/h2&gt;

&lt;p&gt;Here's where I've landed, and I don't say it with bitterness. We all function differently. Until the job market becomes more humane about that, until "can you do the work" stops being quietly replaced by "can you do the work in exactly this shape, these hours, this room", a lot of us are going to have to build sideways. Make our own way in. Contribute through the routes that don't have a broken filter standing at the door. That's what open source has been for me. It's how a merged contribution to a tool used across the industry, or a tool released alongside a European Space Agency project, exists at all not because a hiring funnel let me through, but because the work itself was open to anyone willing to do it. Non-traditional isn't lesser. For a lot of us it's the only door that was never locked.&lt;/p&gt;

&lt;p&gt;And there's an irony I can't get past. So much of this field was built by minds that work the way mine does, the deep, narrow focus; the pull toward systems and patterns; the need to understand a thing completely before trusting it. The traits being screened out at the top of the funnel now are, in a lot of cases, the same traits the industry was built on. We didn't wander into tech by accident. In a real sense the culture of it was ours first. Being filtered back out of it stings precisely because of that. I don't have a tidy fix for an entire hiring market. But I know what I can do: keep building in the open, keep making the systems I touch talk back, and keep writing this down — so the next person who's heard "no" more times than they can count knows the problem was never them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd actually leave you with
&lt;/h2&gt;

&lt;p&gt;You don't need perfect code, and you don't need a perfect brain either. What helps — what actually holds up on a hard day — is a system that talks back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make failures loud, early, and specific. Assume the person reading the error has zero context, because sometimes that person is you in six months, or you at 4pm with nothing left in the tank.&lt;/li&gt;
&lt;li&gt;Externalize state instead of trusting memory. If the setup has fourteen steps, that's fourteen chances to fall off. Automate them.&lt;/li&gt;
&lt;li&gt;Let AI reduce activation energy, but never let it be the safety net. Put the checks around it.&lt;/li&gt;
&lt;li&gt;Design for your worst day, not your best one. The version of you with full focus and energy doesn't need the help. Build for the other version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is charity for the neurodivergent. It's how I ship reliable software, and the fact that it also makes the work survivable for a brain like mine isn't a coincidence. It's the point. The safety net was an accessibility feature the whole time. I just didn't have the words for it until I stopped being embarrassed about needing one.&lt;/p&gt;

&lt;p&gt;I'd really like to hear from you on this one. If you build differently because of ADHD, autism, chronic illness, or anything else about how your particular brain and body work, what's the adjustment or the habit that actually makes the work possible for you? And if you've had to build sideways into this industry rather than through the front door, I'd love to know what that looked like. Drop it in the comments. I read all of them, and I suspect a lot of us are quietly solving the same problems alone.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>a11y</category>
      <category>testing</category>
      <category>adhd</category>
    </item>
  </channel>
</rss>
