<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://marcosbarbero.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://marcosbarbero.com/" rel="alternate" type="text/html" hreflang="en" /><updated>2026-10-05T14:42:20+00:00</updated><id>https://marcosbarbero.com/feed.xml</id><title type="html">Marcos Barbero</title><subtitle>I help engineering teams and their leadership get real, measurable leverage out of AI coding assistants — and build the architecture that holds up underneath.</subtitle><author><name>Marcos Barbero</name><email>marcos@marcosbarbero.com</email></author><entry><title type="html">Where documentation lives</title><link href="https://marcosbarbero.com/blog/where-documentation-lives/" rel="alternate" type="text/html" title="Where documentation lives" /><published>2026-10-02T00:00:00+00:00</published><updated>2026-10-02T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/where-documentation-lives</id><content type="html" xml:base="https://marcosbarbero.com/blog/where-documentation-lives/"><![CDATA[<p>Somebody asks why the service retries three times instead of five. You know the
answer is written down. You do not know whether it is in the design doc, the
onboarding page, a comment on a ticket, or a message from eighteen months ago,
and finding out will take longer than working it out again.</p>

<p>That is the actual failure mode of documentation, and it is not staleness. A
stale document is at least findable. This one is fine, correct, and unlocatable,
because nothing about the way it is stored says what it is for.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="dk-title dk-desc">
      <title id="dk-title">What each document answers, and what needs none</title>
      <desc id="dk-desc">Five documents, each answering a different question, ordered from
        what is being built through to why it is built that way. Beside them, three things
        that have no document because an executable artefact already answers them.</desc>

      <text class="figure__note figure__note--strong" x="60" y="52">one question each</text>
      <circle class="figure__dot figure__dot--a" cx="76" cy="94" r="6" />
      <text class="figure__tick" x="96" y="98">what are we building, and for whom</text>
      <circle class="figure__dot figure__dot--a" cx="76" cy="128" r="6" />
      <text class="figure__tick" x="96" y="132">what does a person actually do</text>
      <circle class="figure__dot figure__dot--a" cx="76" cy="162" r="6" />
      <text class="figure__tick" x="96" y="166">what does this screen do in every state</text>
      <circle class="figure__dot figure__dot--a" cx="76" cy="196" r="6" />
      <text class="figure__tick" x="96" y="200">why is it built this way, and what did we reject</text>
      <circle class="figure__dot figure__dot--a" cx="76" cy="230" r="6" />
      <text class="figure__tick" x="96" y="234">how did we solve this before</text>

      <line class="figure__rule" x1="530" y1="70" x2="530" y2="270" stroke-dasharray="6 6" />

      <text class="figure__note figure__note--strong" x="556" y="94">no document at all</text>
      <circle class="figure__dot figure__dot--b" cx="566" cy="136" r="6" />
      <text class="figure__tick" x="586" y="140">the API shape</text>
      <circle class="figure__dot figure__dot--b" cx="566" cy="170" r="6" />
      <text class="figure__tick" x="586" y="174">the architecture rules</text>
      <circle class="figure__dot figure__dot--b" cx="566" cy="204" r="6" />
      <text class="figure__tick" x="586" y="208">the always-on conventions</text>
      <text class="figure__tick" x="556" y="248">each of these already runs</text>
    </svg>
  </div>
  <figcaption>
    <strong>A second description of something executable is a second source of
    truth.</strong> Nobody checks it, and it is wrong within a quarter.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="five-documents-five-questions">Five documents, five questions</h2>

<p>The fix that has held up for me is embarrassingly simple: give each kind of
document exactly one question, and put it where that kind lives.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>the question it answers</th>
      <th>mutable?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>product requirements</td>
      <td>what are we building, and for whom</td>
      <td>yes, until it ships</td>
    </tr>
    <tr>
      <td>user journey</td>
      <td>what does a person do, start to finish</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>feature states</td>
      <td>what does this screen do in every state</td>
      <td>yes</td>
    </tr>
    <tr>
      <td>decision record</td>
      <td>why is it built this way, and what did we reject</td>
      <td><strong>never</strong></td>
    </tr>
    <tr>
      <td>lexicon</td>
      <td>how did we solve this before</td>
      <td>append only</td>
    </tr>
  </tbody>
</table>

<p>The mutability column is doing more work than it looks. A document that can be
edited is a document whose history you do not have, and for four of these that is
fine. For the fifth it is the whole point.</p>

<h2 id="the-three-that-get-no-document">The three that get no document</h2>

<p>This is the part teams find surprising, and it is the part that keeps the folder
small enough to trust.</p>

<p><strong>The API shape</strong> does not get a document. There is a schema file, both sides are
generated from it, and a prose description would be a second source of truth that
nothing checks. <strong>The architecture rules</strong> do not get a document either: there is
a test, and it fails. <strong>The conventions an assistant needs on every turn</strong> live in
the always-on context file, which is
<a href="/blog/how-a-context-file-earns-its-length/">the only file with a price per turn</a>,
so anything look-up-able goes in a registry instead.</p>

<p>The rule generalises. If something is executable, describing it in prose creates
a second version that will disagree with the first, and the one nobody runs is
the one that will be wrong.</p>

<h2 id="the-order-they-get-written-in">The order they get written in</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>requirements   why this is worth building, and what done means for a user
  -&gt; journey      what the person actually does, step by step
    -&gt; states       what each screen shows, including the bad ones
      -&gt; issue        one implementable unit, with numbered requirements
        -&gt; decision     written when a choice closed off alternatives
</code></pre></div></div>

<p>Only the issue is what somebody, or something, implements. The rest exist so the
issue can be written well enough to be implementable, which is where most of the
failure actually happens: a competent implementation of an ambiguous ticket is
the most expensive thing a team can produce.</p>

<p>The decision record comes last on purpose. You cannot write it before the
decision, and writing it during the argument produces advocacy rather than a
record.</p>

<h2 id="why-this-got-sharper-recently">Why this got sharper recently</h2>

<p>Two people onboarding a year apart used to be the whole audience for this. It is
not any more, and the second reader has different economics.</p>

<p>A person who cannot find the retry decision loses twenty minutes, once. An
assistant that cannot find it re-derives it from scratch on every task, forever,
and produces something plausible each time. The cost of unlocatable documentation
stopped being a one-off and became a subscription.</p>

<p>That is also why the folder having a <code class="language-plaintext highlighter-rouge">README</code> explaining which file answers which
question is worth more than any individual file in it. It is the only page that
tells a stranger, human or otherwise, where to start.</p>

<hr />

<p><em>If your team writes documentation nobody can find,
<a href="/services/ai-adoption/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="documentation" /><category term="workflow" /><category term="architecture" /><category term="ai-assistants" /><summary type="html"><![CDATA[Your docs folder has forty files and nobody opens it. The problem is not that they are out of date. It is that nobody can tell which question any of them was written to answer.]]></summary></entry><entry><title type="html">Let your modules talk by events, not by calls</title><link href="https://marcosbarbero.com/blog/let-your-modules-talk-by-events-not-by-calls/" rel="alternate" type="text/html" title="Let your modules talk by events, not by calls" /><published>2026-09-11T00:00:00+00:00</published><updated>2026-09-11T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/let-your-modules-talk-by-events-not-by-calls</id><content type="html" xml:base="https://marcosbarbero.com/blog/let-your-modules-talk-by-events-not-by-calls/"><![CDATA[<p>Your availability screen needs to know which desks are taken. The bookings live
over there, so you call over there and ask. It takes a minute to write and it is
obviously correct.</p>

<p>It is also the moment your two components became one. Availability now cannot
answer while bookings is down, cannot be deployed separately, and cannot be
scaled for the read-heavy load it actually has. Nobody decided any of that, and
nobody will notice until the day one of them matters.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="rm-title rm-desc">
      <title id="rm-title">A query into the write side against an event out of it</title>
      <desc id="rm-desc">Above, the read side calls into the write side to answer a
        question, so it cannot answer while the write side is down. Below, the write side
        publishes an event and the read side keeps its own copy, so neither calls the other.</desc>

      <text class="figure__note figure__note--strong" x="60" y="52">a query</text>
      <circle class="figure__dot figure__dot--b" cx="200" cy="96" r="7" />
      <text class="figure__tick" x="120" y="76" text-anchor="middle">read side</text>
      <circle class="figure__dot figure__dot--b" cx="560" cy="96" r="7" />
      <text class="figure__tick" x="640" y="76" text-anchor="middle">write side</text>
      <line class="figure__line figure__line--b" x1="214" y1="96" x2="546" y2="96" />
      <text class="figure__tick" x="380" y="128" text-anchor="middle">cannot answer while the other is down</text>

      <text class="figure__note figure__note--strong" x="60" y="196">an event</text>
      <circle class="figure__dot figure__dot--a" cx="200" cy="240" r="7" />
      <text class="figure__tick" x="120" y="220" text-anchor="middle">read side</text>
      <circle class="figure__dot figure__dot--a" cx="560" cy="240" r="7" />
      <text class="figure__tick" x="640" y="220" text-anchor="middle">write side</text>
      <line class="figure__line figure__line--a" x1="546" y1="240" x2="214" y2="240" />
      <text class="figure__tick" x="380" y="272" text-anchor="middle">keeps its own copy, and neither one calls the other</text>
    </svg>
  </div>
  <figcaption>
    <strong>The direction of the arrow is the whole decision.</strong> One of
    these can be split into two deployables later and one cannot.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="turn-the-arrow-around">Turn the arrow around</h2>

