<?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: Jonas Gauffin</title>
    <description>The latest articles on DEV Community by Jonas Gauffin (@jgauffin).</description>
    <link>https://dev.to/jgauffin</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%2F1108%2F7d88381e-7a75-4f83-8211-9c3e73455a4f.png</url>
      <title>DEV Community: Jonas Gauffin</title>
      <link>https://dev.to/jgauffin</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jgauffin"/>
    <language>en</language>
    <item>
      <title>How do you know the agent isn't quietly rotting your codebase?</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Sat, 03 Oct 2026 21:38:18 +0000</pubDate>
      <link>https://dev.to/jgauffin/how-do-you-know-the-agent-isnt-quietly-rotting-your-codebase-o1e</link>
      <guid>https://dev.to/jgauffin/how-do-you-know-the-agent-isnt-quietly-rotting-your-codebase-o1e</guid>
      <description>&lt;p&gt;The question is not whether coding agents write working code. They do. The question is what a year of it leaves behind.&lt;/p&gt;

&lt;p&gt;The decay is quiet because every individual change looks fine. The feature works, the tests are green, the diff was reviewed by somebody who had four other things to do. What accumulates underneath is harder to see: rules nobody agreed to, tests that prove nothing, and files that grew past the point where anyone can hold them in their head.&lt;/p&gt;

&lt;p&gt;Four things in Kiwipow Agent are aimed at exactly that, and each one produces evidence you can look at instead of a promise you have to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The intent exists outside the code
&lt;/h2&gt;

&lt;p&gt;Decay starts when the code becomes the only record of what the product does. From then on, every change is planned against the current behaviour, including the parts of it nobody chose.&lt;/p&gt;

&lt;p&gt;So the feature planner in the agent cannot read the code. It plans from your docs and from the specs of features planned before, and writes the feature as named rules in the product's language. The code gets its say in a separate pass afterwards, which reports only where it and the spec disagree, and each disagreement is ruled on by a person.&lt;/p&gt;

&lt;p&gt;The artefact is a file per feature, in plain language, that a product manager can read and argue with. Not generated from the code, and therefore able to contradict it. &lt;/p&gt;

&lt;p&gt;That ability to contradict is the whole point.&lt;/p&gt;

&lt;p&gt;So you get to plan next quarter from what the product is meant to do rather than from what it happens to do, and a new developer learns the promises in a morning of reading instead of a month of archaeology.&lt;/p&gt;

&lt;h2&gt;
  
  
  Every later change is checked against those rules
&lt;/h2&gt;

&lt;p&gt;A spec that is written once and then ignored is decoration. The specs here are read again by every session that changes code: a chat, a planned change, an implementation run. Each one checks the rules covering the area it is about to touch and asks before breaking one. When you agree, the rule is amended in its spec, so the written record moves with the product instead of falling behind it.&lt;/p&gt;

&lt;p&gt;Which means a decision stays decided. You hear about a conflict while it is still a question in a chat window, not a year later when a customer finds behaviour nobody chose to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tests come from the rules, not from the code
&lt;/h2&gt;

&lt;p&gt;This is the one I see underrated most often.&lt;/p&gt;

&lt;p&gt;A test written from the implementation asserts what the code already does. That is why agent-written suites pass the moment they are written and break the moment anyone refactors. They are snapshots. They catch nothing, and they cost a fortune to maintain, so eventually somebody deletes or regenerates them, and the safety net is gone without a decision ever being made.&lt;/p&gt;

&lt;p&gt;A test written from a rule asserts the promise. &lt;code&gt;Refund on cancel&lt;/code&gt; fails when cancelling stops refunding, not when a service is split in two. The suite survives the restructuring you will do later, and a red test names the promise that broke rather than the line that changed.&lt;/p&gt;

&lt;p&gt;The bookkeeping makes it checkable. The task board is derived from the approved spec rather than written by a model, so every rule is delivered by some task. A task is only finished by naming, per rule, the test that proves it. The spec view shows each rule with its task and its test, or shows the gap. Then the extension runs your test commands with no model involved, over the projects the feature touched, and failures go back for a bounded number of attempts before reaching a person.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftutvxar4sdg0zryfruei.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftutvxar4sdg0zryfruei.png" alt="Tasks marked as tested" width="765" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That buys back the thing a suite is supposed to give you: freedom to restructure. Green means the promises still hold after the move, red names the promise that broke, and nobody has to rewrite a hundred tests because a class got split in two.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality is improved continuously
&lt;/h2&gt;

&lt;p&gt;Passing tests say nothing about whether the implementation left behind a function nobody can follow. So when a feature's tests pass, the files its implementation runs edited are measured: a function against a cognitive complexity limit and a line limit, a type and a file against line limits. Your numbers, in settings, and a limit of zero turns a measure off.&lt;/p&gt;

&lt;p&gt;Anything over a limit is listed, and a person chooses: split all of it, split some, later, or skip. If you split, the run works from the size report alone, and afterwards the files are measured again and the tests run again. One pass per feature, never a second. What is still too big is reported and left alone.&lt;/p&gt;

&lt;p&gt;The restraint is the point. It does not judge taste: no renaming, no opinions about your abstractions, no discovery that a file "could be cleaner". And it stops: one pass, against numbers you chose, on splits you approved. An agent told to improve code will improve it for as long as you pay, and the second pass over its own work is where it starts moving code sideways.&lt;/p&gt;

&lt;p&gt;Which makes the worst outcome a file that is still too big and says so. Not a weekend of unrequested churn through code that already worked, and not a quality step whose cost depends on how much the model felt like rewriting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to ask your own team
&lt;/h2&gt;

&lt;p&gt;Whatever tooling you use, the questions are the same, and they are answerable this afternoon.&lt;/p&gt;

&lt;p&gt;Where does the agent learn what the product is supposed to do? Point at a requirement from last quarter and ask for the test that proves it. Take a test that failed recently and ask which product promise it was defending. Ask what happens when a change contradicts something agreed six months ago, and who finds out.&lt;/p&gt;

&lt;p&gt;If the honest answer to any of them is "the code", you are already paying for this. Just not on a line item.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Kiwipow Agent is a VS Code extension, on the &lt;a href="https://marketplace.visualstudio.com/items?itemName=CoderrAB.kiwipow-agent" rel="noopener noreferrer"&gt;Marketplace&lt;/a&gt;; source and issues on &lt;a href="https://github.com/jgauffin/kiwi-code-agent" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>engineering</category>
      <category>cto</category>
      <category>softwarequality</category>
      <category>ai</category>
    </item>
    <item>
      <title>How an agent verifies a webpage it can never look at</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Wed, 30 Sep 2026 05:27:21 +0000</pubDate>
      <link>https://dev.to/jgauffin/how-an-agent-verifies-a-webpage-it-can-never-look-at-33mh</link>
      <guid>https://dev.to/jgauffin/how-an-agent-verifies-a-webpage-it-can-never-look-at-33mh</guid>
      <description>&lt;p&gt;Fifth in a series on using &lt;a href="https://www.npmjs.com/package/@relax.js/core" rel="noopener noreferrer"&gt;@relax.js/core&lt;/a&gt; with a coding agent. The previous piece was about seeing errors in a test. This one is about getting the component into a state where there is something to see.&lt;/p&gt;

&lt;p&gt;Everything here is from &lt;code&gt;@relax.js/core/testing&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  Why plain jsdom is not enough
&lt;/h2&gt;

&lt;p&gt;An agent that knows Web Components will write this and be puzzled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;connectedCallback&lt;/code&gt; only runs when the element is in a document. Created and never attached, it is inert, for reasons that have nothing to do with the component. And once attached, anything it &lt;code&gt;await&lt;/code&gt;s finishes a task later, so asserting right after attaching sees the element before its data arrived.&lt;/p&gt;

&lt;p&gt;Two helpers cover this. &lt;code&gt;mount()&lt;/code&gt; attaches to &lt;code&gt;document.body&lt;/code&gt; and hands back &lt;code&gt;unmount()&lt;/code&gt;, which is also how you test &lt;code&gt;disconnectedCallback&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stops_listening_once_removed_from_the_page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unmount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;unmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.display-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;flush()&lt;/code&gt; waits for pending microtasks and the next macrotask, so work started by a lifecycle callback has landed. You will see it after every user action in the page tests below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fake the server, not the component
&lt;/h2&gt;

&lt;p&gt;The page loads its data through &lt;code&gt;@relax.js/core/http&lt;/code&gt;. &lt;code&gt;fakeServer()&lt;/code&gt; replaces the network underneath that module with canned responses and records every request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fakeServer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;routing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mountRouting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;captureRelaxErrors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmount&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paths include the configured base URL and exclude the query string, because that is what a server sees. A request nothing was registered for gets a 404 whose body lists what is registered, and it is recorded like any other, so an unexpected call shows up in &lt;code&gt;server.requests&lt;/code&gt; instead of hanging or reaching the network.&lt;/p&gt;