<p>The alternative is that the write side announces what it did and the read side
keeps its own answer.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// booking, when it succeeds</span>
<span class="n">events</span><span class="o">.</span><span class="na">publish</span><span class="o">(</span><span class="k">new</span> <span class="nc">DeskBooked</span><span class="o">(</span><span class="n">booking</span><span class="o">.</span><span class="na">id</span><span class="o">(),</span> <span class="n">deskId</span><span class="o">,</span> <span class="n">date</span><span class="o">,</span> <span class="n">clock</span><span class="o">.</span><span class="na">instant</span><span class="o">()));</span>

<span class="c1">// availability, which never calls booking</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">on</span><span class="o">(</span><span class="nc">DeskBooked</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span>
    <span class="n">occupied</span><span class="o">.</span><span class="na">occupy</span><span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">deskId</span><span class="o">(),</span> <span class="n">event</span><span class="o">.</span><span class="na">date</span><span class="o">());</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Neither one calls the other now. The coupling is a data shape rather than a
method signature, and a data shape is the thing that survives being put on a
queue when you eventually need to.</p>

<h2 id="three-decisions-inside-that-which-are-easy-to-get-wrong">Three decisions inside that, which are easy to get wrong</h2>

<p><strong>Publish facts, not aggregates.</strong> The event carries an id, a desk and a date. It
does not carry the <code class="language-plaintext highlighter-rouge">Booking</code>. Pass the whole object and every field of an
internal type becomes part of the contract between components, and the first
rename is a breaking change nobody expected to be making.</p>

<p><strong>Give the events a home other things may read.</strong> In my own service the rule is
that availability may import <code class="language-plaintext highlighter-rouge">booking.domain.event</code> and nothing else in booking,
and an architecture test says so. Without a named place, “published” and
“internal” are the same package and the boundary lasts until somebody is in a
hurry.</p>

<p><strong>Make the handler idempotent, before you need it.</strong> In process an event arrives
once. The day it comes from a broker it arrives at least once, sometimes twice,
and a handler that increments rather than sets will drift. Write it so replaying
changes nothing, and write the test that proves it while it still looks like
paranoia.</p>

<h2 id="the-test-that-has-to-exist">The test that has to exist</h2>

<p>Here is the failure that this design introduces, and it looks like nothing.</p>

<p>“Booking publishes an event” is testable. “The projection reacts to an event” is
testable. Both can be green while nothing is subscribed, and then the read model
is simply never updated. No error, no exception, no failing test. A desk that was
taken an hour ago sits there looking free.</p>

<p>So one test boots the whole thing, books through the real use case, and asks the
read side:</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">this</span><span class="o">.</span><span class="na">bookDesk</span><span class="o">.</span><span class="na">book</span><span class="o">(</span><span class="no">A01</span><span class="o">,</span> <span class="no">TODAY</span><span class="o">,</span> <span class="s">"ada@example.com"</span><span class="o">);</span>

<span class="n">assertThat</span><span class="o">(</span><span class="n">availabilityOf</span><span class="o">(</span><span class="no">A01</span><span class="o">)).</span><span class="na">isFalse</span><span class="o">();</span>
</code></pre></div></div>

<p>It is the only test in that suite that needs a container, and it is the only one
that would notice a missing subscription. Every design that decouples two things
needs one test that puts them back together.</p>

<h2 id="what-it-costs-said-plainly">What it costs, said plainly</h2>

<p>The read model becomes eventually consistent. In process the window is
microseconds, and it is not zero, and you should say so out loud rather than
discover it during an incident.</p>

<p>You also now have a delivery problem you did not have before: the write commits,
then the event is published, and a crash in between loses it. That is solvable,
and it is a real cost of this shape rather than a footnote.</p>

<p>The thing you get back is that the two halves can be separated later without a
rewrite, because the only thing between them is a message. That is the same trade
as
<a href="/blog/a-deterministic-core-with-reasoning-at-the-edges/">putting the decidable half in a script</a>:
choose the shape that keeps a later decision open, and pay a small, visible cost
now.</p>

<hr />

<p><em>If two parts of your system are welded together by a call nobody remembers
making, <a href="/services/architecture/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="architecture" /><category term="events" /><category term="correctness" /><category term="testing" /><summary type="html"><![CDATA[One part of your system needs to know what another part decided, so it asks. That call is a decision about deployment, scaling and availability that nobody in the room made.]]></summary></entry><entry><title type="html">UI states: design loading, empty and error before the happy path</title><link href="https://marcosbarbero.com/blog/ui-states-loading-empty-and-error/" rel="alternate" type="text/html" title="UI states: design loading, empty and error before the happy path" /><published>2026-08-30T00:00:00+00:00</published><updated>2026-08-30T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/ui-states-loading-empty-and-error</id><content type="html" xml:base="https://marcosbarbero.com/blog/ui-states-loading-empty-and-error/"><![CDATA[<p>The design you were given has three screens and they all have data in them. The
list has eight rows, the form is filled in, the confirmation is green.</p>

<p>Then you build it, and within an hour you need to know what it looks like while
it is loading, what it says when the list is empty, and what happens when the
thing the user just clicked was taken by somebody else four seconds ago. None of
that is in the design, so you decide it, at four in the afternoon, alone.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="st-title st-desc">
      <title id="st-title">Which states get designed and which get improvised</title>
      <desc id="st-desc">A screen has five states. The loaded one is designed in advance.
        Loading, empty, conflict and failure are usually decided at the end of the work, and
        they are the states a user meets when something has gone wrong for them.</desc>

      <text class="figure__note figure__note--strong" x="70" y="56">designed up front</text>
      <circle class="figure__dot figure__dot--a" cx="86" cy="92" r="7" />
      <text class="figure__tick" x="106" y="96">loaded, with data</text>

      <text class="figure__note figure__note--strong" x="70" y="150">decided at the end, if at all</text>
      <circle class="figure__dot figure__dot--b" cx="86" cy="186" r="7" />
      <text class="figure__tick" x="106" y="190">loading</text>
      <circle class="figure__dot figure__dot--b" cx="86" cy="216" r="7" />
      <text class="figure__tick" x="106" y="220">nothing to show</text>
      <circle class="figure__dot figure__dot--b" cx="86" cy="246" r="7" />
      <text class="figure__tick" x="106" y="250">somebody beat you to it</text>
      <circle class="figure__dot figure__dot--b" cx="86" cy="276" r="7" />
      <text class="figure__tick" x="106" y="280">the request failed</text>

      <line class="figure__rule" x1="470" y1="60" x2="470" y2="292" stroke-dasharray="6 6" />
      <text class="figure__note figure__note--strong" x="500" y="150">the four below the line</text>
      <text class="figure__tick" x="500" y="176">are the ones a person</text>
      <text class="figure__tick" x="500" y="196">sees on the day the</text>
      <text class="figure__tick" x="500" y="216">system lets them down</text>
    </svg>
  </div>
  <figcaption>
    <strong>A feature is not designed until the bad states are written down.</strong>
    They are the ones that get improvised, and improvised is how the same error
    ends up worded three ways.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="the-four-that-get-skipped-are-the-four-that-matter">The four that get skipped are the four that matter</h2>

<p>A screen has more states than it has designs. The ones that get drawn are the
ones where everything worked, and the ones that get improvised are the ones a
person meets on the day the system is letting them down.</p>

<p>That is exactly backwards. Nobody remembers a list that loaded. Everybody
remembers being told “an error occurred” while the thing they wanted disappeared.</p>

<p>So the rule is small and blunt: <strong>a feature is not designed until its loading,
empty, conflict and failure states are written down.</strong> Four extra lines in the
document, decided when there is time to think, instead of at the end when there
is not.</p>

<h2 id="what-the-conflict-state-is-really-for">What the conflict state is really for</h2>

<p>Of the four, the conflict is the one teams argue about, because it feels rare.
It usually is not. Any system where people choose from a shared pool has a moment
where two of them choose the same thing, and if your users mostly act at the same
time of day, that moment is not an edge case, it is a Tuesday.</p>

<p>It is also the only state where the system says no to somebody who did nothing
wrong. That deserves better than a generic message, and it is where a
requirement comes from:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Scenario: somebody else took it first
  Given a desk that was free when the page loaded
  When a member books it and it has since been taken
  Then the board says that desk went, keeps the date they had chosen,
    and shows the list already updated
</code></pre></div></div>

<p>Read that back as three product decisions. The message is specific. The context
the user built up is not thrown away. And the screen is usable again immediately,
without a reload.</p>

<p>None of that survives being decided at four in the afternoon.</p>

<h2 id="where-the-wording-belongs">Where the wording belongs</h2>

<p>One rule saves more argument than any other: <strong>failure wording belongs to the
API, not to the screen.</strong></p>

<p>The client renders the message it was sent. It does not invent one, and it does
not translate one. Break that and the same failure is worded three ways in three
clients, drifts apart, and the support team learns three vocabularies for one
event.</p>

<p>It has a useful side effect. Once the wording belongs to the API, a contract test
should be strict about the status and loose about the text, because the text is
not part of the coupling. A design decision made about a screen turns out to
decide what a test asserts, which is the sort of thing you only notice when both
are written down.</p>

<h2 id="the-cheapest-version-of-this">The cheapest version of this</h2>

<p>You do not need a design system or a new document type. You need a table, in
whatever file already describes the feature, with one row per state and one
sentence about what is on screen and why.</p>

<table>
  <thead>
    <tr>
      <th>state</th>
      <th>what is on screen</th>
      <th>why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>loading</td>
      <td>a status the screen reader announces</td>
      <td>a blank frame reads as broken</td>
    </tr>
    <tr>
      <td>empty</td>
      <td>the filter, and an empty list</td>
      <td>hiding the filter removes the way back</td>
    </tr>
    <tr>
      <td>conflict</td>
      <td>the API’s message, list refreshed</td>
      <td>they must be able to pick another</td>
    </tr>
    <tr>
      <td>failed</td>
      <td>the API’s message</td>
      <td>never a message invented here</td>
    </tr>
  </tbody>
</table>

<p>Writing that took four minutes. It settles arguments that otherwise happen twice,
once during build and once during review, and it gives the person building the
screen something to check against rather than something to invent. That is the
same reason
<a href="/blog/a-deterministic-core-with-reasoning-at-the-edges/">a rule you can execute beats a rule you can read</a>.</p>

<hr />

<p><em>If your team’s designs stop at the happy path,
<a href="/services/ai-adoption/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="product" /><category term="ux" /><category term="architecture" /><category term="correctness" /><summary type="html"><![CDATA[Your designs are three happy-path screens. The states a user actually meets on a bad day get invented in the last two days of the sprint, by whoever is holding the ticket.]]></summary></entry><entry><title type="html">What a reviewer must not review</title><link href="https://marcosbarbero.com/blog/what-a-reviewer-must-not-review/" rel="alternate" type="text/html" title="What a reviewer must not review" /><published>2026-08-11T00:00:00+00:00</published><updated>2026-08-11T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/what-a-reviewer-must-not-review</id><content type="html" xml:base="https://marcosbarbero.com/blog/what-a-reviewer-must-not-review/"><![CDATA[<p>You open a pull request with eleven comments on it. Four are about import order.
Three suggest a different name for a variable. Two ask whether a class belongs in
that package. One says the tests do not actually check the thing the ticket
asked for.</p>

<p>Guess which one gets addressed last, if at all. The author works down the list,
the easy ones go first, and by the time they reach the eleventh they have been
in the diff for forty minutes and want it merged.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 320" role="img" aria-labelledby="rs-title rs-desc">
      <title id="rs-title">What a gate already decided, and what is left to review</title>
      <desc id="rs-desc">Five concerns arrive at a review. Formatting, architecture, coverage
        and mutation have already been decided by gates and are marked settled. One question
        remains: whether the change does what the ticket asked, proven by a test that
        asserts it.</desc>

      <text class="figure__note figure__note--strong" x="60" y="52">already decided</text>
      <line class="figure__line figure__line--b" x1="60" y1="88" x2="330" y2="88" />
      <text class="figure__tick" x="346" y="92">formatting</text>
      <line class="figure__line figure__line--b" x1="60" y1="126" x2="330" y2="126" />
      <text class="figure__tick" x="346" y="130">architecture rules</text>
      <line class="figure__line figure__line--b" x1="60" y1="164" x2="330" y2="164" />
      <text class="figure__tick" x="346" y="168">coverage</text>
      <line class="figure__line figure__line--b" x1="60" y1="202" x2="330" y2="202" />
      <text class="figure__tick" x="346" y="206">mutation score</text>

      <text class="figure__note figure__note--strong" x="60" y="266">left to a person</text>
      <line class="figure__line figure__line--a" x1="60" y1="292" x2="330" y2="292" />
      <text class="figure__note figure__note--strong" x="346" y="296">does it do what was asked</text>

      <line class="figure__rule" x1="600" y1="70" x2="600" y2="300" stroke-dasharray="6 6" />
      <text class="figure__tick" x="620" y="150">a comment on any of</text>
      <text class="figure__tick" x="620" y="170">the first four buries</text>
      <text class="figure__tick" x="620" y="190">the only one that is</text>
      <text class="figure__tick" x="620" y="210">still open</text>
    </svg>
  </div>
  <figcaption>
    <strong>A gate has already said yes, or the diff would not be in front of
    you.</strong> Saying it again is not thoroughness, it is noise with a
    reviewer's name on it.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="the-comments-that-should-not-exist">The comments that should not exist</h2>

<p>Every one of those first ten was decidable. Import order is a formatter.
Naming is a lint rule or it is a preference nobody should be spending review on.
Whether a class belongs in a package is an architecture rule, and if you cannot
write that rule, the boundary was never agreed in the first place.</p>

<p>If a machine can decide it and the machine has not been asked, the fix is to ask
the machine, not to ask a person every time forever.</p>

<table>
  <thead>
    <tr>
      <th>the concern</th>
      <th>who should decide it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>formatting, import order</td>
      <td>a formatter, applied automatically</td>
    </tr>
    <tr>
      <td>naming conventions</td>
      <td>a lint rule, or nobody</td>
    </tr>
    <tr>
      <td>package boundaries</td>
      <td>an architecture test</td>
    </tr>
    <tr>
      <td>test coverage</td>
      <td>a threshold in the build</td>
    </tr>
    <tr>
      <td>whether tests assert anything</td>
      <td>a mutation score</td>
    </tr>
    <tr>
      <td><strong>whether the change does what was asked</strong></td>
      <td><strong>a person</strong></td>
    </tr>
  </tbody>
</table>

<p>Only the last row needs judgement. Everything above it arrives at the review
already answered, and answering it again is not thoroughness.</p>

<h2 id="say-it-out-loud-in-the-instructions">Say it out loud, in the instructions</h2>

<p>This is easy to agree with and hard to hold, because a reviewer who spots
something feels obliged to mention it. So it is worth writing down as an
instruction rather than a value.</p>

<p>The reviewer in my own harness is told, in as many words, what it may not do:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>## What you must not review

Formatting, import order, naming style, architecture boundaries, coverage
percentages, mutation score. All of those are already decided by gates, and a
gate has already said yes or the diff would not be in front of you.
Re-reviewing them is reviewer fatigue, it buries the one comment that mattered,
and it teaches people that review is noise.

If you believe a gate is wrong, say so as a separate note. Do not enforce it
again by hand.
</code></pre></div></div>

<p>That last paragraph is the release valve. Disagreeing with a gate is legitimate
and common. Doing it silently, one comment at a time, on somebody else’s change,
is how a rule ends up meaning two things.</p>

<h2 id="the-failure-it-exists-to-catch">The failure it exists to catch</h2>

<p>With everything mechanical settled, one question is left, and it has two links
that both have to hold: is there a scenario covering each requirement, and is
there a test that <em>asserts</em> it?</p>

<p>The second link is the one that goes wrong quietly. A test that runs the code and
checks <code class="language-plaintext highlighter-rouge">isNotNull</code> on a method whose whole job is a boundary condition is
coverage without verification. It passes every gate in the build. A mutation
score catches some of it; a reader who knows what the ticket asked catches the
rest.</p>

<p>That is the review worth a person’s attention, and it is exactly the thing that
gets crowded out by a comment about a blank line.</p>

<h2 id="two-more-that-stay-with-the-human">Two more that stay with the human</h2>

<p><strong>Scope.</strong> Anything in the diff no requirement asked for is a defect, even when
it is an improvement. Especially then: an unrequested improvement is a change
nobody planned, tested against, or can attribute later.</p>

<p><strong>The seam.</strong> If a diff changes a request or a response shape, the contract
should have changed first. A wire change that starts anywhere else is a defect
whether or not it happens to work, for the same reason
<a href="/blog/a-spec-and-a-contract-test-are-not-the-same-gate/">a spec and a contract test answer different questions</a>.</p>

<h2 id="what-to-do-about-it-on-monday">What to do about it on Monday</h2>

<p>Take your last ten reviews and sort the comments into the two columns above. Most
teams find eight in one and two in the other, and the two are the ones that
prevented a defect.</p>

<p>Then pick the largest bucket on the left and make it a gate. You will get the
review time back immediately, and the next person to read a review comment will
find one that was worth writing.</p>

<hr />