&lt;p&gt;The seam is deliberate. Do not stub &lt;code&gt;fetch&lt;/code&gt; globally, and do not mock the component's own &lt;code&gt;load()&lt;/code&gt; method. The component under test is the real one, and the assertion covers both what it rendered and what it asked for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;fills_the_form_from_the_server_when_navigated_to&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProfilePage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="email"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alice@example.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET /api/users/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A function body is called with the request, which is how a &lt;code&gt;PUT&lt;/code&gt; answers with what it was sent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;submit_sends_the_edited_form_and_announces_the_save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProfilePage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="na"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;querySelector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLInputElement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;input[name="displayName"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;put&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;method&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;put&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;alice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;saved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice B&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note &lt;code&gt;requestSubmit()&lt;/code&gt;, not &lt;code&gt;submit()&lt;/code&gt;. The latter skips the &lt;code&gt;submit&lt;/code&gt; event, and the &lt;code&gt;FormValidator&lt;/code&gt; that owns the form never hears about it. When an agent picks the wrong one, the test says so: the &lt;code&gt;PUT&lt;/code&gt; is missing from &lt;code&gt;server.requests&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The failure path is one more &lt;code&gt;on()&lt;/code&gt; with a status:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a_rejected_save_lands_in_the_error_summary_instead_of_throwing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PUT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/users/42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nope&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;409&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;navigate&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;ProfilePage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;42&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;requestSubmit&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;flush&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.status&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;The server rejected the change (409)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;toEqual&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Registration order matters: the first match wins. That is why the &lt;code&gt;PUT&lt;/code&gt; is registered per test and not in &lt;code&gt;beforeEach&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Navigate like the app
&lt;/h2&gt;

&lt;p&gt;The page above is reached with &lt;code&gt;routing.navigate()&lt;/code&gt;, not &lt;code&gt;mount()&lt;/code&gt;. The difference is everything a page depends on: &lt;code&gt;loadRoute()&lt;/code&gt; runs with the route parameters before the element is added, the element lands inside &lt;code&gt;&amp;lt;r-route-target&amp;gt;&lt;/code&gt;, and &lt;code&gt;routeData&lt;/code&gt; is set. &lt;code&gt;mountRouting()&lt;/code&gt; registers the routes and puts the targets in the document; its &lt;code&gt;navigate()&lt;/code&gt; resolves with the rendered component once it is inside the target, after all of that. No &lt;code&gt;flush()&lt;/code&gt; needed for the load itself.&lt;/p&gt;

&lt;p&gt;It rejects the way &lt;code&gt;navigate()&lt;/code&gt; throws in the application: no route matched, or a guard stopped it. When the component never appears within the timeout, the rejection lists the errors reported meanwhile, so a &lt;code&gt;loadRoute()&lt;/code&gt; that threw is named instead of leaving you with an empty target.&lt;/p&gt;

&lt;p&gt;The routes it takes are the app's own:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/profile/:userId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;componentTagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing this exposed while I was writing the example, and I mentioned it in the third article: the test must import the page module for its side effect. A type-only import is elided, &lt;code&gt;profile-page&lt;/code&gt; is never defined, and &lt;code&gt;mountRouting&lt;/code&gt; throws at once with the tag name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reset between tests
&lt;/h2&gt;

&lt;p&gt;Routing targets, the error handler and the fetch replacement are module-level state. Every helper returns the means to undo itself: &lt;code&gt;unmount()&lt;/code&gt;, &lt;code&gt;restore()&lt;/code&gt;. Call them in &lt;code&gt;afterEach&lt;/code&gt; or a &lt;code&gt;finally&lt;/code&gt;, so one test's leftovers cannot explain the next test's failure. The &lt;code&gt;afterEach&lt;/code&gt; above is the whole ritual.&lt;/p&gt;

&lt;h2&gt;
  
  
  The page test
&lt;/h2&gt;

&lt;p&gt;Put together: &lt;code&gt;fakeServer()&lt;/code&gt; for the data, &lt;code&gt;mountRouting()&lt;/code&gt; to reach the page, &lt;code&gt;captureRelaxErrors()&lt;/code&gt; to prove the template resolved, &lt;code&gt;flush()&lt;/code&gt; after each user action. Four tests cover the example page end to end and finish in milliseconds. That is the loop the agent runs after every edit, and it is the only loop it has.&lt;/p&gt;

&lt;p&gt;It still relies on the template being rendered with the right model before a typo shows up. The next article removes that dependency.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>Stop counting duplicated lines: I rank copy-paste by what it costs me</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Tue, 29 Sep 2026 21:33:55 +0000</pubDate>
      <link>https://dev.to/jgauffin/stop-counting-duplicated-lines-i-rank-copy-paste-by-what-it-costs-me-3mp4</link>
      <guid>https://dev.to/jgauffin/stop-counting-duplicated-lines-i-rank-copy-paste-by-what-it-costs-me-3mp4</guid>
      <description>&lt;p&gt;Every CI pipeline I have worked with had a duplication report. I cannot remember the last time I read one.&lt;/p&gt;

&lt;p&gt;The number moves from 4.2% to 4.5%, a list of blocks shows up, and half of them are constructors, guard clauses and property mappings that look alike because that is how the language is written. Meanwhile the copy that actually hurt me never made the list: someone (oops, me) copied a function, renamed three variables, and a bug fix later landed in one copy but not the other. &lt;/p&gt;

&lt;p&gt;We blame tight deadlines, but can't blame StackOverflow anymore, so maybe it's time to clean the code bases.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;dry-mcp&lt;/strong&gt;, an MCP server that finds duplicated code by meaning instead of by token, ranks it by what it costs to keep, and gives the result to my AI agent instead of a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SonarQube actually measures
&lt;/h2&gt;

&lt;p&gt;SonarQube is good at what it is built for, so let's be precise about what that is. According to &lt;a href="https://docs.sonarsource.com/sonarqube-server/10.4/user-guide/metric-definitions" rel="noopener noreferrer"&gt;its documentation&lt;/a&gt;, a block counts as duplicated when there are at least 100 successive duplicated tokens spread over at least 10 lines (Java uses 10 successive statements instead). Differences in indentation and string literals are ignored.&lt;/p&gt;

&lt;p&gt;Two consequences follow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Renamed copies fall through.&lt;/strong&gt; Indentation and literals are ignored, identifiers are not. Rename a variable every few lines and the run of 100 identical tokens is broken.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every duplicated line weighs the same.&lt;/strong&gt; The headline metric is a percentage. It tells you &lt;em&gt;how much&lt;/em&gt; duplication there is, not &lt;em&gt;which&lt;/em&gt; duplication to fix first.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the right design for a quality gate ("new code may not exceed X% duplication"). It is the wrong design for the question I actually have: &lt;strong&gt;where is the duplication that is worth an afternoon?&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Match by meaning, not by token
&lt;/h2&gt;

&lt;p&gt;Matching runs in two passes.&lt;/p&gt;

&lt;p&gt;First, every block is normalized (formatting and comments stripped) and hashed. Identical hashes are exact copies. That is free and never wrong.&lt;/p&gt;

&lt;p&gt;What is left is compared using embeddings from &lt;a href="https://huggingface.co/jinaai/jina-embeddings-v2-base-code" rel="noopener noreferrer"&gt;&lt;code&gt;jina-embeddings-v2-base-code&lt;/code&gt;&lt;/a&gt;, a model trained on code. Two blocks that do the same thing land close together even when every name differs. Measured against that model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pair of blocks&lt;/th&gt;
&lt;th&gt;Similarity&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Same logic, every name changed&lt;/td&gt;
&lt;td&gt;~0.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same logic, different language&lt;/td&gt;
&lt;td&gt;~0.87&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unrelated code&lt;/td&gt;
&lt;td&gt;≤ 0.22&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Yes, the second row means a helper that was ported to another language is still recognized as the same code. I did not set out to build that, but it falls out of matching by meaning.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rank by cost, not by count
&lt;/h2&gt;

&lt;p&gt;A 60-line block copied four times is a real problem: every change has to be made four times, and one will be forgotten. A 3-line fragment repeated forty times is almost always an idiom.&lt;/p&gt;

&lt;p&gt;So the ranking counts block size for more than the number of copies. Want the most-copied code instead? Ask for &lt;code&gt;orderBy: "frequency"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demote idioms, don't report them
&lt;/h2&gt;

&lt;p&gt;Repetition that is just how the language is written gets demoted instead of ranked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Small and frequent.&lt;/strong&gt; Short blocks that show up everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Spread thinly.&lt;/strong&gt; The same shape once in each of a dozen unrelated folders is house style, not one copy-paste incident.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contained in a larger finding.&lt;/strong&gt; Copying a function also copies the loop inside it. Only the outermost block is reported, so the same work is not counted twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configured exclusions.&lt;/strong&gt; Paths and patterns the team has decided to accept.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing is thrown away. &lt;code&gt;includeSuppressed: true&lt;/code&gt; returns the demoted groups with the reason for each, so the rules can be checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for an agent, not a dashboard
&lt;/h2&gt;

&lt;p&gt;Devs are lazy, including me. I don't want to read a duplication report. I want my agent to find the worst copy and fix it. That changes what the output needs to carry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence on every finding.&lt;/strong&gt; Near-miss matching is deliberately inclusive, because missing a large repeated block is worse than flagging a coincidence. So instead of silently filtering, each finding is marked &lt;code&gt;certain&lt;/code&gt;, &lt;code&gt;high&lt;/code&gt;, &lt;code&gt;moderate&lt;/code&gt; or &lt;code&gt;low&lt;/code&gt;, and the reply explains the scale. The agent knows what it can act on and what it has to read first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Honesty about completeness.&lt;/strong&gt; Indexing runs in the background (more on that below). Any reply built from an incomplete index says so, with numbers. A partial answer is never dressed up as a full one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-service scope.&lt;/strong&gt; Without configuration, every source file under the root is analysed. When that looks too wide, the reply says so, names the largest folders, and tells the agent what to write in &lt;code&gt;duplication.config.json&lt;/code&gt;. The agent edits the file, and the next question uses the new scope. No restart.&lt;/p&gt;

&lt;p&gt;The whole surface is four tools:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;detect_duplication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Where is the duplication, worst first?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;duplication_status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Is the index ready?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;explain_duplication&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show me every copy.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reindex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start over.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A trimmed finding looks like this:&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;duplications&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;622069fe1577&lt;/span&gt;
   &lt;span class="na"&gt;occurrences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/analysis/clusterer.ts&lt;/span&gt;
      &lt;span class="na"&gt;startLine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;58&lt;/span&gt;
      &lt;span class="na"&gt;endLine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;177&lt;/span&gt;
      &lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;78&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;src/analysis/duplication-service.ts&lt;/span&gt;
      &lt;span class="na"&gt;startLine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;322&lt;/span&gt;
      &lt;span class="na"&gt;endLine&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;441&lt;/span&gt;
      &lt;span class="na"&gt;lines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;81&lt;/span&gt;
    &lt;span class="c1"&gt;# ...three more&lt;/span&gt;
   &lt;span class="na"&gt;frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
   &lt;span class="na"&gt;medianLines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;81&lt;/span&gt;
   &lt;span class="na"&gt;removableLines&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;324&lt;/span&gt;
   &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1692.69&lt;/span&gt;
   &lt;span class="na"&gt;similarity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.763&lt;/span&gt;
   &lt;span class="na"&gt;confidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;clustersFound&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;98&lt;/span&gt;
 &lt;span class="na"&gt;byConfidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;certain&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;
  &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;50&lt;/span&gt;
  &lt;span class="na"&gt;moderate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;44&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent picks the top finding, calls &lt;code&gt;explain_duplication&lt;/code&gt; with its id to get the source of every copy, and decides how to merge them.&lt;/p&gt;

&lt;h2&gt;
  
  
  No parser, any language
&lt;/h2&gt;

&lt;p&gt;There is no grammar to install. Block boundaries are inferred from braces and indentation, so C#, TypeScript, Java, Go, Rust, Python, Ruby, PHP, SQL, shell, CSS and friends all work out of the box.&lt;/p&gt;

&lt;p&gt;The trade-off: line ranges are approximate. The returned source is authoritative, the numbers around it are a pointer. For an agent, that's not a problem since it will scan all and decide what to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch, part 1: it needs a model
&lt;/h2&gt;

&lt;p&gt;The embedding model is not bundled. Even the smallest weights are around 160 MB, which is too much to push through &lt;code&gt;npm install&lt;/code&gt;, and a download that arrives unannounced in the middle of a question is worse than being told once to run a command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/index.js download-model          &lt;span class="c"&gt;# int8, ~160 MB&lt;/span&gt;
node dist/index.js download-model &lt;span class="nt"&gt;--fp32&lt;/span&gt;   &lt;span class="c"&gt;# ~640 MB, for accuracy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;int8 is the default because it is the precision CPUs actually accelerate. x86 cores without AVX512-FP16 have no native fp16 compute, so an fp16 model often runs &lt;em&gt;slower&lt;/em&gt; than fp32, while int8 uses the VNNI instructions directly.&lt;/p&gt;

&lt;p&gt;Without the model the server still starts. Queries return an empty result with the command to run, not an error.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch, part 2: the first sync is slow
&lt;/h2&gt;

&lt;p&gt;Everything runs locally on the CPU. No API key, no code leaving the machine. The price is that embedding a whole project takes minutes, not seconds.&lt;/p&gt;

&lt;p&gt;It is slower still by design. I typically have several projects open, each with its own server, on the same machine I am compiling on. Left alone, embedding would take every core it can get. So background indexing runs at a &lt;strong&gt;20% duty cycle&lt;/strong&gt;: it works in short slices and rests in between. All servers together cost less than one core, and a project still finishes within an editor session.&lt;/p&gt;

&lt;p&gt;While indexing is in progress, replies carry a progress block:&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;progress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
 &lt;span class="na"&gt;filesInScope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3510&lt;/span&gt;
 &lt;span class="na"&gt;filesEmbedded&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1204&lt;/span&gt;
 &lt;span class="na"&gt;pendingFiles&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2306&lt;/span&gt;
 &lt;span class="na"&gt;percentComplete&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;34&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For projects larger than a couple of hundred files, you get &lt;em&gt;only&lt;/em&gt; that block until half the project is embedded. A ranking drawn from a third of a codebase is not an early version of the real ranking. The worst duplication is most likely in the part not read yet, while the reply would look like an answer and invite acting on it.&lt;/p&gt;

&lt;p&gt;Two things make this bearable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scope first.&lt;/strong&gt; Put an &lt;code&gt;include&lt;/code&gt; list in &lt;code&gt;duplication.config.json&lt;/code&gt; before the first run. Fewer files, faster sync, and less vendored code in the results anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only the first sync is slow.&lt;/strong&gt; Vectors are cached in SQLite keyed by &lt;em&gt;content&lt;/em&gt;, not location. Moving a block, re-indenting it or adding a comment reuses the stored vector, identical blocks in twenty files are embedded once, and after a branch switch only what actually changed gets embedded again.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Node 22.5 or later.&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://github.com/jgauffin/dry-mcp
&lt;span class="nb"&gt;cd &lt;/span&gt;duplication-mcp
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
node dist/index.js download-model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to &lt;code&gt;.mcp.json&lt;/code&gt; in your project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"duplication"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/path/to/duplication-mcp/dist/index.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Optionally scope it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"include"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"src/**"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lib/**"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exclude"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"**/*.generated.*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"**/migrations/**"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then ask your agent where the worst duplication is. If it tells you it is still indexing, that is the honest answer. Ask again in a few minutes.&lt;/p&gt;

&lt;p&gt;I'd love to hear what it finds in your codebase, and especially where it is wrong.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>codequality</category>
      <category>typescript</category>
    </item>
    <item>
      <title>A blank page and a green test: the bug an agent can't see</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Fri, 25 Sep 2026 10:55:27 +0000</pubDate>
      <link>https://dev.to/jgauffin/a-blank-page-and-a-green-test-the-bug-an-agent-cant-see-3noo</link>
      <guid>https://dev.to/jgauffin/a-blank-page-and-a-green-test-the-bug-an-agent-cant-see-3noo</guid>
      <description>&lt;p&gt;Fourth in a series on using &lt;a href="https://www.npmjs.com/package/@relax.js/core" rel="noopener noreferrer"&gt;@relax.js/core&lt;/a&gt; with a coding agent. This is about the failures that make no noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blank element
&lt;/h2&gt;

&lt;p&gt;A template engine has to decide what to do with &lt;code&gt;{{user.naem}}&lt;/code&gt; when &lt;code&gt;user&lt;/code&gt; has no &lt;code&gt;naem&lt;/code&gt;. Throwing means one typo blanks the whole page, so like most engines this one renders an empty string and moves on. That is the right call for a user in a browser.&lt;/p&gt;

&lt;p&gt;It is the wrong default for an agent. The agent's only view of the page is a test. It writes the component, writes the test, runs it, and sees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;passes_on_a_blank_element_because_nobody_read_the_errors&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;{{user.naem}}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Green. The &lt;code&gt;&amp;lt;p&amp;gt;&lt;/code&gt; exists. It is empty, and nothing said why. This test is in the example app on purpose, as the thing not to write.&lt;/p&gt;

&lt;h2&gt;
  
  
  One channel
&lt;/h2&gt;

&lt;p&gt;Every failure the library detects goes through one function, &lt;code&gt;reportError()&lt;/code&gt;, which builds a &lt;code&gt;RelaxError&lt;/code&gt; with a message and a &lt;code&gt;context&lt;/code&gt; object and hands it to whatever handler &lt;code&gt;onError()&lt;/code&gt; registered. In the application that handler logs to your service or shows a toast. If nothing is registered, the error is still kept: &lt;code&gt;window.relaxErrors&lt;/code&gt; holds the last fifty, and the first unhandled one prints a single line to the console naming that array. Once per page load. It is a signpost, not noise.&lt;/p&gt;

&lt;p&gt;The design rule behind it, which the agent-facing docs state outright: diagnostics go in values, not in log lines. A human watches a console. An agent reads what a function returned or what a test printed. An error object with &lt;code&gt;{ expression, location }&lt;/code&gt; on it is something a test can assert on; twelve &lt;code&gt;console.log&lt;/code&gt; lines describing a render are not.&lt;/p&gt;

&lt;h2&gt;
  
  
  In a test
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;captureRelaxErrors()&lt;/code&gt; from &lt;code&gt;@relax.js/core/testing&lt;/code&gt; swaps in a handler that collects instead of throwing, and gives it back with &lt;code&gt;restore()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;CapturedErrors&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;beforeEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;captureRelaxErrors&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;afterEach&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;restore&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the same typo is a failing assertion, with the reason in the message:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a_mistyped_path_renders_empty_and_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;{{user.naem}}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Cannot resolve "user.naem"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The testing skill says to assert &lt;code&gt;captured.messages()&lt;/code&gt; is empty even in tests that are about something else, and every test in the example's page suite ends with that line. It is the cheapest assertion in the file and the one that catches the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failures that produced no DOM and no error
&lt;/h2&gt;