<p><em>If your reviews are long and your defects still ship,
<a href="/services/ai-adoption/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="review" /><category term="workflow" /><category term="correctness" /><category term="ai-assistants" /><summary type="html"><![CDATA[Your reviews are not slow because there are too many pull requests. They are slow because your reviewers keep re-deciding things a gate already decided, and it buries the one comment that mattered.]]></summary></entry><entry><title type="html">A spec and a contract test are not the same gate</title><link href="https://marcosbarbero.com/blog/a-spec-and-a-contract-test-are-not-the-same-gate/" rel="alternate" type="text/html" title="A spec and a contract test are not the same gate" /><published>2026-07-27T00:00:00+00:00</published><updated>2026-07-27T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/a-spec-and-a-contract-test-are-not-the-same-gate</id><content type="html" xml:base="https://marcosbarbero.com/blog/a-spec-and-a-contract-test-are-not-the-same-gate/"><![CDATA[<p>Your two services agree about a schema. The file is checked in, both sides
generate from it, and neither can name a field the other does not have. Then a
release goes out and the front end shows the wrong thing anyway, and the spec is
still, exactly, correct.</p>

<p>That combination is confusing enough that teams usually conclude somebody skipped
a step. Nobody did. A specification describes what an API <em>may</em> do, and that is a
larger set than what any particular caller is relying on.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="pd-title pd-desc">
      <title id="pd-title">What the spec permits against what a client depends on</title>
      <desc id="pd-desc">A large dashed region shows every response the specification allows.
        A smaller solid region inside it shows the responses one client actually relies on.
        A change that moves a behaviour out of the smaller region while staying inside the
        larger one still matches the specification and breaks that client.</desc>

      <text class="figure__note figure__note--strong" x="432" y="46" text-anchor="middle">what the spec permits</text>
      <rect class="figure__rule" x="120" y="64" width="624" height="176" rx="10" fill="none" stroke-dasharray="6 6" />

      <text class="figure__note figure__note--strong" x="286" y="100">what this client depends on</text>
      <rect class="figure__rule" x="160" y="112" width="300" height="104" rx="10" fill="none" />

      <circle class="figure__dot figure__dot--a" cx="260" cy="168" r="7" />
      <text class="figure__tick" x="260" y="196" text-anchor="middle">409</text>

      <line class="figure__line figure__line--b" x1="274" y1="168" x2="596" y2="168" />
      <circle class="figure__dot figure__dot--b" cx="610" cy="168" r="7" />
      <text class="figure__tick" x="610" y="196" text-anchor="middle">400</text>
      <text class="figure__note figure__note--strong" x="636" y="164">still legal</text>

      <text class="figure__tick" x="432" y="274" text-anchor="middle">the spec still passes; the screen that had to say "that desk just went" does not</text>
    </svg>
  </div>
  <figcaption>
    <strong>A specification is deliberately permissive.</strong> It says what may
    happen, which is a larger set than what anybody is relying on.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="the-change-that-proves-it">The change that proves it</h2>

<p>Here is the one I ran deliberately, to find out whether the gap was real.</p>

<p>Booking a desk that is already taken answers <code class="language-plaintext highlighter-rouge">409</code>. The spec documents <code class="language-plaintext highlighter-rouge">400</code> and
<code class="language-plaintext highlighter-rouge">409</code> on that endpoint, because both are genuinely possible: a malformed body is
one, a conflict is the other. So I changed the conflict path to answer <code class="language-plaintext highlighter-rouge">400</code>.</p>

<table>
  <thead>
    <tr>
      <th>what checked it</th>
      <th>what it said</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>the OpenAPI file</td>
      <td>fine, 400 is documented</td>
    </tr>
    <tr>
      <td>the generated server interface</td>
      <td>unchanged</td>
    </tr>
    <tr>
      <td>the generated client types</td>
      <td>unchanged</td>
    </tr>
    <tr>
      <td>the unit tests</td>
      <td>green</td>
    </tr>
    <tr>
      <td>the browser</td>
      <td>shows a generic error and loses the date the member had chosen</td>
    </tr>
  </tbody>
</table>

<p>Nothing in either build noticed. Both sides were individually correct, both were
green, and they disagreed.</p>

<h2 id="what-the-pact-adds">What the pact adds</h2>

<p>A consumer contract is smaller and stricter than a spec, because it is written
from the other end. It does not describe the API, it records what one caller
would notice if it changed.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1) Verifying a pact between deskspace-frontend and deskspace-backend
   - a booking for a desk that is taken: has status code 409
     1.1) status: expected status of 409 but was 400
</code></pre></div></div>

<p>That is the only thing in either repository that said anything.</p>

<h2 id="the-part-that-stops-it-being-annoying">The part that stops it being annoying</h2>

<p>A contract test that is strict about everything is a test somebody deletes. So it
has to be strict about the coupling and loose about everything else, and knowing
which is which is a design decision, not a mechanical one.</p>

<p>In the same service, changing the problem <em>title</em> from “Desk already booked” to
“Conflict” does <strong>not</strong> fail verification, and it should not. The screen renders
whatever title the API sends, which is written down as a rule: failure wording
belongs to the API, not to the client, so it is worded once instead of three times
in three clients. The title is therefore not part of the coupling.</p>

<p>The status is. So the pact pins the status and the shape, and matches the title
by type.</p>

<h2 id="why-not-just-tighten-the-spec">Why not just tighten the spec</h2>

<p>The obvious alternative is to delete <code class="language-plaintext highlighter-rouge">400</code> from that endpoint so <code class="language-plaintext highlighter-rouge">409</code> is the
only failure. It works for this one case and it makes the document worse.</p>

<p><code class="language-plaintext highlighter-rouge">400</code> is genuinely possible there. Removing it to make the spec behave like a
test means the spec is now false, and the next person reading it to find out what
the API does gets a wrong answer. A permissive document plus a real gate beats a
document that has been bent into a gate, for the same reason
<a href="/blog/a-deterministic-core-with-reasoning-at-the-edges/">a script beats a paragraph</a>:
each thing should be doing the job it is good at.</p>

<h2 id="what-this-costs">What this costs</h2>

<p>Provider verification needs the states each interaction assumes, set up through
the real code paths rather than by writing rows directly. That is a real cost and
it is where most of the work is.</p>

<p>One thing that is not optional: pin the clock. A pact names a specific date, and
with a real clock that date drifts into the past until the service starts
rejecting it, and the build goes red having been touched by nobody. A red build
that nobody caused is the fastest way to teach a team to ignore a suite.</p>

<hr />

<p><em>If your services agree on a schema and disagree in production,
<a href="/services/architecture/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="architecture" /><category term="testing" /><category term="contracts" /><category term="correctness" /><summary type="html"><![CDATA[You have an OpenAPI file, both sides are generated from it, and your services still broke each other. The spec was never lying. It just permits more than anybody relies on.]]></summary></entry><entry><title type="html">Put the shared gate in CI, not in git hooks</title><link href="https://marcosbarbero.com/blog/shared-gates-in-ci-not-git-hooks/" rel="alternate" type="text/html" title="Put the shared gate in CI, not in git hooks" /><published>2026-07-03T00:00:00+00:00</published><updated>2026-07-03T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/shared-gates-in-ci-not-git-hooks</id><content type="html" xml:base="https://marcosbarbero.com/blog/shared-gates-in-ci-not-git-hooks/"><![CDATA[<p>Somebody joins on Monday, clones the repo and starts committing. Every check you
have written is sitting in that clone. None of them is running, and nothing tells
either of you, because git hooks are not cloned and the setting that enables them
is per machine.</p>

<p>So the honest description of local enforcement is that it works on the machines
where somebody turned it on, and you cannot see which those are.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="last-title last-desc">
      <title id="last-title">Local hooks vary; the shared gate does not</title>
      <desc id="last-desc">Three clones of the same repository, one with hooks enabled, one
        with them turned off and one where they were never configured, all send their work
        through a single shared gate, which is the only check that runs before anything
        reaches the live site.</desc>

      <text class="figure__tick" x="60" y="104">hooks on</text>
      <text class="figure__tick" x="60" y="174">hooks off</text>
      <text class="figure__tick" x="60" y="244">never configured</text>
      <circle class="figure__dot figure__dot--b" cx="250" cy="100" r="6" />
      <circle class="figure__dot figure__dot--b" cx="250" cy="170" r="6" />
      <circle class="figure__dot figure__dot--b" cx="250" cy="240" r="6" />

      <path class="figure__line figure__line--b" d="M262 100 L 508 152" />
      <path class="figure__line figure__line--b" d="M262 170 L 508 170" />
      <path class="figure__line figure__line--b" d="M262 240 L 508 188" />

      <text class="figure__note figure__note--strong" x="516" y="106" text-anchor="middle">one shared gate</text>
      <line class="figure__line figure__line--a" x1="516" y1="122" x2="516" y2="220" />

      <path class="figure__line figure__line--a" d="M524 170 L 736 170" />
      <circle class="figure__dot figure__dot--a" cx="748" cy="170" r="7" />
      <text class="figure__note" x="766" y="166">the live site</text>

      <text class="figure__tick" x="432" y="278" text-anchor="middle">the only rung that is the same for everybody</text>
    </svg>
  </div>
  <figcaption>
    <strong>Local enforcement is a per-machine setting.</strong> It is worth having
    for the speed, and it is not the thing you can make a promise about, because you
    cannot see which clones have it switched on.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="two-different-promises">Two different promises</h2>