&lt;p&gt;Once the channel existed, I went looking for everything that used to fail without going through it. Version 1.8.0's changelog is the list. Four of them are in the example app as tests.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;render()&lt;/code&gt; compares the context by identity. Mutate the object and render it again and nothing changes, because from the engine's side nothing did:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rendering_the_same_object_twice_changes_nothing_and_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;p&amp;gt;{{count}}&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;render() was given the same context object&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent coming from Vue writes exactly this and expects reactivity to notice. The message tells it what to do instead: pass a new object, &lt;code&gt;render({ ...state })&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A handler needs parentheses. &lt;code&gt;r-click="save"&lt;/code&gt; binds nothing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a_handler_without_parentheses_is_not_bound_and_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compileTemplate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;button r-click="save"&amp;gt;Save&amp;lt;/button&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;save&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;r-click must be a function call, got "save"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;html&lt;/code&gt; tagged literal gives one instance per literal. Binding it twice re-drives the first one and returns an empty fragment, so the second card never appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;binding_an_html_template_twice_redrives_the_first_instance_and_reports&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="s2"&gt;`&amp;lt;p&amp;gt;{{name}}&amp;lt;/p&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Alice&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;card&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toHaveLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;p&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)?.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Bob&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;toContain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;This html template was already bound&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And a route pointing at a tag that was never defined fails at the moment the routes are defined, not later when someone navigates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a_route_whose_tag_was_never_defined_fails_when_routes_are_defined&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="nf"&gt;mountRouting&lt;/span&gt;&lt;span class="p"&gt;([{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/missing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;componentTagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-pgae&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}]),&lt;/span&gt;
    &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toThrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Component with tagName 'profile-pgae' is not defined in customElements.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one throws rather than reports, because at definition time there is no page to keep alive and failing fast is free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strict when you want it
&lt;/h2&gt;

&lt;p&gt;The template engine takes &lt;code&gt;{ strict: true }&lt;/code&gt;, and then every reported template error throws instead. I do not use it in application code, for the reason at the top: one typo should not blank the page for a user. In a test the capture is better than strict, because it collects everything instead of stopping at the first.&lt;/p&gt;

&lt;p&gt;The remaining question is the one that bothered me most. Everything above happens when the template renders. The agent still has to write the test that renders it, with the right model, and remember the capture. The sixth article is about catching the typo before anything renders at all. Before that, the rest of the test seam: how the agent gets a page on screen without a browser.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>testing</category>
      <category>ai</category>
    </item>
    <item>
      <title>No virtual DOM, nothing re-renders: the whole model in four rules</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:45:15 +0000</pubDate>
      <link>https://dev.to/jgauffin/the-model-in-one-page-5cpm</link>
      <guid>https://dev.to/jgauffin/the-model-in-one-page-5cpm</guid>
      <description>&lt;p&gt;Third in a series on using &lt;a href="https://www.npmjs.com/package/@relax.js/core" rel="noopener noreferrer"&gt;@relax.js/core&lt;/a&gt; with a coding agent. This one is the model the agent has to hold in its head. It is short on purpose. Every code block below is from a small example app that runs under vitest; nothing here is sketched.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A component is a plain custom element
&lt;/h2&gt;

&lt;p&gt;No base class. No decorator that registers it. &lt;code&gt;extends HTMLElement&lt;/code&gt;, the native lifecycle, &lt;code&gt;customElements.define&lt;/code&gt; at the bottom of the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileHeader&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;HTMLElement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;HTMLElement&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;connectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;header&amp;gt;&amp;lt;strong class="display-name"&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/header&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.display-name&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onProfileSaved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nf"&gt;disconnectedCallback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onProfileSaved&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nx"&gt;onProfileSaved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;customElements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-header&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ProfileHeader&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why it matters for an agent: the lifecycle is documented on MDN, which the agent has read more of than any framework's docs. And &lt;code&gt;customElements.define('profile-header', ...)&lt;/code&gt; is a literal string, so the route table, the test and the HTML that use &lt;code&gt;profile-header&lt;/code&gt; are all one grep away.&lt;/p&gt;

&lt;p&gt;One trap I hit while writing the example, and it is worth knowing: if a test file imports the class only as a type (&lt;code&gt;navigate&amp;lt;ProfilePage&amp;gt;(...)&lt;/code&gt;), the bundler elides the import, the module never runs, and the tag is never defined. Import the module for its side effect: &lt;code&gt;import '../src/pages/ProfilePage'&lt;/code&gt;. &lt;code&gt;defineRoutes&lt;/code&gt; fails fast with the tag name when this happens, which is how I noticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. The lifecycle is synchronous
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;connectedCallback&lt;/code&gt; returns before anything you &lt;code&gt;await&lt;/code&gt; in it has finished. Marking it &lt;code&gt;async&lt;/code&gt; compiles, and the browser ignores the promise. This is the first habit an agent brings from &lt;code&gt;ngOnInit&lt;/code&gt; and &lt;code&gt;onMounted&lt;/code&gt;, where the framework at least knows you started something.&lt;/p&gt;

&lt;p&gt;The pattern is: do the synchronous part, kick off the async part, and let the async part update the DOM when it lands. In the example app the profile page does its loading in &lt;code&gt;loadRoute&lt;/code&gt;, which the router does await, so it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;loadRoute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RouteParams&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;heading&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your profile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;discard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;statusLine&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fragment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;FormValidator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;FindForm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormValidator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;useSummary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;submitCallback&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything above the &lt;code&gt;await&lt;/code&gt; is on the page before the request goes out. A test that mounts the component and asserts immediately sees the empty form; one that waits sees the data. There is a helper for the waiting, in the fifth article.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nothing re-renders on its own
&lt;/h2&gt;

&lt;p&gt;There is no reactive state. When data changes, update the DOM at that point. The header above does it with &lt;code&gt;textContent&lt;/code&gt;. The page does it with a second, tiny template for the only part that changes after load:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readData&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Profile&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;validator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addErrorToSummary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Save&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`The server rejected the change (&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;statusCode&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;profile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The form itself is rendered once and never again, because every render writes &lt;code&gt;value&lt;/code&gt; back into the inputs and would replace what the user is typing. The native form is the state. &lt;code&gt;readData&lt;/code&gt; reads it, &lt;code&gt;setFormData&lt;/code&gt; writes it. There is no mirror of the field values anywhere in the class.&lt;/p&gt;

&lt;p&gt;This is the rule that costs the most when you come from Vue. It is also the one that makes the diff say what happens. An agent that adds a field to this page has to add the place where the field is updated, and a reviewer sees both in the same hunk.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Events are classes
&lt;/h2&gt;

&lt;p&gt;Components do not call each other. The page dispatches, the header listens, and neither imports the other. The thing they share is the event class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProfileSavedEvent&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;profile-saved&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="nx"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kr"&gt;declare&lt;/span&gt; &lt;span class="nb"&gt;global&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;HTMLElementEventMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;DocumentEventMap&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;ProfileSavedEvent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not &lt;code&gt;CustomEvent&lt;/code&gt; with a &lt;code&gt;detail&lt;/code&gt; bag. A class with properties, registered in the event map of whatever you listen on, so &lt;code&gt;addEventListener&lt;/code&gt; infers the type and &lt;code&gt;e.displayName&lt;/code&gt; is checked. I had to add &lt;code&gt;DocumentEventMap&lt;/code&gt; while writing this, because the header listens on &lt;code&gt;document&lt;/code&gt;; &lt;code&gt;HTMLElementEventMap&lt;/code&gt; alone covers elements only. The compiler told me, which is the point.&lt;/p&gt;

&lt;p&gt;Grep &lt;code&gt;ProfileSavedEvent&lt;/code&gt; and you have every producer and every consumer in the codebase. That is the whole "shared state" story for a small app, and it is the one the first article promised: the connection between two places is a literal name the agent can search for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is deliberately missing
&lt;/h2&gt;

&lt;p&gt;No store. No computed properties. No context or provide/inject. If a value is derived, compute it where the source changes and pass the result on. If two components far apart need the same data, the page that owns it places both, through slots, instead of threading it down. The library's &lt;code&gt;docs/WhyRelaxjs.md&lt;/code&gt; argues each of these at length; the skill just says "do not".&lt;/p&gt;

&lt;p&gt;Four rules. They fit in the core skill with room to spare, and the agent has them loaded before it writes a line. Next: what happens when the line it writes is wrong, and why nothing throws.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>webcomponents</category>
      <category>ai</category>
    </item>
    <item>
      <title>Skills, not docs</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Sun, 20 Sep 2026 09:03:09 +0000</pubDate>
      <link>https://dev.to/jgauffin/skills-not-docs-50b0</link>
      <guid>https://dev.to/jgauffin/skills-not-docs-50b0</guid>
      <description>&lt;p&gt;Second in a series on using &lt;a href="https://www.npmjs.com/package/@relax.js/core" rel="noopener noreferrer"&gt;@relax.js/core&lt;/a&gt; with a coding agent. The first piece made the argument; this one is about the first thing you do in a project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install the rules, not the manual
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx @relax.js/core init-agents
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That writes seven files into &lt;code&gt;.claude/skills/&lt;/code&gt;, one per area: the core model, then templates, forms, routing, services, testing and setup. Claude Code loads a skill when its description matches what the agent is doing. Other tools without skill support can be pointed at &lt;code&gt;node_modules/@relax.js/core/skills/relaxjs/SKILL.md&lt;/code&gt; from their instruction file; the core skill links to the rest.&lt;/p&gt;

&lt;p&gt;Each copy is stamped with the package version it came from. Run the command again after an upgrade and it lists the copies that are behind, and leaves them alone unless you pass &lt;code&gt;--force&lt;/code&gt;. A skill is a snapshot, and a snapshot that describes an older library is worse than none, because the agent trusts it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The border
&lt;/h2&gt;

&lt;p&gt;The interesting decision was not to write the skills. It was deciding what does not go in them.&lt;/p&gt;

&lt;p&gt;The library has a &lt;code&gt;docs/&lt;/code&gt; folder like any other. An agent could read it, and sometimes it should. But documentation is written for someone who already knows they need this API. It answers "how does this work, what is available". &lt;/p&gt;

&lt;p&gt;A skill loads before the agent knows it has a problem. Its job is to overwrite a wrong default and route to the right doc. The &lt;code&gt;skills/README.md&lt;/code&gt; in the package states two tests for where a sentence belongs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Would an agent that never read this produce code that compiles, type-checks and does nothing? Skill. Would it merely not know a name? Docs.&lt;/p&gt;

&lt;p&gt;Would the sentence need editing when the implementation changes? Docs. Only when the design changes? Skill.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The two must not overlap. A skill that accumulates examples is turning into a doc, and should hand off to one instead. The only thing a skill repeats from a doc is its filename.&lt;/p&gt;

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

&lt;p&gt;Here is the whole "Do not" section of the core skill:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Do not&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; Reach for a state store, computed properties or a reactive wrapper. Update the DOM where the
  change happens.
&lt;span class="p"&gt;-&lt;/span&gt; Add a component base class, a render loop or a diffing layer.
&lt;span class="p"&gt;-&lt;/span&gt; Use &lt;span class="sb"&gt;`CustomEvent`&lt;/span&gt;, or &lt;span class="sb"&gt;`enum`&lt;/span&gt; where a &lt;span class="sb"&gt;`declare type`&lt;/span&gt; string union works.
&lt;span class="p"&gt;-&lt;/span&gt; Duplicate native HTML. Use &lt;span class="sb"&gt;`&amp;lt;dialog&amp;gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&amp;lt;details&amp;gt;`&lt;/span&gt;, &lt;span class="sb"&gt;`&amp;lt;input type="date"&amp;gt;`&lt;/span&gt; and friends before
  writing a component.
&lt;span class="p"&gt;-&lt;/span&gt; Swallow errors. An empty &lt;span class="sb"&gt;`catch`&lt;/span&gt; is a bug.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every line is a habit. None of them is a fact about an API. An agent that never reads this will write a store, a base class and a &lt;code&gt;CustomEvent&lt;/code&gt;, and all three will compile.&lt;/p&gt;

&lt;p&gt;Compare the forms skill, which opens with the one thing agents get wrong most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## FormValidator owns the submit event&lt;/span&gt;

Its constructor attaches the listener. Do not add your own, and construct one even when you have
no validation rules, because taking over submit is what it is for. Supplying &lt;span class="sb"&gt;`submitCallback`&lt;/span&gt;
suppresses the native submit, so the page never navigates away.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then a diagnosis, because skills are also loaded when something is already broken:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;A form that still navigates away on submit means no &lt;span class="sb"&gt;`FormValidator`&lt;/span&gt; was constructed for it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And at the bottom, the hand-off:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Detail&lt;/span&gt;
&lt;span class="p"&gt;
-&lt;/span&gt; &lt;span class="sb"&gt;`@relax.js/core/docs/forms/form-page.md`&lt;/span&gt; for the end-to-end shape of an edit page. Start here
&lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="sb"&gt;`@relax.js/core/docs/forms/validation.md`&lt;/span&gt; for rules, the error summary and every option
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;init-agents&lt;/code&gt; rewrites those &lt;code&gt;@relax.js/core/docs/&lt;/code&gt; references to the real path of the installed package, so the agent can follow them without knowing where &lt;code&gt;node_modules&lt;/code&gt; is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the border holds up
&lt;/h2&gt;

&lt;p&gt;I tried the other shape first: one big instruction file with everything in it. It rotted in two ways.&lt;/p&gt;

&lt;p&gt;First, every sentence that described how something worked went stale when that thing changed, and there was no signal which sentences. Splitting on "does this change when the implementation changes, or only when the design changes" is exactly the signal: the docs get updated with the code, the skills get updated with the design, and the design changes rarely.&lt;/p&gt;

&lt;p&gt;Second, a long file is context spent. A skill that is loaded on every UI task and carries an example of every option costs the same as the code the agent is supposed to be writing. Short skills that route to long docs let the agent spend its context on the problem, and pull the reference in only for the part it is actually touching.&lt;/p&gt;

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

&lt;p&gt;I said in the first piece that greppability matters because agents navigate by search. The skills lean on that. When a skill says "see &lt;code&gt;docs/forms/form-page.md&lt;/code&gt;", the agent opens the file. When it says "&lt;code&gt;FormValidator.FindForm(this)&lt;/code&gt;", the agent greps it and lands in the source. A skill never describes a mechanism the agent cannot then find by name.&lt;/p&gt;

&lt;p&gt;That is also the test I used when writing one. Pick any identifier in the skill and search the package for it. If the search lands on the thing being described, the sentence belongs. If it only lands back in the skill, the sentence is prose about a convention, and conventions are what agents guess at.&lt;/p&gt;

&lt;p&gt;Next: the model itself, and why it fits on one page.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>claudecode</category>
    </item>
    <item>
      <title>Why I stopped handing agents a framework</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Sat, 19 Sep 2026 11:00:44 +0000</pubDate>
      <link>https://dev.to/jgauffin/why-i-stopped-handing-agents-a-framework-3efh</link>
      <guid>https://dev.to/jgauffin/why-i-stopped-handing-agents-a-framework-3efh</guid>
      <description>&lt;p&gt;I have shipped SPAs in Vue and in Angular. Today most of the UI code in my projects is written by a coding agent, and the library underneath is one I wrote myself: &lt;a href="https://www.npmjs.com/package/@relax.js/core" rel="noopener noreferrer"&gt;@relax.js/core&lt;/a&gt;, a small Web Component library with routing, forms, templates and DI, and no virtual DOM.&lt;/p&gt;

&lt;p&gt;That is a strange choice on paper. This series is about why it works, where it does not, and what I had to build to make it work. &lt;/p&gt;

&lt;h2&gt;
  
  
  The case against me
&lt;/h2&gt;

&lt;p&gt;An agent has seen millions of Vue components and Angular modules in training. Its first draft of a Vue SFC is usually right. Its first draft of a Relaxjs component is usually wrong :O It carries habits over. It marks &lt;code&gt;connectedCallback&lt;/code&gt; as &lt;code&gt;async&lt;/code&gt; and expects the browser to wait. It reaches for a reactive store. It adds its own &lt;code&gt;submit&lt;/code&gt; listener next to the one the library already owns.&lt;/p&gt;

&lt;p&gt;The core skill that ships with the library opens with this sentence, because it is the failure mode:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Patterns carried over from React compile, type-check and do nothing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I have not written React myself, but I have watched agents write it unprompted, and it is what they reach for when nobody tells them otherwise.&lt;/p&gt;

&lt;p&gt;There is a second point against me. Angular's compiler type-checks templates. &lt;code&gt;vue-tsc&lt;/code&gt; does the same for Vue SFCs. A typo in a template is a build error before anything runs. In a library where the template is a string, &lt;code&gt;{{user.naem}}&lt;/code&gt; is just a string until the page renders. I will come back to that in a later article, because it bothered me enough to fix.&lt;/p&gt;

&lt;p&gt;So: worse priors, and until recently weaker static checking. Why bother?&lt;/p&gt;

&lt;h2&gt;
  
  
  What an agent actually has
&lt;/h2&gt;

&lt;p&gt;An agent cannot open a browser. It reads files, greps for names, runs the type checker and runs the tests. That is the whole feedback loop. Anything that is only observable at runtime is invisible to it.&lt;/p&gt;

&lt;p&gt;Now think about the bugs I spent the most time on in Vue and Angular, the ones that survived review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;watch&lt;/code&gt; that fired one tick later than I thought, so the DOM showed the previous value.&lt;/li&gt;
&lt;li&gt;A computed that never recalculated because the dependency was read behind a condition.&lt;/li&gt;
&lt;li&gt;Change detection that did not run because the update came from outside the zone.&lt;/li&gt;
&lt;li&gt;An &lt;code&gt;ngOnInit&lt;/code&gt; that assumed an &lt;code&gt;@Input&lt;/code&gt; that arrives on the next cycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are visible in the file where the symptom appears. The cause is in the framework's scheduler, and you find it by opening DevTools, setting a breakpoint and watching. An agent cannot do any of that. It will reason about the file it opened, conclude that the file is correct, and start changing things speculatively. That is the most expensive thing an agent does: producing plausible edits in the wrong place.&lt;/p&gt;

&lt;p&gt;Explicit code does not have that class of bug. When the update is &lt;code&gt;this.nameSpan.textContent = user.name&lt;/code&gt; at the place where &lt;code&gt;user&lt;/code&gt; changed, the update is where the change is. A reviewer can verify it by reading. So can an agent. Nothing decides later whether it ran.&lt;/p&gt;

&lt;h2&gt;
  
  
  What helps the agent
&lt;/h2&gt;

&lt;p&gt;The usual pitch for a small library is "the whole thing fits in the context window". True, and beside the point. An agent rarely needs to read a framework's source; it needs correct memory of the framework's behaviour, and that memory rots with every major version. &lt;/p&gt;

&lt;p&gt;The three properties that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure locality.&lt;/strong&gt; The bug is in the file that shows the symptom. No scheduler, no dependency graph, no zone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Greppability.&lt;/strong&gt; A typed event class is one grep away from every producer and every consumer. &lt;code&gt;r-click="save()"&lt;/code&gt; in a template is one grep away from &lt;code&gt;save&lt;/code&gt;. Agents navigate by search, and a connection that is not a shared literal name is a connection the agent will guess at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reviewability.&lt;/strong&gt; The diff says what will happen. Not what the framework will decide to do with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I had to build
&lt;/h2&gt;

&lt;p&gt;Small and explicit is not enough on its own. Three things turned "an agent can in principle work here" into "an agent does work here":&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Skills, not docs.&lt;/strong&gt; The library ships a set of short files an agent loads before it starts, and every sentence in them is something an agent gets wrong from habit. The docs explain how things work; the skills say what you will get wrong. The border between them is stated, and it is worth reading.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silence turned into errors.&lt;/strong&gt; A template that cannot resolve a path renders an empty string. For a human that is a blank spot on the page; for an agent it is a green test. Every such quiet failure now reports through one error channel, and a test helper turns the channel into assertions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;A checker.&lt;/strong&gt; &lt;code&gt;npx @relax.js/core check&lt;/code&gt; resolves every template expression against the TypeScript types at the call site and prints &lt;code&gt;tsc&lt;/code&gt;-style lines. That closes most of the gap with Angular's template type-checking, and it does so without a compiler in the build.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Then the test seam: &lt;code&gt;mount()&lt;/code&gt;, &lt;code&gt;flush()&lt;/code&gt;, &lt;code&gt;fakeServer()&lt;/code&gt;, &lt;code&gt;mountRouting()&lt;/code&gt;. The agent verifies by running vitest, not by asking me to click.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would still pick the framework
&lt;/h2&gt;

&lt;p&gt;If the agent's zero-shot correctness is the whole game, meaning nobody reviews the diff and no skills get loaded, Vue or Angular wins on priors alone. If the app is large with deeply interdependent state, the reactive engine earns its complexity, and I said as much in the library's own README. If you need SSR, there is nothing here for you.&lt;/p&gt;

&lt;p&gt;For a small-to-medium SPA where a human reads what the agent wrote, I have found the explicit model cheaper every time, and the rest of this series is the evidence. &lt;/p&gt;

&lt;p&gt;Next: what a skill is and why it is not documentation.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>javascript</category>
      <category>webcomponents</category>
    </item>
    <item>
      <title>Live API specs for coding agents</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Sun, 30 Aug 2026 12:33:00 +0000</pubDate>
      <link>https://dev.to/jgauffin/live-api-specs-for-coding-agents-2dcm</link>
      <guid>https://dev.to/jgauffin/live-api-specs-for-coding-agents-2dcm</guid>
      <description>&lt;h1&gt;
  
  
  Live API specs for coding agents
&lt;/h1&gt;

&lt;p&gt;An agent writing frontend code has to know the backend's API. It has three options. It can read the backend source and work out from scratch what the service already publishes. It can ask you, which promotes you to API documentation. Or it can swallow the entire OpenAPI document in order to use one route out of it.&lt;/p&gt;

&lt;p&gt;Then it does the same thing again tomorrow, against a stale &lt;code&gt;swagger.json&lt;/code&gt; you exported last week.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docs-mcpserver&lt;/code&gt; takes the spec straight from the running service, caches it, and serves it one operation at a time.&lt;/p&gt;

&lt;h2&gt;
  
  
  The config
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"libraries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Order handling service"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://localhost:5001/openapi/v1.json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"orders"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; docs-mcpserver
claude mcp add docs &lt;span class="nt"&gt;--&lt;/span&gt; docs-mcpserver &lt;span class="nt"&gt;--config&lt;/span&gt; /path/to/dev-docs.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  One operation, not the whole spec
&lt;/h2&gt;

&lt;p&gt;The agent lists the definitions in &lt;code&gt;orders&lt;/code&gt;, picks the one it needs, and fetches that. For an OpenAPI document the path operations are exposed as definitions named &lt;code&gt;GET /orders/{id}&lt;/code&gt;, so it can also search by keyword.&lt;/p&gt;

&lt;p&gt;A few hundred tokens for the operation it is writing against, instead of the entire document. That keeps working as the service grows, which a pasted spec does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The backend does not have to be running
&lt;/h2&gt;

&lt;p&gt;Every call is answered from the cached spec, never from the network. The fetch happens on startup and then in the background while you work, so an endpoint you added 20 seconds ago is already visible.&lt;/p&gt;

&lt;p&gt;Start the backend once, shut it down, and keep building the frontend. The agent still has real routes and real payload shapes. If the service is down, or answers with something that is not a spec, the last known-good copy keeps being served.&lt;/p&gt;

&lt;p&gt;Code and issues: &lt;a href="https://github.com/jgauffin/dev-docs-mcp" rel="noopener noreferrer"&gt;github.com/jgauffin/dev-docs-mcp&lt;/a&gt;. On npm as &lt;code&gt;docs-mcpserver&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>openapi</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Giving AI agents knowledge they were never trained on</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Thu, 14 May 2026 22:02:53 +0000</pubDate>
      <link>https://dev.to/jgauffin/giving-ai-agents-knowledge-they-were-never-trained-on-5fd7</link>
      <guid>https://dev.to/jgauffin/giving-ai-agents-knowledge-they-were-never-trained-on-5fd7</guid>
      <description>&lt;p&gt;I love coding my own stuff, and my clients typically have lots of internal specifications and libraries to use.&lt;/p&gt;

&lt;p&gt;But since LLMs haven't been trained on that, it's hard to get them to code accurately using those specs, libraries, or frameworks.&lt;/p&gt;

&lt;p&gt;You can, of course, let the agents parse everything, but that wastes tokens and your patience :)&lt;/p&gt;