<p>A local hook and a shared pipeline look like the same check written twice. They
are answering different questions.</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>a hook on a laptop</th>
      <th>a job in the pipeline</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>answers</td>
      <td>did <em>this</em> machine check?</td>
      <td>did <em>anything</em> reach the site unchecked?</td>
    </tr>
    <tr>
      <td>feedback</td>
      <td>instant, before the commit</td>
      <td>a minute later, after the push</td>
    </tr>
    <tr>
      <td>coverage</td>
      <td>whoever configured it</td>
      <td>everybody, always</td>
    </tr>
    <tr>
      <td>can be turned off</td>
      <td>quietly, per clone</td>
      <td>by a reviewed change to a tracked file</td>
    </tr>
  </tbody>
</table>

<p>Both are worth having, for different reasons. The hook is for speed, and speed
is what makes people keep a check rather than resent it. The pipeline is for
truth, and it is the only one you can make a promise about.</p>

<h2 id="the-trap-in-writing-it-twice">The trap in writing it twice</h2>

<p><a href="/blog/pre-invocation-gates/">A gate that runs before the action</a> is only
as good as the rule inside it, and the fastest way to rot the rule is to have two
copies of it. Write the check once as a script and call that script from both
places. The hook is four lines of shell around it. The pipeline job is a few
lines of configuration around it.</p>

<p>If the hook and the pipeline ever disagree, you have the same failure as a rule
in two directories: the local one passes, the shared one fails, and somebody
starts arguing that the pipeline is broken.</p>

<p>The version worth copying is blunter than that. In
<a href="https://github.com/marcosbarbero/deskspace">deskspace</a> there is one script,
<code class="language-plaintext highlighter-rouge">toolbox/verify</code>, and both callers are one line:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .github/workflows/verify.yml</span>
<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">toolbox/verify --mutation</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">toolbox/verify --mutation</span>
</code></pre></div></div>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># .githooks/pre-push</span>
<span class="s2">"</span><span class="nv">$REPO</span><span class="s2">/toolbox/verify"</span>
</code></pre></div></div>

<p>The pipeline has no steps of its own. Not because listing them would be hard, but
because a list is a second definition of done, and the two would agree right up
until somebody added a gate to one of them.</p>

<p>That script decides everything: the registries, formatting, lint, the
architecture rules, the tests, coverage, the consumer contract, whether the
generated client still matches the spec, and the evals that check the gates
themselves. One answer to “is it green”, with one place to change it.</p>

<h2 id="check-what-changed-not-everything">Check what changed, not everything</h2>

<p>The one design decision that decides whether this survives is scope. This site
carries an archive written years before half the current rules existed, and a
pipeline that swept the whole tree would fail on every single run.</p>

<p>A check that is always red stops being information. People learn its colour by
heart, stop reading it, and eventually somebody deletes it during an unrelated
cleanup and nobody objects. So the job checks the posts in the push and leaves
the archive alone, and the archive’s exemption is a fact about the range, not a
list of filenames somebody maintains.</p>

<h2 id="where-this-leaves-the-series">Where this leaves the series</h2>

<p>Nine parts, and the same move in every one of them. Reason about something once,
encode the result, then put the encoding somewhere it runs without anyone
choosing to run it.</p>

<p>The order was deliberate: a prompt is the least durable place, a script is more
durable than a document, a named sequence beats a remembered one, an index makes
it findable, and a gate makes it unavoidable. Each rung stores the same decision
somewhere slightly harder to lose. What is left over at the top, the part that
never becomes a script, is the part worth your attention, and it is smaller than
it looks before you start.</p>

<hr />

<p><em>If your team’s practices hold on the machines of the people who wrote them and
nowhere else, <a href="/services/ai-adoption/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="ai-assistants" /><category term="workflow" /><category term="determinism" /><category term="tooling" /><summary type="html"><![CDATA[Somebody joins on Monday and clones the repo. Every check you wrote is in that clone. None of them is running, and nothing tells either of you.]]></summary></entry><entry><title type="html">Pre-invocation gates: enforcing a rule before the action runs</title><link href="https://marcosbarbero.com/blog/pre-invocation-gates/" rel="alternate" type="text/html" title="Pre-invocation gates: enforcing a rule before the action runs" /><published>2026-06-20T00:00:00+00:00</published><updated>2026-06-20T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/pre-invocation-gates</id><content type="html" xml:base="https://marcosbarbero.com/blog/pre-invocation-gates/"><![CDATA[<p>You have written the same rule in three places. It is in the README, it is in
the contributing guide, and you have said it out loud in review twice this
quarter. It got broken again yesterday by somebody who agrees with it.</p>

<p>The fourth place will not help, and the reason is not that people are careless.
The rule and its enforcement live in different places, and only one of those
runs.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="gate-title gate-desc">
      <title id="gate-title">A gate after the action against a gate before it</title>
      <desc id="gate-desc">In the first sequence the action runs, a broken state exists, a
        check fails and the state has to be undone. In the second sequence the action is
        requested, the gate refuses, and nothing was created that anyone has to undo.</desc>

      <text class="figure__note figure__note--strong" x="60" y="64">a gate after the act</text>
      <text class="figure__tick" x="110" y="96" text-anchor="middle">you run it</text>
      <text class="figure__tick" x="300" y="96" text-anchor="middle">broken state exists</text>
      <text class="figure__tick" x="490" y="96" text-anchor="middle">check fails</text>
      <text class="figure__tick" x="700" y="96" text-anchor="middle">undo it</text>
      <line class="figure__line figure__line--b" x1="110" y1="120" x2="700" y2="120" />
      <circle class="figure__dot figure__dot--b" cx="110" cy="120" r="6" />
      <circle class="figure__dot figure__dot--b" cx="300" cy="120" r="6" />
      <circle class="figure__dot figure__dot--b" cx="490" cy="120" r="6" />
      <circle class="figure__dot figure__dot--b" cx="700" cy="120" r="6" />

      <text class="figure__note figure__note--strong" x="60" y="176">a gate before the act</text>
      <text class="figure__tick" x="110" y="208" text-anchor="middle">you ask for it</text>
      <text class="figure__tick" x="300" y="208" text-anchor="middle">the gate refuses</text>
      <text class="figure__tick" x="490" y="208" text-anchor="middle">nothing happened</text>
      <line class="figure__line figure__line--a" x1="110" y1="232" x2="490" y2="232" />
      <circle class="figure__dot figure__dot--a" cx="110" cy="232" r="6" />
      <circle class="figure__dot figure__dot--a" cx="300" cy="232" r="6" />
      <circle class="figure__dot figure__dot--a" cx="490" cy="232" r="6" />

      <text class="figure__tick" x="432" y="280" text-anchor="middle">the difference is whether the broken state ever existed</text>
    </svg>
  </div>
  <figcaption>
    <strong>Every gate costs the same to write and differs in when it fires.</strong>
    The late one is an argument about a change that already exists. The early one is
    a sentence nobody has to discuss.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="sort-your-rules-by-when-they-fire">Sort your rules by when they fire</h2>

<p>Take any rule your team has and ask one question about it: at what moment does
something consult it? Not how good the rule is, not how well it is written. When
does it get looked at, and what already exists by then.</p>

<table>
  <thead>
    <tr>
      <th>where the rule lives</th>
      <th>when it fires</th>
      <th>what exists by then</th>
      <th>cost of ignoring it</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>a document</td>
      <td>never, unless somebody remembers</td>
      <td>nothing yet</td>
      <td>none</td>
    </tr>
    <tr>
      <td>a review comment</td>
      <td>after the change</td>
      <td>the change, plus an argument</td>
      <td>a shrug</td>
    </tr>
    <tr>
      <td>a commit or push hook</td>
      <td>after the change, before it travels</td>
      <td>the change</td>
      <td>one flag</td>
    </tr>
    <tr>
      <td>a gate before the action</td>
      <td>before the change</td>
      <td>nothing</td>
      <td>there is no flag</td>
    </tr>
  </tbody>
</table>

<p>The same rule, worded identically, sits in all four rows. What separates them is
how much has already happened by the time anyone checks, and every row down that
table is a smaller pile of things to undo.</p>

<h2 id="test-first-as-a-preference-and-as-a-gate">Test-first, as a preference and as a gate</h2>

<p>Take a rule every team claims to hold: write the test first. As a document it is
uncontroversial and routinely skipped, because on a busy afternoon the test after
is obviously fine and nobody is watching.</p>

<p>As a gate before the action it looks like this. A hook runs on every file edit an
assistant attempts and refuses the ones touching production code on a branch with
no test change:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BLOCKED by the harness: test-first, backend.

You are editing backend/src/main/java/.../BookDesk.java
and this branch has no new or changed backend test.

Write the failing test first, then implement. The test is the specification: if
you cannot express the requirement as a failing test, the requirement is not yet
clear enough to implement.

A test on the other side of the wire does not count. A component test does not
prove a booking rule, and a service test does not prove a screen.

This is a gate, not a preference.
</code></pre></div></div>

<p>Asking an assistant to work test-first gets compliance most of the time. A
non-zero exit code gets it every time, and the difference between those two is
the whole argument for this rung. Nothing was created and then reverted. The edit
did not happen.</p>

<p>Read the fourth paragraph again, because it is the part that only shows up in a
system with more than one deployable.
<a href="https://github.com/marcosbarbero/deskspace">Deskspace</a> has a Java service and a
React front end, and the gate answers <strong>per side of the wire</strong>. A backend test on
the branch unlocks backend code and does not unlock a component; a component test
does the reverse. A harness with one deployable has no reason to get that right,
and is wrong the moment a second one appears.</p>

<h2 id="the-rung-most-people-never-build">The rung most people never build</h2>

<p>Almost every team has rows one and two of that table. Plenty have row three. Row
four is rare, and it is the only one where the wrong thing never exists.</p>

<p><a href="/blog/repo-discoverability-for-humans-and-agents/">Making an encoded rule findable</a>
matters here with the sign flipped. Every other kind of encoded rule has to be
discovered before it helps. This is the one kind that does not: it arrives at the
moment it is relevant, whether or not anybody opened the README.</p>

<h2 id="moving-the-gate-is-not-passing-it">Moving the gate is not passing it</h2>

<p>A gate that can be edited by the thing it constrains is decoration. So the same
harness denies a second, smaller set of actions outright: edits to the hooks, to
the architecture rules test, to the coverage and mutation thresholds, and pushes
carrying a flag that skips the local checks.</p>

<p>That deny list is short on purpose. It does not try to enumerate bad behaviour.
It protects the small number of files whose whole job is to say no, on the theory
that an assistant which cannot satisfy a gate will otherwise satisfy it by
lowering it, and will do so with a clear and reasonable explanation.</p>

<h2 id="the-design-constraint-that-keeps-it-alive">The design constraint that keeps it alive</h2>

<p>A gate with false positives gets switched off within a week, and takes the rule
with it. So the test-first hook is deliberately narrow: it ignores edits to
tests, docs, configuration and resources, fires only on production Java, and
carries one documented environment variable for demos.</p>

<p>That last part is not a loophole if it is loud. A bypass that prints why it
fired, in a variable nobody sets by accident, is different from a flag anybody
can append to a command out of habit.</p>

<h2 id="what-it-is-not">What it is not</h2>

<p>This is not a security boundary, and reading it as one will get you into trouble.
Anyone determined can open a second terminal, edit the file by hand, or work
somewhere the gate is not watching.</p>

<p>It removes the accidental case: the tired Friday, the flag copied out of an
answer online, the assistant helpfully working around an error it does not
understand. That case is not the rare one. It is nearly all of them.</p>

<h2 id="the-question-to-ask">The question to ask</h2>

<p>For each rule you have written down more than twice, ask what would have to be
true for it to be unbreakable rather than agreed. Sometimes the answer is a type,
sometimes a schema, sometimes a gate on the tool call. Often the answer is that
it should not be a rule at all, because nobody can say precisely when it applies,
and that is worth finding out too.</p>

<hr />

<p><em>If your organisation keeps agreeing on practices that do not survive contact
with a deadline, <a href="/services/company-transformation/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="ai-assistants" /><category term="workflow" /><category term="determinism" /><category term="tooling" /><summary type="html"><![CDATA[You have written the same rule in three places and it got broken again yesterday. Asking for test-first works most of the time. A non-zero exit code works every time.]]></summary></entry><entry><title type="html">AI coding maturity levels: what your team stopped reading</title><link href="https://marcosbarbero.com/blog/ai-coding-maturity-levels-l1-to-l5/" rel="alternate" type="text/html" title="AI coding maturity levels: what your team stopped reading" /><published>2026-06-04T00:00:00+00:00</published><updated>2026-06-04T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/ai-coding-maturity-levels-l1-to-l5</id><content type="html" xml:base="https://marcosbarbero.com/blog/ai-coding-maturity-levels-l1-to-l5/"><![CDATA[<p>Your team sits one level higher on the autonomy ladder in your head than it does
in your repository. Not because anyone is inflating it. You answered with the
level you have the tooling for, because that is the question everybody thinks is
being asked.</p>

<p>The tooling is not the level. What decides it is a subtraction: something a
person used to read, and no longer does.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 320" role="img" aria-labelledby="lad-title lad-desc">
      <title id="lad-title">How much a person still reads at each level of autonomy</title>
      <desc id="lad-desc">Five levels of autonomy shown as bars of decreasing length. At L1 a
        person reads every completion, at L2 every diff, at L3 the evidence and the gates
        rather than the code, at L3.5 only the risky changes, and at L4 only the exceptions.
        The largest drop is between L2 and L3.</desc>

      <text class="figure__tick" x="60" y="94">L1</text>
      <line class="figure__line figure__line--b" x1="140" y1="90" x2="600" y2="90" />
      <text class="figure__note" x="614" y="94">every completion</text>

      <text class="figure__tick" x="60" y="134">L2</text>
      <line class="figure__line figure__line--b" x1="140" y1="130" x2="540" y2="130" />
      <text class="figure__note" x="554" y="134">every diff</text>

      <text class="figure__tick" x="60" y="174">L3</text>
      <line class="figure__line figure__line--a" x1="140" y1="170" x2="380" y2="170" />
      <text class="figure__note figure__note--strong" x="394" y="174">the evidence, not the code</text>

      <text class="figure__tick" x="60" y="214">L3.5</text>
      <line class="figure__line figure__line--a" x1="140" y1="210" x2="280" y2="210" />
      <text class="figure__note" x="294" y="214">only the risky changes</text>

      <text class="figure__tick" x="60" y="254">L4</text>
      <line class="figure__line figure__line--a" x1="140" y1="250" x2="200" y2="250" />
      <text class="figure__note" x="214" y="254">the exceptions</text>

      <text class="figure__tick" x="432" y="298" text-anchor="middle">how much a person still reads, per change</text>
    </svg>
  </div>
  <figcaption>
    <strong>The jump from L2 to L3 is not a tooling change.</strong> It is the point
    where a person stops reading the code and starts reading what the checks say
    about it, which is only safe if the checks deserve it.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="the-ladder-as-a-sequence-of-things-you-stopped-reading">The ladder, as a sequence of things you stopped reading</h2>

<p>The levels-of-autonomy framing has converged across the industry over the last
couple of years, and the clearest articulation of it is Ben Blackmore’s
<a href="https://www.dash0.com/knowledge/the-six-levels-of-agentic-software-engineering">six levels of agentic software engineering</a>.
The levels are usually described by what the machine does. Read them instead by
what the human stopped doing, and they turn into a test you can actually apply:</p>

<table>
  <thead>
    <tr>
      <th>level</th>
      <th>what a person stopped reading</th>
      <th>what has to be true for that to be safe</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>L1</td>
      <td>nothing</td>
      <td>nothing; you judge every suggestion as it appears</td>
    </tr>
    <tr>
      <td>L2</td>
      <td>nothing yet, but faster</td>
      <td>you still read every diff before it merges</td>
    </tr>
    <tr>
      <td>L3</td>
      <td>the diff</td>
      <td>your tests, scans and gates are trustworthy enough to read instead</td>
    </tr>
    <tr>
      <td>L3.5</td>
      <td>the diff, on changes that qualify</td>
      <td>you can state which services qualify, and why</td>
    </tr>
    <tr>
      <td>L4</td>
      <td>the request itself</td>
      <td>the scope is narrow, bounded, and someone owns the escalations</td>
    </tr>
  </tbody>
</table>

<p>Every row is a transfer of attention, not an addition of capability. You are not
gaining a reviewer. You are giving up a reader and asking something else to take
the shift.</p>

<h2 id="the-claim-that-gives-it-away">The claim that gives it away</h2>

<p>The most common overstatement is a team that has adopted L3 behaviour with L2
evidence. People have stopped reading diffs properly, because reading a
forty-file generated change is miserable and the assistant is usually right. But
nothing was strengthened to replace them. The tests are the same tests. The scans
are the same scans.</p>

<p>That is not L3. That is L2 with the review quietly switched off, and it holds
until the first change that is plausible and wrong, which is the exact failure
mode automated review is supposed to catch and unexamined review never will.</p>

<p>The honest version of the question is not “are we at L3?” It is: <strong>what would
have caught it?</strong></p>

<h2 id="making-the-answer-specific">Making the answer specific</h2>

<p>The good news is that the question has a concrete answer, and it looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ ./gates.sh feature/new-pricing
  contract tests           pass    covers the changed endpoints
  mutation score           71%     threshold 70
  dependency scan          pass    no new advisories
  architecture rules       pass    no new inbound edges to billing
  migration reversible     FAIL    2 of 3 have no down step
</code></pre></div></div>

<p>Something like that is what a person at L3 reads instead of the diff. If your
equivalent is “CI is green” with no statement of what green covers, you have not
moved up a level. You have moved the reading somewhere nobody does it.</p>

<p>This is the same split as
<a href="/blog/a-deterministic-core-with-reasoning-at-the-edges/">a deterministic core with reasoning at the edges</a>,
applied to review rather than to work: the decidable part of a review becomes a
check with a name and a threshold, and human attention concentrates on the part
no check can express.</p>

<h2 id="where-the-leverage-actually-is">Where the leverage actually is</h2>