&lt;p&gt;The same goes for well-known libraries, but you are stuck on a specific version that you must follow. You don't want it to guess the API.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docs-mcpserver&lt;/code&gt; exists to deal with both.&lt;/p&gt;

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

&lt;p&gt;It is an MCP server that provides an agent with accurate knowledge of a framework or specification using documentation as the medium. It reads three kinds of docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Markdown docs&lt;/strong&gt; — your &lt;code&gt;*.md&lt;/code&gt; files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;API reference&lt;/strong&gt; — C# XML documentation, or TypeDoc JSON.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema&lt;/strong&gt; — JSON Schema, OpenAPI 3.x, Swagger 2.0.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What the agent gets out of it is the same in every case: the real names, the real&lt;br&gt;
signatures, the real shapes. Sources can come from a&lt;br&gt;
local folder or straight from a GitHub URL. A single&lt;br&gt;
server instance can host several libraries side by side. For instance, your in-house framework, a client's framework, and a specific version of some public library. &lt;/p&gt;

&lt;p&gt;The agent picks which one to query.&lt;/p&gt;

&lt;p&gt;I personally have used it to code against a specification called DATEX (traffic information for roads), which is HUGE, my own &lt;a href="https://github.com/relax-js/core" rel="noopener noreferrer"&gt;SPA library&lt;/a&gt;, and against sound format specifications for a sound app I'm building.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not just give the agent the files
&lt;/h2&gt;