<p>L1 and L2 arrive on their own within weeks of buying licences, and most teams
land there without any deliberate effort. What still varies wildly inside those
levels is whether anyone states an exit condition before they start, which is
<a href="/blog/vibe-coding-vs-a-defined-workflow/">the difference between a good day and a bad one</a>
and is worth fixing before anybody argues about levels. L5 is a useful horizon
and a poor objective. The distance that pays is L2 to L3.5, and it is not a
tooling project.</p>

<p>It is a trust project, and trust here has a measurable form. Every level above L2
is a claim that something automated is a good enough substitute for a person’s
attention on a class of change. That claim is either supported by evidence about
your checks, or it is a hope with a dashboard.</p>

<h2 id="the-two-minute-version">The two-minute version</h2>

<p>Pick a change that merged last week. Ask who read it, what they read, and what
they would have caught. Then ask what would have caught it if nobody had.</p>

<p>If the answer to the second question is silence, your level is one lower than the
one on your slide, and the work is not to move up. It is to earn the level you
are already behaving as though you have.</p>

<hr />

<p><em>If you need a truthful placement for each of your teams rather than the one on
the slide, <a href="/services/training/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="ai-assistants" /><category term="workflow" /><category term="maturity" /><category term="review" /><summary type="html"><![CDATA[You think your team is a level higher than it is. The test is not which tools you bought. It is what you stopped reading, and whether anything replaced you.]]></summary></entry><entry><title type="html">Repo discoverability for humans and agents</title><link href="https://marcosbarbero.com/blog/repo-discoverability-for-humans-and-agents/" rel="alternate" type="text/html" title="Repo discoverability for humans and agents" /><published>2026-05-13T00:00:00+00:00</published><updated>2026-05-13T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/repo-discoverability-for-humans-and-agents</id><content type="html" xml:base="https://marcosbarbero.com/blog/repo-discoverability-for-humans-and-agents/"><![CDATA[<p>There is a scripts directory in your repo with eleven files in it. You wrote
four. Nobody has opened the other seven this year, and last month somebody wrote
a ninth thing that does what the third one already did, slightly differently,
because looking took longer than writing.</p>

<p>The hour is not the loss. The loss is that the rule now has two implementations,
they disagree in one edge case, and the disagreement will surface as a bug report
eight months from now that nobody can reproduce.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="find-title find-desc">
      <title id="find-title">The same rule with and without an index</title>
      <desc id="find-desc">On the left, three people look for an encoded rule that is not in
        any index. Two of them find it; the third does not and writes a second version, so the
        repository ends up with two implementations that disagree. On the right, an index sends
        all three to the same one.</desc>

      <text class="figure__note figure__note--strong" x="210" y="56" text-anchor="middle">not in the index</text>
      <text class="figure__note figure__note--strong" x="654" y="56" text-anchor="middle">in the index</text>

      <circle class="figure__dot figure__dot--b" cx="90" cy="110" r="6" />
      <circle class="figure__dot figure__dot--b" cx="90" cy="170" r="6" />
      <circle class="figure__dot figure__dot--b" cx="90" cy="230" r="6" />
      <path class="figure__line figure__line--b" d="M102 110 L 288 128" />
      <path class="figure__line figure__line--b" d="M102 170 L 288 134" />
      <path class="figure__line figure__line--b" d="M102 230 L 288 222" />
      <circle class="figure__dot figure__dot--b" cx="300" cy="130" r="7" />
      <text class="figure__note" x="318" y="126">the script</text>
      <circle class="figure__dot figure__dot--b" cx="300" cy="224" r="7" />
      <text class="figure__note figure__note--strong" x="318" y="220">a second one</text>
      <text class="figure__tick" x="210" y="272" text-anchor="middle">two versions, quietly disagreeing</text>

      <circle class="figure__dot figure__dot--a" cx="534" cy="110" r="6" />
      <circle class="figure__dot figure__dot--a" cx="534" cy="170" r="6" />
      <circle class="figure__dot figure__dot--a" cx="534" cy="230" r="6" />
      <path class="figure__line figure__line--a" d="M546 110 L 732 166" />
      <path class="figure__line figure__line--a" d="M546 170 L 732 170" />
      <path class="figure__line figure__line--a" d="M546 230 L 732 174" />
      <circle class="figure__dot figure__dot--a" cx="744" cy="170" r="7" />
      <text class="figure__note" x="762" y="166">the script</text>
      <text class="figure__tick" x="654" y="272" text-anchor="middle">one version, everyone on it</text>
    </svg>
  </div>
  <figcaption>
    <strong>Nobody rebuilds a thing on purpose.</strong> They rebuild it because
    looking took longer than writing, which is a statement about the index rather
    than about them.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="encoding-is-half-the-job">Encoding is half the job</h2>

<p>A rule you have written down and encoded is not yet a rule your team follows. It
is a rule <em>you</em> follow, and a file the next person has to stumble over.</p>

<p>Everything in this series so far has been about the first half: reason once,
encode, replay. <a href="/blog/agentic-workflows-from-runbook-to-command/">Turning a runbook into a command</a>
only pays if the second person knows the command exists, and that turns out to be
a separate problem with a separate fix.</p>

<p>Three things make an encoded rule survive contact with a second person, and only
the third one is usually missing:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>what it means</th>
      <th>fails when</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>a name</td>
      <td>says what it does, not how it does it</td>
      <td>it is called <code class="language-plaintext highlighter-rouge">utils</code></td>
    </tr>
    <tr>
      <td>a location</td>
      <td>the first place someone would look</td>
      <td>it lives where its author happened to be</td>
    </tr>
    <tr>
      <td>an index</td>
      <td>one file that names everything</td>
      <td>it is a wiki page from 2023</td>
    </tr>
  </tbody>
</table>

<h2 id="the-second-reader-changed-the-economics">The second reader changed the economics</h2>

<p>This used to be a politeness problem. A colleague who cannot find your script
loses an afternoon, once, and you feel vaguely bad about it.</p>

<p>An assistant that cannot find your script rediscovers the repository from
scratch, on every task, forever. It gets there, you paid for it, and you will pay
again next session, because nothing it worked out was written down anywhere.</p>

<p>That is the shift worth internalising. Discoverability stopped being courtesy and
became a per-task cost with a recurring bill.</p>

<h2 id="four-registries-four-questions">Four registries, four questions</h2>

<p><a href="https://github.com/marcosbarbero/deskspace">Deskspace</a>, a Spring Boot and React
service I keep as a reference implementation, answers each of them with a file
and a lookup tool rather than with a document somebody is supposed to read:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>toolbox/tool_mapping.py list         <span class="c"># what tools exist, by key</span>
toolbox/arch_map.py get booking      <span class="c"># where the code lives, and what may depend on it</span>
toolbox/lexicon.py search <span class="s2">"checkstyle"</span>  <span class="c"># how we solved this before</span>
toolbox/check_readmes.py             <span class="c"># does every directory explain itself</span>
</code></pre></div></div>

<p>The architecture map is the clearest case. A package structure is a fact about
the repository, so it belongs in a file rather than in a model’s reasoning. Ask
an assistant to add a validation rule with no map and it opens half a dozen files
to infer a layout that has not changed since the repo was created, then does it
again next session. Ask with a map and it is told.</p>

<p>The lexicon is the only one of the four that makes the repository better over
time rather than merely keeping it green. Gates stop bad things escaping; a
record of how a recurring problem was solved stops the same afternoon being spent
twice. Every entry in that one cost somebody real time: that a bare
<code class="language-plaintext highlighter-rouge">checkstyle:check</code> silently runs Sun’s ruleset, that one code generator breaks
against the current TypeScript, that two adapters onto the same upstream collide
on a bean name at context startup rather than at compile time.</p>

<h2 id="an-index-is-load-bearing-so-it-drifts">An index is load-bearing, so it drifts</h2>

<p>The index is the piece that keeps working after everyone who built the thing has
moved on, which is exactly why it decays unnoticed. Nothing breaks when an index
goes stale. The build passes. The tests pass. The only symptom is somebody
rebuilding something, months later, in another directory.</p>

<p>So the registry is checked rather than trusted. That repository fails its own
build when a tool exists on disk without being registered, and when the
architecture map disagrees with the actual package tree:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ toolbox/tool_mapping.py check
  FAIL  coverage_gaps.py exists but is not registered in mapping.json:
        an unregistered tool is an undiscoverable tool

$ toolbox/arch_map.py check
  FAIL  map lists slice 'billing', no such package in src/main/java
</code></pre></div></div>

<p>Both directions fail differently. Something on disk missing from the index gets
rebuilt. Something in the index missing from disk is worse: it sends the next
reader, or the next agent, confidently to a place that does not exist, which is
more expensive than having no map at all.</p>

<h2 id="the-way-this-check-fails-is-the-point">The way this check fails is the point</h2>

<p>Moving those scripts into one directory broke the checker in the most
instructive way available. It kept globbing the old path, and globbing a
directory that no longer exists yields nothing, so nothing was ever unregistered,
so the check reported ok.</p>

<p>Forever. The build was green and the index was unenforced, and there is no
symptom for that.</p>