&lt;p&gt;You could point the agent to the folders and let it read them. The MCP server does a few things that raw file access does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is sandboxed.&lt;/strong&gt; Each source is scoped, with path-traversal protection. The
agent reads what you exposed, nothing else on the disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It reads in pieces.&lt;/strong&gt; Instead of loading a 4000-line reference file, the agent
asks for the table of contents, then pulls the one chapter it needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It searches properly.&lt;/strong&gt; Dedicated search tools with regex and glob support,
instead of the agent improvising its own grep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is self-describing.&lt;/strong&gt; With several libraries configured, the agent calls
one tool to discover what is available. You do not have to spell out every path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub works without cloning.&lt;/strong&gt; Give it a repo URL and it handles the rest.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The multi-library part is the point. Instead of running several MCP servers for&lt;br&gt;
documentation, you get one with a small toolset. No token waste.&lt;/p&gt;
&lt;h2&gt;
  
  
  Setting it up
&lt;/h2&gt;

&lt;p&gt;Install and build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The quick way, a single folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docs-mcpserver ./docs &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"My Docs"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the real use case — several libraries — use a config file. Here is an&lt;br&gt;
in-house framework served from disk, next to a pinned version of a public&lt;br&gt;
library pulled from GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dev-docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Frameworks the model has not been trained on"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cacheDir"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./cache"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"libraries"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"acme-core"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Our internal application framework"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./frameworks/acme-core/docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docs"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disk"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./frameworks/acme-core/api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"api"&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"somelib-3.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SomeLib, pinned to v3.2.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"sources"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"github"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"origin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://github.com/someorg/somelib/tree/v3.2.0/docs"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="nl"&gt;"kind"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"docs"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start it with the config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docs-mcpserver &lt;span class="nt"&gt;--config&lt;/span&gt; dev-docs.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And register it with Claude Code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude mcp add mydocs &lt;span class="nt"&gt;--&lt;/span&gt; node /path/to/markdown-docs-mcp/dist/index.js &lt;span class="nt"&gt;--config&lt;/span&gt; /path/to/dev-docs.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For private GitHub repos, set &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in the environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the agent actually sees
&lt;/h2&gt;

&lt;p&gt;Each library exposes tools based on the &lt;code&gt;kind&lt;/code&gt; of its sources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;docs&lt;/strong&gt; — &lt;code&gt;get_doc_index&lt;/code&gt;, &lt;code&gt;get_sub_index&lt;/code&gt;, &lt;code&gt;read_doc_file&lt;/code&gt;, &lt;code&gt;get_file_toc&lt;/code&gt;,
&lt;code&gt;get_chapters&lt;/code&gt;, &lt;code&gt;search_docs&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;api&lt;/strong&gt; — &lt;code&gt;get_api_index&lt;/code&gt;, &lt;code&gt;get_api_type&lt;/code&gt;, &lt;code&gt;get_api_member&lt;/code&gt;, &lt;code&gt;search_api&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;schema&lt;/strong&gt; — &lt;code&gt;list_schemas&lt;/code&gt;, &lt;code&gt;list_definitions&lt;/code&gt;, &lt;code&gt;get_definition&lt;/code&gt;,
&lt;code&gt;search_definitions&lt;/code&gt;, &lt;code&gt;search_all_schemas&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical run looks like this. The agent calls &lt;code&gt;list_libraries&lt;/code&gt; and sees&lt;br&gt;
&lt;code&gt;acme-core&lt;/code&gt; and &lt;code&gt;somelib-3.2&lt;/code&gt;. It needs to know how &lt;code&gt;acme-core&lt;/code&gt; handles&lt;br&gt;
configuration, so it calls &lt;code&gt;search_docs&lt;/code&gt; with &lt;code&gt;library: "acme-core"&lt;/code&gt;, finds the&lt;br&gt;
right file, asks for its table of contents with &lt;code&gt;get_file_toc&lt;/code&gt;, then pulls the&lt;br&gt;
one relevant section with &lt;code&gt;get_chapters&lt;/code&gt;. It answers the question without ever&lt;br&gt;
loading the whole file.&lt;/p&gt;

&lt;p&gt;When multiple libraries are configured, every tool takes a &lt;code&gt;library&lt;/code&gt; parameter.&lt;br&gt;
When there is only one, the parameter disappears, and the tools behave like a&lt;br&gt;
plain single-library server.&lt;/p&gt;

&lt;p&gt;The same applies to schema sources. For an OpenAPI spec, path operations show up&lt;br&gt;
as definitions named like &lt;code&gt;GET /pets&lt;/code&gt;, so the agent can ask for one endpoint&lt;br&gt;
without reading the whole document. Useful when you want the agent to call your&lt;br&gt;
API correctly rather than guess at the shape of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generating the API input
&lt;/h2&gt;

&lt;p&gt;One thing worth knowing up front: the &lt;code&gt;api&lt;/code&gt; pipeline does not read source code.&lt;br&gt;
It consumes a generated documentation file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript / JavaScript&lt;/strong&gt; — use TypeDoc's JSON serializer:
&lt;code&gt;typedoc --json api.json src/index.ts&lt;/code&gt;. Point the source at that &lt;code&gt;.json&lt;/code&gt; file.
The markdown output from &lt;code&gt;typedoc-plugin-markdown&lt;/code&gt; is not supported — it has to
be the JSON serializer output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;C#&lt;/strong&gt; — enable &lt;code&gt;&amp;lt;GenerateDocumentationFile&amp;gt;true&amp;lt;/GenerateDocumentationFile&amp;gt;&lt;/code&gt;
and point the source at the generated &lt;code&gt;*.xml&lt;/code&gt; file, or the build output folder
that contains it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it does not do
&lt;/h2&gt;

&lt;p&gt;It does not read source code. If you want API reference, you generate the doc&lt;br&gt;
file first, as above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The code is on &lt;a href="https://github.com/jgauffin/dev-docs-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;, or on npm as &lt;code&gt;docs-mcpserver&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Feel free to leave feedback, or check my other MCP servers on &lt;a href="https://github.com/jgauffin" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>llm</category>
    </item>
    <item>
      <title>Exception handling explained</title>
      <dc:creator>Jonas Gauffin</dc:creator>
      <pubDate>Thu, 10 May 2018 11:33:24 +0000</pubDate>
      <link>https://dev.to/jgauffin/exception-handling-explained-ima</link>
      <guid>https://dev.to/jgauffin/exception-handling-explained-ima</guid>
      <description>&lt;p&gt;This article explains what exception handling is and how it differs from&lt;br&gt;
traditional error handling with error codes. The article does not get&lt;br&gt;
into usage or exception classes, but only to explain their purpose.&lt;/p&gt;

&lt;p&gt;Let's start with errors.&lt;/p&gt;
&lt;h1&gt;
  
  
  A brief introduction to error
&lt;/h1&gt;

&lt;p&gt;From the dawn of programming, error codes have been used to deal with errors in applications. An error code is used to indicate if the&lt;br&gt;
executed function was successful or not.&lt;/p&gt;

&lt;p&gt;Here is a simple example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;SaveDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserIdFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetAccountFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;StoreDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example illustrates that when you use error codes, it is the API&lt;br&gt;
consumer that must abort if something fails. You might, but how about&lt;br&gt;
the rest of the team, or those who maintained the application before&lt;br&gt;
you? It is like you would retire the entire police force and expect&lt;br&gt;
&lt;strong&gt;&lt;em&gt;all&lt;/em&gt;&lt;/strong&gt; citizens to behave and be good law abiding citizens.&lt;/p&gt;

&lt;p&gt;One developer could have been lazy and just written (or refactored) the&lt;br&gt;
code as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;SaveDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserIdFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Arne"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetAccountFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Savings account"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;StoreDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem is that the code looks perfectly legal, but silently ignores&lt;br&gt;
errors. If you for instance mistakenly add a white space after all&lt;br&gt;
account names on the account selection page, the code above starts to&lt;br&gt;
fail silently. What's worse is that you will not know about it until&lt;br&gt;
code dependent upon the default account starts to fail, and that can be&lt;br&gt;
after a while. Tracking down that subsequent error can be challenging,&lt;br&gt;
primarily if the default account is set in different ways.&lt;/p&gt;

&lt;p&gt;Error codes are easy to get started with and can be quite powerful. Even&lt;br&gt;
modern languages like Go-lang uses errors instead of exceptions. Here is&lt;br&gt;
a go snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="p"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"filename.ext"&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;err&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fatal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While errors are easy to understand and use, they have three&lt;br&gt;
significant drawbacks:&lt;/p&gt;
&lt;h2&gt;
  
  
  They do not convey context
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;This section is for languages that only uses error codes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For instance, error code 2 means "File Not Found" in Windows. There is&lt;br&gt;
no way to state which file nor other information that might help you to&lt;br&gt;
understand why the file was missing. Error codes are just that. Codes&lt;br&gt;
that indicate a specific error, without context or clues.&lt;/p&gt;

&lt;p&gt;The problem with that is that it is hard to understand why or under what&lt;br&gt;
circumstances that the error occurred. The Windows API solves this by&lt;br&gt;
introducing a method called &lt;code&gt;GetLastError()&lt;/code&gt; which is used to get more&lt;br&gt;
information about the error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;OFSTRUCT&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;HFILE&lt;/span&gt; &lt;span class="n"&gt;hFile&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;OpenFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"d:\\sample.txt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OF_READ&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hFile&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;HFILE_ERROR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// all this is required to get the error message.&lt;/span&gt;
    &lt;span class="c1"&gt;// you typically add it to a separate function&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the error code, as hFile only indicates an error&lt;/span&gt;
    &lt;span class="c1"&gt;// but not which one.&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;errorCode&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetLastError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="c1"&gt;// Get the generic error message which&lt;/span&gt;
    &lt;span class="c1"&gt;// represents the above error code.&lt;/span&gt;
    &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpMsgBuf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;LPVOID&lt;/span&gt; &lt;span class="n"&gt;lpDisplayBuf&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nf"&gt;FormatMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;FORMAT_MESSAGE_ALLOCATE_BUFFER&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt; 
        &lt;span class="n"&gt;FORMAT_MESSAGE_FROM_SYSTEM&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;FORMAT_MESSAGE_IGNORE_INSERTS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;errorCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nf"&gt;MAKELANGID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LANG_NEUTRAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUBLANG_DEFAULT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LPTSTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;lpMsgBuf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NULL&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My point is that an error code is not enough when you want to&lt;br&gt;