<p>What caught it was not the check. It was a test asserting that the check
<strong>fails</strong> when a tool is unregistered. The happy-path test still passed and told
us nothing, which is the general shape: a gate with only a happy-path test proves
that it runs, not that it gates.</p>

<h2 id="write-the-index-for-both-readers">Write the index for both readers</h2>

<p>There is no version of this that serves a person onboarding and not an assistant
starting a task. Both arrive with no context, both need to know what already
exists before building anything, and both give up in the same way when the answer
is buried.</p>

<p>That is convenient rather than profound, but it settles an argument that used to
take a while: there is no separate machine-readable index to maintain. There is
the index, and it is checked, and the human-facing README is generated from it so
the two cannot disagree.</p>

<h2 id="the-test">The test</h2>

<p>Delete the person who wrote it. Can the next one find it in under a minute,
without asking anybody, starting from the file they were already going to open?</p>

<p>If the answer is no, the thing is not encoded yet. It is stored, which is not the
same, and storage without retrieval is how a team ends up paying to keep a
decision it no longer benefits from.</p>

<hr />

<p><em>If your team keeps solving the same problem in two directories because nobody
could find the first solution, <a href="/services/architecture/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="ai-assistants" /><category term="workflow" /><category term="determinism" /><category term="tooling" /><summary type="html"><![CDATA[Your repo has a scripts directory nobody opens, and somebody rebuilt one of the files in it last month. An agent does the same thing, on every task, and bills you for the rediscovery.]]></summary></entry><entry><title type="html">Write the architecture rule before you trust the architecture</title><link href="https://marcosbarbero.com/blog/write-the-rule-before-you-trust-the-architecture/" rel="alternate" type="text/html" title="Write the architecture rule before you trust the architecture" /><published>2026-05-01T00:00:00+00:00</published><updated>2026-05-01T00:00:00+00:00</updated><id>https://marcosbarbero.com/blog/write-the-rule-before-you-trust-the-architecture</id><content type="html" xml:base="https://marcosbarbero.com/blog/write-the-rule-before-you-trust-the-architecture/"><![CDATA[<p>Your team agreed on the boundaries eighteen months ago, drew them, and wrote them
down. Everybody can recite them. Ask three people whether a particular import
crosses one and you will get two answers and a pause.</p>

<p>That is not a discipline problem. A boundary described in a document is checked
by whoever remembers it at the moment they are in a hurry, which is the same as
not being checked, and the drift is invisible because nothing goes red.</p>

<figure class="figure">
  <div class="figure__frame">
    <svg viewBox="0 0 864 300" role="img" aria-labelledby="bnd-title bnd-desc">
      <title id="bnd-title">A boundary written down and a boundary asserted</title>
      <desc id="bnd-desc">On the left a dashed boundary described in a document, with three
        dependencies crossing it unimpeded. On the right the same boundary as a test: two
        dependencies cross at declared points and the third is refused.</desc>

      <text class="figure__note figure__note--strong" x="200" y="52" text-anchor="middle">written in a document</text>
      <line class="figure__rule" x1="200" y1="72" x2="200" y2="238" stroke-dasharray="6 6" />
      <line class="figure__line figure__line--b" x1="70" y1="104" x2="330" y2="104" />
      <line class="figure__line figure__line--b" x1="70" y1="156" x2="330" y2="156" />
      <line class="figure__line figure__line--b" x1="70" y1="208" x2="330" y2="208" />
      <text class="figure__tick" x="200" y="266" text-anchor="middle">everything crosses</text>

      <text class="figure__note figure__note--strong" x="640" y="52" text-anchor="middle">asserted by a test</text>
      <line class="figure__rule" x1="640" y1="72" x2="640" y2="238" />
      <line class="figure__line figure__line--a" x1="510" y1="104" x2="770" y2="104" />
      <line class="figure__line figure__line--a" x1="510" y1="156" x2="770" y2="156" />
      <line class="figure__line figure__line--b" x1="510" y1="208" x2="626" y2="208" />
      <circle class="figure__dot figure__dot--b" cx="640" cy="208" r="7" />
      <text class="figure__tick" x="640" y="266" text-anchor="middle">one of these is now a build failure</text>
    </svg>
  </div>
  <figcaption>
    <strong>A boundary you cannot assert is a boundary you do not have.</strong>
    The first run of a rule tells you which one you had.
    <span class="figure__disclaimer">Illustrative &mdash; the shape of the argument, not measured data.</span>
  </figcaption>
</figure>

<h2 id="what-a-rule-that-runs-actually-is">What a rule that runs actually is</h2>

<p>An architecture test is a few sentences about packages, executed. The value is
not the sentences, which you already had. It is that they now have a truth value.</p>

<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@ArchTest</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">ArchRule</span> <span class="n">availability_knows_booking_only_through_its_events</span> <span class="o">=</span>
    <span class="n">noClasses</span><span class="o">().</span><span class="na">that</span><span class="o">().</span><span class="na">resideInAPackage</span><span class="o">(</span><span class="no">AVAILABILITY</span><span class="o">)</span>
        <span class="o">.</span><span class="na">should</span><span class="o">().</span><span class="na">dependOnClassesThat</span><span class="o">(</span>
            <span class="n">resideInAPackage</span><span class="o">(</span><span class="no">BOOKING</span><span class="o">).</span><span class="na">and</span><span class="o">(</span><span class="n">resideOutsideOfPackage</span><span class="o">(</span><span class="no">PUBLISHED_EVENTS</span><span class="o">)))</span>
        <span class="o">.</span><span class="na">because</span><span class="o">(</span><span class="s">"events are the published language between these two; a call in "</span>
               <span class="o">+</span> <span class="s">"either direction would make them one deployable forever"</span><span class="o">);</span>
</code></pre></div></div>

<p>Note the <code class="language-plaintext highlighter-rouge">because</code>. When that fails at two in the morning, the reason it exists is
in the failure message, not in an ADR somebody has to go and find.</p>

<h2 id="the-first-run-is-the-point">The first run is the point</h2>

<p>I wrote eleven rules for a service I had built the week before, to boundaries I
had chosen and believed in. Four failed immediately, and every one was a design
problem rather than a rule problem.</p>

<p><strong>A shared type nobody meant to share.</strong> One slice’s outbound port returned
another slice’s domain object. It reads as a saved class and it is a shared
kernel: two supposedly independent slices now recompile together and neither
owner knows. Fixed by giving the consumer its own small type and mapping in the
adapter.</p>

<p><strong>A package name that made the rule lie.</strong> A directory named <code class="language-plaintext highlighter-rouge">desk</code> inside the
booking slice matched a rule about the <code class="language-plaintext highlighter-rouge">desk</code> slice, so the rule was quietly
answering a different question than the one I asked. Renaming the package fixed
the rule.</p>

<p><strong>A real cycle, through the one place nobody watches.</strong> A single error-handling
class imported every slice’s exceptions, and the shared package it lived in was
imported by all of them. Each slice translates its own failures now.</p>

<p><strong>A collision only a container notices.</strong> Two adapters onto the same upstream
picked the same bean name and the context failed to start. That one is not
architecture, and I only found it because these rules pushed me to boot the thing.</p>

<p>Four for eleven, on code written by somebody who was paying attention. That ratio
is the argument.</p>

<h2 id="write-the-rule-expecting-it-to-fail">Write the rule expecting it to fail</h2>

<p>The practical advice is a small change in stance. Do not write a rule to confirm
what you believe. Write it to find out, and read the first red carefully, because
it is usually right and you are usually the thing that has to change.</p>

<p>Two failure modes to avoid while doing that:</p>

<ul>
  <li><strong>Do not weaken the rule to make it pass.</strong> If a violation is genuinely
acceptable, that is an exception with a name and a reason, not a looser
predicate. A rule with a hole nobody can see is worse than no rule.</li>
  <li><strong>Do not assert what you cannot state.</strong> I tried to say “availability may
depend on booking’s events and nothing else” and first wrote a rule that meant
something different and passed. A rule that passes for the wrong reason is the
most expensive kind, and it is the same failure as
<a href="/blog/fail-fast-tools-that-refuse-ambiguous-input/">an assertion that tests existence when the invariant is uniqueness</a>:
green, and about the wrong thing.</li>
</ul>

<h2 id="where-to-start-on-monday">Where to start on Monday</h2>

<p>Pick the boundary your team argues about most and write one rule for it. Not a
suite, one rule, with a <code class="language-plaintext highlighter-rouge">because</code> naming the consequence you are avoiding.</p>

<p>If it passes, you have learned something cheap and you have a rule that will keep
being true. If it fails, you have learned something that was going to cost a lot
more later, and you found it on a Monday instead of during an incident.</p>

<hr />

<p><em>If your architecture is agreed in a document and contradicted in the code,
<a href="/services/architecture/">that’s the work I do</a>.</em></p>]]></content><author><name>Marcos Barbero</name></author><category term="architecture" /><category term="testing" /><category term="correctness" /><category term="review" /><summary type="html"><![CDATA[Your architecture diagram says the boundaries hold. The interesting question is what happens the first time you assert them against code you believed was clean.]]></summary></entry></feed>