&lt;em&gt;solve&lt;/em&gt; the error.&lt;/p&gt;
&lt;h2&gt;
  
  
  The burden is on the function caller
&lt;/h2&gt;

&lt;p&gt;It is safe to say that all applications have errors, it is exceptionally&lt;br&gt;
rare (pun intended) that errors can be ignored. Ignoring errors might&lt;br&gt;
seem to work, but sooner or later consequential problems will surface.&lt;br&gt;
It will be much harder to find the root cause since the found error is&lt;br&gt;
just a consequence of the first one. Any kind of "fix" is just a&lt;br&gt;
workaround which clutters the code base but does not prevent the root&lt;br&gt;
cause from happening again.&lt;/p&gt;

&lt;p&gt;It is crucial that all errors are handled in your code. When you use&lt;br&gt;
error codes, it is so easy to ignore or forget errors. Had a tight&lt;br&gt;
deadline? Wrestled with an obscure bug or incomprehensible requirements?&lt;br&gt;
Those situations make it so easy to take a shortcut. If not all&lt;br&gt;
developers on your team have the same discipline, errors will get&lt;br&gt;
ignored.&lt;/p&gt;
&lt;h1&gt;
  
  
  Enter exceptions
&lt;/h1&gt;

&lt;p&gt;Exceptions are for exceptional situations. If something didn't go as&lt;br&gt;
expected, you got an exceptional situation.&lt;/p&gt;

&lt;p&gt;Sounds easy, huh?&lt;/p&gt;

&lt;p&gt;But what does that mean?&lt;/p&gt;
&lt;h2&gt;
  
  
  An exceptional example
&lt;/h2&gt;

&lt;p&gt;If you expect that something can fail, you should guard against that&lt;br&gt;
situation.&lt;/p&gt;

&lt;p&gt;Here is an example:&lt;/p&gt;

&lt;p&gt;Let's say that you love shopping the newest and hottest technical&lt;br&gt;
gadgets. When the new gadget is released, you want to be first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fo83dfiz9eacxmggbl7pf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthepracticaldev.s3.amazonaws.com%2Fi%2Fo83dfiz9eacxmggbl7pf.jpg"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Scenario 1
&lt;/h3&gt;

&lt;p&gt;If you are like most of us, you can probably not just go on a shopping&lt;br&gt;
spree. You need to make sure that you have enough money.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Check your bank account.&lt;/li&gt;
&lt;li&gt; Go shopping.&lt;/li&gt;
&lt;li&gt; Pay&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Scenario 2
&lt;/h3&gt;

&lt;p&gt;However, if you are fortunate enough to have plenty of money, you can&lt;br&gt;
go shopping directly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go shopping.&lt;/li&gt;
&lt;li&gt; Pay&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  The difference
&lt;/h3&gt;

&lt;p&gt;In the first scenario, we have a known issue that we need to deal with:&lt;br&gt;
A money limit. Therefore, we always need to check that we have enough&lt;br&gt;
money. In the second scenario, we should have enough money.&lt;/p&gt;

&lt;p&gt;What happens if someone has hacked us in scenario 2:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Someone hacked us&lt;/li&gt;
&lt;li&gt; Go shopping&lt;/li&gt;
&lt;li&gt; Pay &amp;lt;-- Failed, no money&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In that case, we got an exception, since it is a case that shouldn't&lt;br&gt;
happen since we &lt;strong&gt;&lt;em&gt;expect&lt;/em&gt;&lt;/strong&gt; to have enough money.&lt;/p&gt;

&lt;p&gt;That is an exceptional situation.&lt;/p&gt;
&lt;h3&gt;
  
  
  &lt;em&gt;Why can't we always write the code like in scenario 1?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;First, it's about communicating intent. We mostly get a set of business&lt;br&gt;
requirements that we should fulfill. They tell us what to expect when&lt;br&gt;
implementing different use cases. If we go and add many checks for&lt;br&gt;
things that might, but should not, happen we lose the connection between&lt;br&gt;
our use cases and the code. It will be hard to tell the intent of the&lt;br&gt;
code, which in turn lead to assumptions and in the end decreased code&lt;br&gt;
quality.&lt;/p&gt;

&lt;p&gt;Second, if we add many checks we are coding workarounds as the real&lt;br&gt;
problem is that our account was hacked, not that we could not pay. By&lt;br&gt;
adding checks and abort instead of paying we are hiding that fact.&lt;/p&gt;
&lt;h2&gt;
  
  
  What exceptions are
&lt;/h2&gt;

&lt;p&gt;Exceptions are for situations that wasn't considered when defining what&lt;br&gt;
the application should do. When writing a messaging library for message&lt;br&gt;
queues you expect to receive complete messages, but when you write one&lt;br&gt;
for TCP you expect to receive partial messages. What's exceptional in&lt;br&gt;
one case doesn't necessarily have to be exceptional in another.&lt;/p&gt;

&lt;p&gt;If you would open a file there are several errors that can happen.&lt;br&gt;
Non-existent directory, file is missing, access denied, partial file,&lt;br&gt;
etc. All those errors are known to most programmers, so they are not&lt;br&gt;
exceptions, right?&lt;/p&gt;

&lt;p&gt;Wrong. In most cases, they are exceptions. Because you typically do&lt;br&gt;
expect that a file exists and that it's complete and readable. Well, if&lt;br&gt;
you are writing a data forensics application, most of those errors are&lt;br&gt;
expected and should be dealt with (and therefore not exceptional cases).&lt;/p&gt;
&lt;h3&gt;
  
  
  Exceptions exist to prevent your application from doing something stupid.
&lt;/h3&gt;

&lt;p&gt;It's crucial for you to understand that. Don't think of exceptions as&lt;br&gt;
something you can use to control your application when coding. Think of&lt;br&gt;
exceptions as a mechanism to guard against your application doing&lt;br&gt;
something wrong/unexpected.&lt;/p&gt;
&lt;h3&gt;
  
  
  Exceptions exist to help you fix problems in the future
&lt;/h3&gt;

&lt;p&gt;Since exceptions are not a flow control mechanism, they do add little&lt;br&gt;
value when executing your code (previously described point excluded).&lt;/p&gt;

&lt;p&gt;However, writing informative exception messages makes it much easier to&lt;br&gt;
correct future bugs, since they add context to the error. Always try to&lt;br&gt;
do so, your future self will thank you for it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Code example
&lt;/h2&gt;

&lt;p&gt;Let's take the same code as was found in the beginning of this article,&lt;br&gt;
but changed to use exception handling instead.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;SaveDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;userName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;accountName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetUserIdFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Arne"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetAccountFromName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Savings account"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;StoreDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, the method now returns void as it does not need to&lt;br&gt;
indicate that everything went successfully. Nor does it need to validate&lt;br&gt;
error codes from the called methods. One can safely assume that an&lt;br&gt;
exception abort the processing if the expected result cannot be&lt;br&gt;
guaranteed.&lt;/p&gt;

&lt;p&gt;In fact, in most cases, we do not have to care if exceptions are thrown&lt;br&gt;
at all. Remember, exceptions are used to communicate that something&lt;br&gt;
unexpected happened. If we cannot predict it, how on earth could we be&lt;br&gt;
able to handle the exception?&lt;/p&gt;

&lt;p&gt;Let's look at the &lt;code&gt;StoreDefaultAccount&lt;/code&gt; method. The most important thing&lt;br&gt;
to understand is that the method says that a default account should be&lt;br&gt;
stored successfully. Since that is the method promise, we must throw an&lt;br&gt;
exception every time we find something that would prevent the default&lt;br&gt;
account from being stored.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;StoreDefaultAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"A valid user id must be specified."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentOutOfRangeException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"A valid account id must be specified."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_accountRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OwnerId&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"User &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; do not own account &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultAccount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;accountId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;_userRepository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first two exceptions are used to mitigate errors like parse errors&lt;br&gt;
or invalid data.&lt;/p&gt;

&lt;p&gt;The third exception is for a business rule. We may only use the user's&lt;br&gt;
own accounts as default accounts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;_accountRepository.GetById(accountId);&lt;/code&gt; will in turn throw an&lt;br&gt;
exception if the given accountId do not exist in the database since the&lt;br&gt;
method name states that an account should be fetched.&lt;/p&gt;

&lt;h1&gt;
  
  
  Summary
&lt;/h1&gt;

&lt;p&gt;The purpose of exceptions is not to allow you to take different actions&lt;br&gt;
depending on if something failed or not. i.e.&amp;nbsp;they are not a flow&lt;br&gt;
control mechanism. Instead, exceptions are used to make sure that your&lt;br&gt;
application delivers the expected result (or die trying).&lt;/p&gt;

&lt;p&gt;With that in mind, I hope that you find them as useful as I do. With&lt;br&gt;
the right mindset (and using exceptions) you can save much time since&lt;br&gt;
you do not have to track down why your database has a lot of data&lt;br&gt;
inconsistencies (which leads to bugs later).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This article is part of our exception handling series. To learn more, visit our &lt;a href="https://coderr.io/exception-handling/" rel="noopener noreferrer"&gt;website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>exceptions</category>
      <category>errors</category>
    </item>
  </channel>
</rss>
